Skip to content

Commit f06140b

Browse files
authored
Merge commit from fork
Add validation to the version string passed to npm install commands. Npm has a multitude of handy ways to specify the source for a module install, but we want to install modules only from the official npm repository.
1 parent de74bb5 commit f06140b

2 files changed

Lines changed: 106 additions & 4 deletions

File tree

src/modules.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -166,15 +166,20 @@ export function restoreModules(
166166
runNpm(config, null, null, 'remove', onData, onErr, onClose)
167167
}
168168

169-
function runNpm(
169+
export function runNpm(
170170
config: Config,
171171
name: any,
172-
version: any,
172+
version: string | null,
173173
command: string,
174174
onData: (output: any) => any,
175175
onErr: (err: Error) => any,
176176
onClose: (code: number) => any
177177
) {
178+
if (version && version !== '' && !semver.valid(version)) {
179+
onErr(new Error('Invalid version: ' + version))
180+
onClose(-1)
181+
return
182+
}
178183
let npm
179184

180185
const opts: { cwd?: string } = {}
@@ -391,5 +396,6 @@ module.exports = {
391396
getAuthor,
392397
getKeywords,
393398
restoreModules,
394-
importOrRequire
399+
importOrRequire,
400+
runNpm
395401
}

test/modules.js

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ const {
66
modulesWithKeyword,
77
checkForNewServerVersion,
88
getLatestServerVersion,
9-
importOrRequire
9+
importOrRequire,
10+
runNpm
1011
} = require('../dist/modules')
1112

1213
describe('modulesWithKeyword', () => {
@@ -212,3 +213,98 @@ describe('importOrRequire', () => {
212213
chai.expect(mod).to.be.a('function')
213214
})
214215
})
216+
217+
describe('runNpm version validation', () => {
218+
const config = {
219+
configPath: '/tmp',
220+
name: 'signalk-server'
221+
}
222+
223+
const testVersion = (version, shouldPass) => {
224+
return new Promise((resolve, reject) => {
225+
let errCalled = false
226+
const onErr = (err) => {
227+
errCalled = true
228+
if (shouldPass) {
229+
reject(
230+
new Error(`Should have passed but failed with: ${err.message}`)
231+
)
232+
} else {
233+
chai.expect(err.message).to.contain('Invalid version')
234+
resolve()
235+
}
236+
}
237+
238+
const onClose = (code) => {
239+
if (shouldPass && !errCalled) {
240+
resolve()
241+
} else if (!shouldPass && !errCalled) {
242+
reject(new Error(`Should have failed but passed (code ${code})`))
243+
}
244+
}
245+
246+
// We mock spawn to do nothing if validation passes
247+
const originalSpawn = require('child_process').spawn
248+
require('child_process').spawn = () => ({
249+
stdout: { on: () => {} },
250+
stderr: { on: () => {} },
251+
on: (event, cb) => {
252+
if (event === 'close') cb(0)
253+
}
254+
})
255+
256+
try {
257+
runNpm(
258+
config,
259+
'some-package',
260+
version,
261+
'install',
262+
() => {},
263+
onErr,
264+
onClose
265+
)
266+
} finally {
267+
require('child_process').spawn = originalSpawn
268+
}
269+
})
270+
}
271+
272+
it('should accept valid semantic versions', () => {
273+
return testVersion('1.0.0', true)
274+
})
275+
276+
it('should accept valid prerelease versions', () => {
277+
return testVersion('1.0.0-alpha.1', true)
278+
})
279+
280+
it('should accept empty version', () => {
281+
return testVersion('', true)
282+
})
283+
284+
it('should reject URL encoded http URL', () => {
285+
return testVersion('http:%2F%2Fattacker.com%2Fpkg.tgz', false)
286+
})
287+
288+
it('should reject URL encoded git URL', () => {
289+
return testVersion(
290+
'git%2Bhttps:%2F%2Fattacker.com%2Fmalicious-plugin.git',
291+
false
292+
)
293+
})
294+
295+
it('should reject scoped package path', () => {
296+
return testVersion('attacker%2Fmalicious-plugin', false)
297+
})
298+
299+
it('should reject npm alias', () => {
300+
return testVersion('npm:malicious-package@1.0.0', false)
301+
})
302+
303+
it('should reject plain http URL', () => {
304+
return testVersion('http://attacker.com/pkg.tgz', false)
305+
})
306+
307+
it('should reject plain git URL', () => {
308+
return testVersion('git+https://attacker.com/malicious-plugin.git', false)
309+
})
310+
})

0 commit comments

Comments
 (0)