High severity7.5NVD Advisory· Published Jan 23, 2017· Updated May 13, 2026
CVE-2015-8855
CVE-2015-8855
Description
The semver package before 4.3.2 for Node.js allows attackers to cause a denial of service (CPU consumption) via a long version string, aka a "regular expression denial of service (ReDoS)."
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
semvernpm | >= 1.0.4, < 4.3.2 | 4.3.2 |
Patches
2c80180d8341aPrevent version strings > 256 chars, or with giant numbers
2 files changed · +50 −1
semver.js+26 −1 modified@@ -20,6 +20,9 @@ if (typeof module === 'object' && module.exports === exports) // Not necessarily the package version of this code. exports.SEMVER_SPEC_VERSION = '2.0.0'; +var MAX_LENGTH = 256; +var MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER || 9007199254740991; + // The actual regexps go on exports.re var re = exports.re = []; var src = exports.src = []; @@ -233,8 +236,18 @@ for (var i = 0; i < R; i++) { exports.parse = parse; function parse(version, loose) { + if (version.length > MAX_LENGTH) + return null; + var r = loose ? re[LOOSE] : re[FULL]; - return (r.test(version)) ? new SemVer(version, loose) : null; + if (!r.test(version)) + return null; + + try { + return new SemVer(version, loose); + } catch (er) { + return null; + } } exports.valid = valid; @@ -262,6 +275,9 @@ function SemVer(version, loose) { throw new TypeError('Invalid Version: ' + version); } + if (version.length > MAX_LENGTH) + throw new TypeError('version is longer than ' + MAX_LENGTH + ' characters') + if (!(this instanceof SemVer)) return new SemVer(version, loose); @@ -279,6 +295,15 @@ function SemVer(version, loose) { this.minor = +m[2]; this.patch = +m[3]; + if (this.major > MAX_SAFE_INTEGER || this.major < 0) + throw new TypeError('Invalid major version') + + if (this.minor > MAX_SAFE_INTEGER || this.minor < 0) + throw new TypeError('Invalid minor version') + + if (this.patch > MAX_SAFE_INTEGER || this.patch < 0) + throw new TypeError('Invalid patch version') + // numberify any prerelease numeric ids if (!m[4]) this.prerelease = [];
test/big-numbers.js+24 −0 added@@ -0,0 +1,24 @@ +var test = require('tap').test +var semver = require('../') + +test('long version is too long', function (t) { + var v = '1.2.' + new Array(256).join('1') + t.throws(function () { + new semver.SemVer(v) + }) + t.equal(semver.valid(v, false), null) + t.equal(semver.valid(v, true), null) + t.equal(semver.inc(v, 'patch'), null) + t.end() +}) + +test('big number is like too long version', function (t) { + var v = '1.2.' + new Array(100).join('1') + t.throws(function () { + new semver.SemVer(v) + }) + t.equal(semver.valid(v, false), null) + t.equal(semver.valid(v, true), null) + t.equal(semver.inc(v, 'patch'), null) + t.end() +})
2 files changed · +128 −7
README.md+32 −0 modified@@ -84,3 +84,35 @@ The following range styles are supported: Ranges can be joined with either a space (which implies "and") or a `||` (which implies "or"). + +## Functions + +* valid(v): Return the parsed version, or null if it's not valid. + +### Comparison + +* gt(v1, v2): `v1 > v2` +* gte(v1, v2): `v1 >= v2` +* lt(v1, v2): `v1 < v2` +* lte(v1, v2): `v1 <= v2` +* eq(v1, v2): `v1 == v2` This is true if they're logically equivalent, + even if they're not the exact same string. You already know how to + compare strings. +* neq(v1, v2): `v1 != v2` The opposite of eq. +* cmp(v1, comparator, v2): Pass in a comparison string, and it'll call + the corresponding function above. `"==="` and `"!=="` do simple + string comparison, but are included for completeness. Throws if an + invalid comparison string is provided. +* compare(v1, v2): Return 0 if v1 == v2, or 1 if v1 is greater, or -1 if + v2 is greater. Sorts in ascending order if passed to Array.sort(). +* rcompare(v1, v2): The reverse of compare. Sorts an array of versions + in descending order when passed to Array.sort(). + + +### Ranges + +* validRange(range): Return the valid range or null if it's not valid +* satisfies(version, range): Return true if the version satisfies the + range. +* maxSatisfying(versions, range): Return the highest version in the list + that satisfies the range, or null if none of them do.
semver.js+96 −7 modified@@ -3,7 +3,7 @@ // This implementation is a *hair* less strict in that it allows // v1.2.3 things, and also tags that don't begin with a char. -var semver = "[v=]*([0-9]+)" // major +var semver = "\\s*[v=]*\\s*([0-9]+)" // major + "\\.([0-9]+)" // minor + "\\.([0-9]+)" // patch + "(-[0-9]+-?)?" // build @@ -30,7 +30,13 @@ exports.clean = clean exports.compare = compare exports.satisfies = satisfies exports.gt = gt +exports.gte = gte exports.lt = lt +exports.lte = lte +exports.eq = eq +exports.neq = neq +exports.cmp = cmp + exports.valid = valid exports.validPackage = validPackage exports.validRange = validRange @@ -41,9 +47,11 @@ function clean (ver) { if (!v) return v return [v[1]||'', v[2]||'', v[3]||''].join(".") + (v[4]||'') + (v[5]||'') } + function valid (version) { return exports.parse(version) && version.trim().replace(/^[v=]+/, '') } + function validPackage (version) { return version.match(expressions.parsePackage) && version.trim() } @@ -97,6 +105,7 @@ function replaceXRanges (ranges) { .map(replaceXRange) .join(" ") } + function replaceXRange (version) { return version.trim().replace(expressions.parseXRange, function (v, gtlt, M, m, p) { @@ -191,10 +200,32 @@ function satisfies (version, range) { // return v1 > v2 ? 1 : -1 function compare (v1, v2) { - return v1 === v2 ? 0 : gt(v1, v2) ? 1 : -1 + var g = gt(v1, v2) + return g === null ? 0 : g ? 1 : -1 +} + +function rcompare (v1, v2) { + return compare(v2, v1) } function lt (v1, v2) { return gt(v2, v1) } +function gte (v1, v2) { return !lt(v1, v2) } +function lte (v1, v2) { return !gt(v1, v2) } +function eq (v1, v2) { return gt(v1, v2) === null } +function neq (v1, v2) { return gt(v1, v2) !== null } +function cmp (v1, c, v2) { + switch (c) { + case ">": return gt(v1, v2) + case "<": return lt(v1, v2) + case ">=": return gte(v1, v2) + case "<=": return lte(v1, v2) + case "==": return eq(v1, v2) + case "!=": return neq(v1, v2) + case "===": return v1 === v2 + case "!==": return v1 !== v2 + default: throw new Error("Y U NO USE VALID COMPARATOR!? "+c) + } +} // return v1 > v2 function num (v) { @@ -214,7 +245,13 @@ function gt (v1, v2) { // no tag is > than any tag, or use lexicographical order. var tag1 = v1[5] || "" , tag2 = v2[5] || "" - return !!tag2 && (!tag1 || tag1 > tag2) + + // kludge: null means they were equal. falsey, and detectable. + // embarrassingly overclever, though, I know. + return tag1 === tag2 ? null + : !tag1 ? true + : !tag2 ? false + : tag1 > tag2 } if (module === require.main) { // tests below @@ -243,11 +280,63 @@ var assert = require("assert") , ["1.2.3-4-foo", "1.2.3"] , ["1.2.3-5", "1.2.3-5-foo"] , ["1.2.3-5", "1.2.3-4"] + , ["1.2.3-5-foo", "1.2.3-5-Foo"] + ].forEach(function (v) { + var v0 = v[0] + , v1 = v[1] + assert.ok(gt(v0, v1), "gt('"+v0+"', '"+v1+"')") + assert.ok(lt(v1, v0), "lt('"+v1+"', '"+v0+"')") + assert.ok(!gt(v1, v0), "!gt('"+v1+"', '"+v0+"')") + assert.ok(!lt(v0, v1), "!lt('"+v0+"', '"+v1+"')") + assert.ok(eq(v0, v0), "eq('"+v0+"', '"+v0+"')") + assert.ok(eq(v1, v1), "eq('"+v1+"', '"+v1+"')") + assert.ok(neq(v0, v1), "neq('"+v0+"', '"+v1+"')") + assert.ok(cmp(v1, "==", v1), "cmp("+v1+"=="+v1+")") + assert.ok(cmp(v0, ">=", v1), "cmp("+v0+"<="+v1+")") + assert.ok(cmp(v1, "<=", v0), "cmp("+v1+">="+v0+")") + assert.ok(cmp(v0, "!=", v1), "cmp("+v0+"!="+v1+")") + }) + +// equality tests +; [ ["1.2.3", "v1.2.3"] + , ["1.2.3", "=1.2.3"] + , ["1.2.3", "v 1.2.3"] + , ["1.2.3", "= 1.2.3"] + , ["1.2.3", " v1.2.3"] + , ["1.2.3", " =1.2.3"] + , ["1.2.3", " v 1.2.3"] + , ["1.2.3", " = 1.2.3"] + , ["1.2.3-0", "v1.2.3-0"] + , ["1.2.3-0", "=1.2.3-0"] + , ["1.2.3-0", "v 1.2.3-0"] + , ["1.2.3-0", "= 1.2.3-0"] + , ["1.2.3-0", " v1.2.3-0"] + , ["1.2.3-0", " =1.2.3-0"] + , ["1.2.3-0", " v 1.2.3-0"] + , ["1.2.3-0", " = 1.2.3-0"] + , ["1.2.3-01", "v1.2.3-1"] + , ["1.2.3-01", "=1.2.3-1"] + , ["1.2.3-01", "v 1.2.3-1"] + , ["1.2.3-01", "= 1.2.3-1"] + , ["1.2.3-01", " v1.2.3-1"] + , ["1.2.3-01", " =1.2.3-1"] + , ["1.2.3-01", " v 1.2.3-1"] + , ["1.2.3-01", " = 1.2.3-1"] + , ["1.2.3beta", "v1.2.3beta"] + , ["1.2.3beta", "=1.2.3beta"] + , ["1.2.3beta", "v 1.2.3beta"] + , ["1.2.3beta", "= 1.2.3beta"] + , ["1.2.3beta", " v1.2.3beta"] + , ["1.2.3beta", " =1.2.3beta"] + , ["1.2.3beta", " v 1.2.3beta"] + , ["1.2.3beta", " = 1.2.3beta"] ].forEach(function (v) { - assert.ok(gt(v[0], v[1]), "gt('"+v[0]+"', '"+v[1]+"')") - assert.ok(lt(v[1], v[0]), "lt('"+v[1]+"', '"+v[0]+"')") - assert.ok(!gt(v[1], v[0]), "!gt('"+v[1]+"', '"+v[0]+"')") - assert.ok(!lt(v[0], v[1]), "!lt('"+v[0]+"', '"+v[1]+"')") + var v0 = v[0] + , v1 = v[1] + assert.ok(eq(v0, v1), "eq('"+v0+"', '"+v1+"')") + assert.ok(eq(v0, v1), "eq('"+v0+"', '"+v1+"')") + assert.ok(eq(v0, v1), "eq('"+v0+"', '"+v1+"')") + assert.ok(eq(v0, v1), "eq('"+v0+"', '"+v1+"')") })
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- nodesecurity.io/advisories/31nvdPatchVendor Advisory
- www.openwall.com/lists/oss-security/2016/04/20/11nvdMailing ListThird Party AdvisoryWEB
- www.securityfocus.com/bid/86957nvdThird Party AdvisoryVDB EntryWEB
- github.com/advisories/GHSA-x6fg-f45m-jf5qghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2015-8855ghsaADVISORY
- github.com/github/advisory-database/pull/7102ghsaWEB
- github.com/npm/node-semver/commit/5c4c9f6e26c7052a42b5ced2a7481c5c9b4363a0ghsaWEB
- github.com/npm/node-semver/commit/c80180d8341a8ada0236815c29a2be59864afd70ghsaWEB
- www.npmjs.com/advisories/31ghsaWEB
- www.owasp.org/index.php/Regular_expression_Denial_of_Service_-_ReDoSghsaWEB
News mentions
0No linked articles in our index yet.