VYPR
High severityOSV Advisory· Published Jan 1, 2026· Updated Jan 2, 2026

Signal K Server Vulnerable to Remote Code Execution via Malicious npm Package

CVE-2025-68619

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.

PackageAffected versionsPatched versions
signalk-servernpm
< 2.9.02.9.0

Affected products

1

Patches

1
f06140bed702

Merge commit from fork

https://github.com/SignalK/signalk-serverTeppo KurkiDec 27, 2025via ghsa
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

News mentions

0

No linked articles in our index yet.