Signal K Server Vulnerable to Remote Code Execution via Malicious npm Package
Description
Signal K Server is a server application that runs on a central hub in a boat. Versions prior to 2.19.0 of the appstore interface allow administrators to install npm packages through a REST API endpoint. While the endpoint validates that the package name exists in the npm registry as a known plugin or webapp, the version parameter accepts arbitrary npm version specifiers including URLs. npm supports installing packages from git repositories, GitHub shorthand syntax, and HTTP/HTTPS URLs pointing to tarballs. When npm installs a package, it can automatically execute any postinstall script defined in package.json, enabling arbitrary code execution. The vulnerability exists because npm's version specifier syntax is extremely flexible, and the SignalK code passes the version parameter directly to npm without sanitization. An attacker with admin access can install a package from an attacker-controlled source containing a malicious postinstall script. Version 2.19.0 contains a patch for the issue.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
signalk-servernpm | < 2.9.0 | 2.9.0 |
Affected products
1- Range: 0.1.1, 0.1.10, 0.1.11, …
Patches
1f06140bed702Merge commit from fork
2 files changed · +106 −4
src/modules.ts+9 −3 modified@@ -166,15 +166,20 @@ export function restoreModules( runNpm(config, null, null, 'remove', onData, onErr, onClose) } -function runNpm( +export function runNpm( config: Config, name: any, - version: any, + version: string | null, command: string, onData: (output: any) => any, onErr: (err: Error) => any, onClose: (code: number) => any ) { + if (version && version !== '' && !semver.valid(version)) { + onErr(new Error('Invalid version: ' + version)) + onClose(-1) + return + } let npm const opts: { cwd?: string } = {} @@ -391,5 +396,6 @@ module.exports = { getAuthor, getKeywords, restoreModules, - importOrRequire + importOrRequire, + runNpm }
test/modules.js+97 −1 modified@@ -6,7 +6,8 @@ const { modulesWithKeyword, checkForNewServerVersion, getLatestServerVersion, - importOrRequire + importOrRequire, + runNpm } = require('../dist/modules') describe('modulesWithKeyword', () => { @@ -212,3 +213,98 @@ describe('importOrRequire', () => { chai.expect(mod).to.be.a('function') }) }) + +describe('runNpm version validation', () => { + const config = { + configPath: '/tmp', + name: 'signalk-server' + } + + const testVersion = (version, shouldPass) => { + return new Promise((resolve, reject) => { + let errCalled = false + const onErr = (err) => { + errCalled = true + if (shouldPass) { + reject( + new Error(`Should have passed but failed with: ${err.message}`) + ) + } else { + chai.expect(err.message).to.contain('Invalid version') + resolve() + } + } + + const onClose = (code) => { + if (shouldPass && !errCalled) { + resolve() + } else if (!shouldPass && !errCalled) { + reject(new Error(`Should have failed but passed (code ${code})`)) + } + } + + // We mock spawn to do nothing if validation passes + const originalSpawn = require('child_process').spawn + require('child_process').spawn = () => ({ + stdout: { on: () => {} }, + stderr: { on: () => {} }, + on: (event, cb) => { + if (event === 'close') cb(0) + } + }) + + try { + runNpm( + config, + 'some-package', + version, + 'install', + () => {}, + onErr, + onClose + ) + } finally { + require('child_process').spawn = originalSpawn + } + }) + } + + it('should accept valid semantic versions', () => { + return testVersion('1.0.0', true) + }) + + it('should accept valid prerelease versions', () => { + return testVersion('1.0.0-alpha.1', true) + }) + + it('should accept empty version', () => { + return testVersion('', true) + }) + + it('should reject URL encoded http URL', () => { + return testVersion('http:%2F%2Fattacker.com%2Fpkg.tgz', false) + }) + + it('should reject URL encoded git URL', () => { + return testVersion( + 'git%2Bhttps:%2F%2Fattacker.com%2Fmalicious-plugin.git', + false + ) + }) + + it('should reject scoped package path', () => { + return testVersion('attacker%2Fmalicious-plugin', false) + }) + + it('should reject npm alias', () => { + return testVersion('npm:malicious-package@1.0.0', false) + }) + + it('should reject plain http URL', () => { + return testVersion('http://attacker.com/pkg.tgz', false) + }) + + it('should reject plain git URL', () => { + return testVersion('git+https://attacker.com/malicious-plugin.git', false) + }) +})
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-93jc-vqqc-vvvhghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-68619ghsaADVISORY
- github.com/SignalK/signalk-server/commit/f06140bed702de93a5dbb6b33dc2486960764d1dghsaWEB
- github.com/SignalK/signalk-server/releases/tag/v2.19.0ghsax_refsource_MISCWEB
- github.com/SignalK/signalk-server/security/advisories/GHSA-93jc-vqqc-vvvhghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.