High severity7.5NVD Advisory· Published Jul 2, 2016· Updated May 6, 2026
CVE-2016-3956
CVE-2016-3956
Description
The CLI in npm before 2.15.1 and 3.x before 3.8.3, as used in Node.js 0.10 before 0.10.44, 0.12 before 0.12.13, 4 before 4.4.2, and 5 before 5.10.0, includes bearer tokens with arbitrary requests, which allows remote HTTP servers to obtain sensitive information by reading Authorization headers.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
npmnpm | < 2.15.1 | 2.15.1 |
npmnpm | >= 3.0.0, < 3.8.3 | 3.8.3 |
Patches
2fea8cc92cee0config: only send token to registry hosts
7 files changed · +268 −27
doc/cli/npm-adduser.md+5 −3 modified@@ -59,9 +59,11 @@ registries. Can be used with `--registry` and / or `--scope`, e.g. npm adduser --registry=http://private-registry.example.com --always-auth This will ensure that all requests to that registry (including for tarballs) -include an authorization header. See `always-auth` in `npm-config(7)` for more -details on always-auth. Registry-specific configuration of `always-auth` takes -precedence over any global configuration. +include an authorization header. This setting may be necessary for use with +private registries where metadata and package tarballs are stored on hosts with +different hostnames. See `always-auth` in `npm-config(7)` for more details on +always-auth. Registry-specific configuration of `always-auth` takes precedence +over any global configuration. ## SEE ALSO
lib/cache.js+1 −1 modified@@ -288,7 +288,7 @@ function add (args, where, cb) { break case "remote": // get auth, if possible - mapToRegistry(spec, npm.config, function (err, uri, auth) { + mapToRegistry(p.raw, npm.config, function (err, uri, auth) { if (err) return cb(err) addRemoteTarball(p.spec, {name : p.name}, null, auth, cb)
lib/config/get-credentials-by-uri.js+10 −9 modified@@ -20,8 +20,16 @@ function getCredentialsByURI (uri) { alwaysAuth : undefined } - if (this.get(nerfed + ":_authToken")) { - c.token = this.get(nerfed + ":_authToken") + // used to override scope matching for tokens as well as legacy auth + if (this.get(nerfed + ':always-auth') !== undefined) { + var val = this.get(nerfed + ':always-auth') + c.alwaysAuth = val === 'false' ? false : !!val + } else if (this.get('always-auth') !== undefined) { + c.alwaysAuth = this.get('always-auth') + } + + if (this.get(nerfed + ':_authToken')) { + c.token = this.get(nerfed + ':_authToken') // the bearer token is enough, don't confuse things return c } @@ -58,13 +66,6 @@ function getCredentialsByURI (uri) { c.email = this.get("email") } - if (this.get(nerfed + ":always-auth") !== undefined) { - var val = this.get(nerfed + ":always-auth") - c.alwaysAuth = val === "false" ? false : !!val - } else if (this.get("always-auth") !== undefined) { - c.alwaysAuth = this.get("always-auth") - } - if (c.username && c.password) { c.auth = new Buffer(c.username + ":" + c.password).toString("base64") }
lib/utils/map-to-registry.js+48 −4 modified@@ -48,9 +48,53 @@ function mapToRegistry(name, config, cb) { var auth = config.getCredentialsByURI(registry) // normalize registry URL so resolution doesn't drop a piece of registry URL - var normalized = registry.slice(-1) !== "/" ? registry+"/" : registry - var uri = url.resolve(normalized, name) - log.silly("mapToRegistry", "uri", uri) + var normalized = registry.slice(-1) !== '/' ? registry + '/' : registry + var uri + log.silly('mapToRegistry', 'data', data) + if (data.type === 'remote') { + uri = data.spec + } else { + uri = url.resolve(normalized, name) + } + + log.silly('mapToRegistry', 'uri', uri) + + cb(null, uri, scopeAuth(uri, registry, auth), normalized) +} + +function scopeAuth (uri, registry, auth) { + var cleaned = { + scope: auth.scope, + email: auth.email, + alwaysAuth: auth.alwaysAuth, + token: undefined, + username: undefined, + password: undefined, + auth: undefined + } + + var requestHost + var registryHost + + if (auth.token || auth.auth || (auth.username && auth.password)) { + requestHost = url.parse(uri).hostname + registryHost = url.parse(registry).hostname + + if (requestHost === registryHost) { + cleaned.token = auth.token + cleaned.auth = auth.auth + cleaned.username = auth.username + cleaned.password = auth.password + } else if (auth.alwaysAuth) { + log.verbose('scopeAuth', 'alwaysAuth set for', registry) + cleaned.token = auth.token + cleaned.auth = auth.auth + cleaned.username = auth.username + cleaned.password = auth.password + } else { + log.silly('scopeAuth', uri, "doesn't share host with registry", registry) + } + } - cb(null, uri, auth, normalized) + return cleaned }
test/tap/bearer-token-check.js+119 −0 added@@ -0,0 +1,119 @@ +var resolve = require('path').resolve +var writeFileSync = require('graceful-fs').writeFileSync + +var mkdirp = require('mkdirp') +var mr = require('npm-registry-mock') +var osenv = require('osenv') +var rimraf = require('rimraf') +var test = require('tap').test + +var common = require('../common-tap.js') +var toNerfDart = require('../../lib/config/nerf-dart.js') + +var pkg = resolve(__dirname, 'install-bearer-check') +var outfile = resolve(pkg, '_npmrc') +var modules = resolve(pkg, 'node_modules') +var tarballPath = '/scoped-underscore/-/scoped-underscore-1.3.1.tgz' +// needs to be a different hostname to verify tokens (not) being sent correctly +var tarballURL = 'http://lvh.me:' + common.port + tarballPath +var tarball = resolve(__dirname, '../fixtures/scoped-underscore-1.3.1.tgz') + +var server + +var EXEC_OPTS = { cwd: pkg } + +function mocks (server) { + var auth = 'Bearer 0xabad1dea' + server.get(tarballPath, { authorization: auth }).reply(403, { + error: 'token leakage', + reason: 'This token should not be sent.' + }) + server.get(tarballPath).replyWithFile(200, tarball) +} + +test('setup', function (t) { + mr({ port: common.port, plugin: mocks }, function (er, s) { + server = s + t.ok(s, 'set up mock registry') + setup() + t.end() + }) +}) + +test('authed npm install with tarball not on registry', function (t) { + common.npm( + [ + 'install', + '--loglevel', 'silent', + '--json', + '--fetch-retries', 0, + '--userconfig', outfile + ], + EXEC_OPTS, + function (err, code, stdout, stderr) { + t.ifError(err, 'test runner executed without error') + t.equal(code, 0, 'npm install exited OK') + t.notOk(stderr, 'no output on stderr') + try { + var results = JSON.parse(stdout) + } catch (ex) { + console.error('#', ex) + t.ifError(ex, 'stdout was valid JSON') + } + + if (results) { + var installedversion = { + 'name': '@scoped/underscore', + 'version': '1.3.1', + 'from': 'http://lvh.me:1337/scoped-underscore/-/scoped-underscore-1.3.1.tgz', + 'dependencies': {} + } + t.isDeeply(results[0], installedversion, '@scoped/underscore installed') + } + + t.end() + } + ) +}) + +test('cleanup', function (t) { + server.close() + cleanup() + t.end() +}) + +var contents = '@scoped:registry=' + common.registry + '\n' + + toNerfDart(common.registry) + ':_authToken=0xabad1dea\n' + +var json = { + name: 'test-package-install', + version: '1.0.0' +} + +var shrinkwrap = { + name: 'test-package-install', + version: '1.0.0', + dependencies: { + '@scoped/underscore': { + resolved: tarballURL, + from: '>=1.3.1 <2', + version: '1.3.1' + } + } +} + +function setup () { + cleanup() + mkdirp.sync(modules) + writeFileSync(resolve(pkg, 'package.json'), JSON.stringify(json, null, 2) + '\n') + writeFileSync(outfile, contents) + writeFileSync( + resolve(pkg, 'npm-shrinkwrap.json'), + JSON.stringify(shrinkwrap, null, 2) + '\n' + ) +} + +function cleanup () { + process.chdir(osenv.tmpdir()) + rimraf.sync(pkg) +}
test/tap/config-credentials.js+7 −7 modified@@ -73,13 +73,13 @@ test("set with token", function (t) { }, "needs only token") var expected = { - scope : "//registry.lvh.me:8661/", - token : "simple-token", - username : undefined, - password : undefined, - email : undefined, - auth : undefined, - alwaysAuth : undefined + scope: '//registry.lvh.me:8661/', + token: 'simple-token', + username: undefined, + password: undefined, + email: undefined, + auth: undefined, + alwaysAuth: false } t.same(conf.getCredentialsByURI(URI), expected, "got bearer token and scope")
test/tap/map-to-registry.js+78 −3 modified@@ -48,7 +48,7 @@ test('mapRegistryToURI', function (t) { password: undefined, email: undefined, auth: undefined, - alwaysAuth: undefined + alwaysAuth: false }) t.equal(registry, 'http://reg.npm/design/-/rewrite/') }) @@ -66,7 +66,7 @@ test('mapRegistryToURI', function (t) { password: undefined, email: undefined, auth: undefined, - alwaysAuth: undefined + alwaysAuth: false }) t.equal(registry, 'http://reg.npm/-/rewrite/') }) @@ -84,8 +84,83 @@ test('mapRegistryToURI', function (t) { password: undefined, email: undefined, auth: undefined, - alwaysAuth: undefined + alwaysAuth: false }) t.equal(registry, 'http://reg.npm/design/-/rewrite/relative/') }) }) + +test('mapToRegistry token scoping', function (t) { + npm.config.set('scope', '') + npm.config.set('registry', 'https://reg.npm/') + npm.config.set('//reg.npm/:_authToken', 'r-token') + + t.test('pass token to registry host', function (t) { + mapRegistry( + 'https://reg.npm/packages/e/easy-1.0.0.tgz', + npm.config, + function (er, uri, auth, registry) { + t.ifError(er, 'mapRegistryToURI worked') + t.equal(uri, 'https://reg.npm/packages/e/easy-1.0.0.tgz') + t.deepEqual(auth, { + scope: '//reg.npm/', + token: 'r-token', + username: undefined, + password: undefined, + email: undefined, + auth: undefined, + alwaysAuth: false + }) + t.equal(registry, 'https://reg.npm/') + } + ) + t.end() + }) + + t.test("don't pass token to non-registry host", function (t) { + mapRegistry( + 'https://butts.lol/packages/e/easy-1.0.0.tgz', + npm.config, + function (er, uri, auth, registry) { + t.ifError(er, 'mapRegistryToURI worked') + t.equal(uri, 'https://butts.lol/packages/e/easy-1.0.0.tgz') + t.deepEqual(auth, { + scope: '//reg.npm/', + token: undefined, + username: undefined, + password: undefined, + email: undefined, + auth: undefined, + alwaysAuth: false + }) + t.equal(registry, 'https://reg.npm/') + } + ) + t.end() + }) + + t.test('pass token to non-registry host with always-auth', function (t) { + npm.config.set('always-auth', true) + mapRegistry( + 'https://butts.lol/packages/e/easy-1.0.0.tgz', + npm.config, + function (er, uri, auth, registry) { + t.ifError(er, 'mapRegistryToURI worked') + t.equal(uri, 'https://butts.lol/packages/e/easy-1.0.0.tgz') + t.deepEqual(auth, { + scope: '//reg.npm/', + token: 'r-token', + username: undefined, + password: undefined, + email: undefined, + auth: undefined, + alwaysAuth: true + }) + t.equal(registry, 'https://reg.npm/') + } + ) + t.end() + }) + + t.end() +})
f67ecad59e99config: only send token to registry hosts
7 files changed · +257 −17
doc/cli/npm-adduser.md+5 −3 modified@@ -59,9 +59,11 @@ registries. Can be used with `--registry` and / or `--scope`, e.g. npm adduser --registry=http://private-registry.example.com --always-auth This will ensure that all requests to that registry (including for tarballs) -include an authorization header. See `always-auth` in `npm-config(7)` for more -details on always-auth. Registry-specific configuration of `always-auth` takes -precedence over any global configuration. +include an authorization header. This setting may be necessary for use with +private registries where metadata and package tarballs are stored on hosts with +different hostnames. See `always-auth` in `npm-config(7)` for more details on +always-auth. Registry-specific configuration of `always-auth` takes precedence +over any global configuration. ## SEE ALSO
lib/cache.js+1 −1 modified@@ -285,7 +285,7 @@ function add (args, where, cb) { break case 'remote': // get auth, if possible - mapToRegistry(spec, npm.config, function (err, uri, auth) { + mapToRegistry(p.raw, npm.config, function (err, uri, auth) { if (err) return cb(err) addRemoteTarball(p.spec, { name: p.name }, null, auth, cb)
lib/config/get-credentials-by-uri.js+8 −7 modified@@ -20,6 +20,14 @@ function getCredentialsByURI (uri) { alwaysAuth: undefined } + // used to override scope matching for tokens as well as legacy auth + if (this.get(nerfed + ':always-auth') !== undefined) { + var val = this.get(nerfed + ':always-auth') + c.alwaysAuth = val === 'false' ? false : !!val + } else if (this.get('always-auth') !== undefined) { + c.alwaysAuth = this.get('always-auth') + } + if (this.get(nerfed + ':_authToken')) { c.token = this.get(nerfed + ':_authToken') // the bearer token is enough, don't confuse things @@ -58,13 +66,6 @@ function getCredentialsByURI (uri) { c.email = this.get('email') } - if (this.get(nerfed + ':always-auth') !== undefined) { - var val = this.get(nerfed + ':always-auth') - c.alwaysAuth = val === 'false' ? false : !!val - } else if (this.get('always-auth') !== undefined) { - c.alwaysAuth = this.get('always-auth') - } - if (c.username && c.password) { c.auth = new Buffer(c.username + ':' + c.password).toString('base64') }
lib/utils/map-to-registry.js+46 −2 modified@@ -49,8 +49,52 @@ function mapToRegistry (name, config, cb) { // normalize registry URL so resolution doesn't drop a piece of registry URL var normalized = registry.slice(-1) !== '/' ? registry + '/' : registry - var uri = url.resolve(normalized, name) + var uri + log.silly('mapToRegistry', 'data', data) + if (data.type === 'remote') { + uri = data.spec + } else { + uri = url.resolve(normalized, name) + } + log.silly('mapToRegistry', 'uri', uri) - cb(null, uri, auth, normalized) + cb(null, uri, scopeAuth(uri, registry, auth), normalized) +} + +function scopeAuth (uri, registry, auth) { + var cleaned = { + scope: auth.scope, + email: auth.email, + alwaysAuth: auth.alwaysAuth, + token: undefined, + username: undefined, + password: undefined, + auth: undefined + } + + var requestHost + var registryHost + + if (auth.token || auth.auth || (auth.username && auth.password)) { + requestHost = url.parse(uri).hostname + registryHost = url.parse(registry).hostname + + if (requestHost === registryHost) { + cleaned.token = auth.token + cleaned.auth = auth.auth + cleaned.username = auth.username + cleaned.password = auth.password + } else if (auth.alwaysAuth) { + log.verbose('scopeAuth', 'alwaysAuth set for', registry) + cleaned.token = auth.token + cleaned.auth = auth.auth + cleaned.username = auth.username + cleaned.password = auth.password + } else { + log.silly('scopeAuth', uri, "doesn't share host with registry", registry) + } + } + + return cleaned }
test/tap/bearer-token-check.js+118 −0 added@@ -0,0 +1,118 @@ +var resolve = require('path').resolve +var writeFileSync = require('graceful-fs').writeFileSync + +var mkdirp = require('mkdirp') +var mr = require('npm-registry-mock') +var osenv = require('osenv') +var rimraf = require('rimraf') +var test = require('tap').test + +var common = require('../common-tap.js') +var toNerfDart = require('../../lib/config/nerf-dart.js') + +var pkg = resolve(__dirname, 'install-bearer-check') +var outfile = resolve(pkg, '_npmrc') +var modules = resolve(pkg, 'node_modules') +var tarballPath = '/scoped-underscore/-/scoped-underscore-1.3.1.tgz' +// needs to be a different hostname to verify tokens (not) being sent correctly +var tarballURL = 'http://lvh.me:' + common.port + tarballPath +var tarball = resolve(__dirname, '../fixtures/scoped-underscore-1.3.1.tgz') + +var server + +var EXEC_OPTS = { cwd: pkg } + +function mocks (server) { + var auth = 'Bearer 0xabad1dea' + server.get(tarballPath, { authorization: auth }).reply(403, { + error: 'token leakage', + reason: 'This token should not be sent.' + }) + server.get(tarballPath).replyWithFile(200, tarball) +} + +test('setup', function (t) { + mr({ port: common.port, plugin: mocks }, function (er, s) { + server = s + t.ok(s, 'set up mock registry') + setup() + t.end() + }) +}) + +test('authed npm install with tarball not on registry', function (t) { + common.npm( + [ + 'install', + '--loglevel', 'silent', + '--json', + '--fetch-retries', 0, + '--userconfig', outfile + ], + EXEC_OPTS, + function (err, code, stdout, stderr) { + t.ifError(err, 'test runner executed without error') + t.equal(code, 0, 'npm install exited OK') + t.notOk(stderr, 'no output on stderr') + try { + var results = JSON.parse(stdout) + } catch (ex) { + console.error('#', ex) + t.ifError(ex, 'stdout was valid JSON') + } + + if (results) { + var installedversion = { + 'version': '1.3.1', + 'from': '>=1.3.1 <2', + 'resolved': 'http://lvh.me:1337/scoped-underscore/-/scoped-underscore-1.3.1.tgz' + } + t.isDeeply(results.dependencies['@scoped/underscore'], installedversion, '@scoped/underscore installed') + } + + t.end() + } + ) +}) + +test('cleanup', function (t) { + server.close() + cleanup() + t.end() +}) + +var contents = '@scoped:registry=' + common.registry + '\n' + + toNerfDart(common.registry) + ':_authToken=0xabad1dea\n' + +var json = { + name: 'test-package-install', + version: '1.0.0' +} + +var shrinkwrap = { + name: 'test-package-install', + version: '1.0.0', + dependencies: { + '@scoped/underscore': { + resolved: tarballURL, + from: '>=1.3.1 <2', + version: '1.3.1' + } + } +} + +function setup () { + cleanup() + mkdirp.sync(modules) + writeFileSync(resolve(pkg, 'package.json'), JSON.stringify(json, null, 2) + '\n') + writeFileSync(outfile, contents) + writeFileSync( + resolve(pkg, 'npm-shrinkwrap.json'), + JSON.stringify(shrinkwrap, null, 2) + '\n' + ) +} + +function cleanup () { + process.chdir(osenv.tmpdir()) + rimraf.sync(pkg) +}
test/tap/config-credentials.js+1 −1 modified@@ -79,7 +79,7 @@ test('set with token', function (t) { password: undefined, email: undefined, auth: undefined, - alwaysAuth: undefined + alwaysAuth: false } t.same(conf.getCredentialsByURI(URI), expected, 'got bearer token and scope')
test/tap/map-to-registry.js+78 −3 modified@@ -48,7 +48,7 @@ test('mapRegistryToURI', function (t) { password: undefined, email: undefined, auth: undefined, - alwaysAuth: undefined + alwaysAuth: false }) t.equal(registry, 'http://reg.npm/design/-/rewrite/') }) @@ -66,7 +66,7 @@ test('mapRegistryToURI', function (t) { password: undefined, email: undefined, auth: undefined, - alwaysAuth: undefined + alwaysAuth: false }) t.equal(registry, 'http://reg.npm/-/rewrite/') }) @@ -84,8 +84,83 @@ test('mapRegistryToURI', function (t) { password: undefined, email: undefined, auth: undefined, - alwaysAuth: undefined + alwaysAuth: false }) t.equal(registry, 'http://reg.npm/design/-/rewrite/relative/') }) }) + +test('mapToRegistry token scoping', function (t) { + npm.config.set('scope', '') + npm.config.set('registry', 'https://reg.npm/') + npm.config.set('//reg.npm/:_authToken', 'r-token') + + t.test('pass token to registry host', function (t) { + mapRegistry( + 'https://reg.npm/packages/e/easy-1.0.0.tgz', + npm.config, + function (er, uri, auth, registry) { + t.ifError(er, 'mapRegistryToURI worked') + t.equal(uri, 'https://reg.npm/packages/e/easy-1.0.0.tgz') + t.deepEqual(auth, { + scope: '//reg.npm/', + token: 'r-token', + username: undefined, + password: undefined, + email: undefined, + auth: undefined, + alwaysAuth: false + }) + t.equal(registry, 'https://reg.npm/') + } + ) + t.end() + }) + + t.test("don't pass token to non-registry host", function (t) { + mapRegistry( + 'https://butts.lol/packages/e/easy-1.0.0.tgz', + npm.config, + function (er, uri, auth, registry) { + t.ifError(er, 'mapRegistryToURI worked') + t.equal(uri, 'https://butts.lol/packages/e/easy-1.0.0.tgz') + t.deepEqual(auth, { + scope: '//reg.npm/', + token: undefined, + username: undefined, + password: undefined, + email: undefined, + auth: undefined, + alwaysAuth: false + }) + t.equal(registry, 'https://reg.npm/') + } + ) + t.end() + }) + + t.test('pass token to non-registry host with always-auth', function (t) { + npm.config.set('always-auth', true) + mapRegistry( + 'https://butts.lol/packages/e/easy-1.0.0.tgz', + npm.config, + function (er, uri, auth, registry) { + t.ifError(er, 'mapRegistryToURI worked') + t.equal(uri, 'https://butts.lol/packages/e/easy-1.0.0.tgz') + t.deepEqual(auth, { + scope: '//reg.npm/', + token: 'r-token', + username: undefined, + password: undefined, + email: undefined, + auth: undefined, + alwaysAuth: true + }) + t.equal(registry, 'https://reg.npm/') + } + ) + t.end() + }) + + t.end() +})
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
10- github.com/npm/npm/commit/f67ecad59e99a03e5aad8e93cd1a086ae087cb29nvdPatchThird Party AdvisoryWEB
- github.com/npm/npm/commit/fea8cc92cee02c720b58f95f14d315507ccad401nvdPatchThird Party AdvisoryWEB
- blog.npmjs.org/post/142036323955/fixing-a-bearer-token-vulnerabilitynvdVendor AdvisoryWEB
- www-01.ibm.com/support/docview.wssnvdVendor AdvisoryWEB
- github.com/advisories/GHSA-m5h6-hr3q-22h5ghsaADVISORY
- github.com/npm/npm/issues/8380nvdThird Party AdvisoryWEB
- nodejs.org/en/blog/vulnerability/npm-tokens-leak-march-2016/nvdVendor Advisory
- nvd.nist.gov/vuln/detail/CVE-2016-3956ghsaADVISORY
- nodejs.org/en/blog/vulnerability/npm-tokens-leak-march-2016ghsaWEB
- www.npmjs.com/advisories/98ghsaWEB
News mentions
0No linked articles in our index yet.