CVE-2024-45813
Description
find-my-way is a fast, open source HTTP router, internally using a Radix Tree (aka compact Prefix Tree), supports route params, wildcards, and it's framework independent. A bad regular expression is generated any time one has two parameters within a single segment, when adding a - at the end, like /:a-:b-. This may cause a denial of service in some instances. Users are advised to update to find-my-way v8.2.2 or v9.0.1. or subsequent versions. There are no known workarounds for this issue.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
find-my-waynpm | >= 5.5.0, < 8.2.2 | 8.2.2 |
find-my-waynpm | >= 9.0.0, < 9.0.1 | 9.0.1 |
Patches
49e666a1170c75e9e0eb5d8d4Merge commit from fork
4 files changed · +31 −8
index.js+13 −4 modified@@ -192,6 +192,8 @@ Router.prototype._on = function _on (method, path, opts, handler, store) { if (isParametricNode) { let isRegexNode = false + let isParamSafe = true + let backtrack = '' const regexps = [] let lastParamStartIndex = i + 1 @@ -219,8 +221,10 @@ Router.prototype._on = function _on (method, path, opts, handler, store) { regexps.push(trimRegExpStartAndEnd(regexString)) j = endOfRegexIndex + 1 + isParamSafe = true } else { - regexps.push('(.*?)') + regexps.push(isParamSafe ? '(.*?)' : `(${backtrack}|(?:(?!${backtrack}).)*)`) + isParamSafe = false } const staticPartStartIndex = j @@ -238,7 +242,7 @@ Router.prototype._on = function _on (method, path, opts, handler, store) { if (staticPart) { staticPart = staticPart.split('::').join(':') staticPart = staticPart.split('%').join('%25') - regexps.push(escapeRegExp(staticPart)) + regexps.push(backtrack = escapeRegExp(staticPart)) } lastParamStartIndex = j + 1 @@ -335,6 +339,8 @@ Router.prototype.findRoute = function findNode (method, path, constraints = {}) if (isParametricNode) { let isRegexNode = false + let isParamSafe = true + let backtrack = '' const regexps = [] let lastParamStartIndex = i + 1 @@ -344,6 +350,7 @@ Router.prototype.findRoute = function findNode (method, path, constraints = {}) const isRegexParam = charCode === 40 const isStaticPart = charCode === 45 || charCode === 46 const isEndOfNode = charCode === 47 || j === pattern.length + if (isRegexParam || isStaticPart || isEndOfNode) { const paramName = pattern.slice(lastParamStartIndex, j) params.push(paramName) @@ -361,8 +368,10 @@ Router.prototype.findRoute = function findNode (method, path, constraints = {}) regexps.push(trimRegExpStartAndEnd(regexString)) j = endOfRegexIndex + 1 + isParamSafe = false } else { - regexps.push('(.*?)') + regexps.push(isParamSafe ? '(.*?)' : `(${backtrack}|(?:(?!${backtrack}).)*)`) + isParamSafe = false } const staticPartStartIndex = j @@ -380,7 +389,7 @@ Router.prototype.findRoute = function findNode (method, path, constraints = {}) if (staticPart) { staticPart = staticPart.split('::').join(':') staticPart = staticPart.split('%').join('%25') - regexps.push(escapeRegExp(staticPart)) + regexps.push(backtrack = escapeRegExp(staticPart)) } lastParamStartIndex = j + 1
test/issue-17.test.js+2 −2 modified@@ -132,8 +132,8 @@ test('Multi parametric route / 2', t => { }) findMyWay.on('GET', '/a/:p1-:p2', (req, res, params) => { - t.equal(params.p1, 'foo') - t.equal(params.p2, 'bar-baz') + t.equal(params.p1, 'foo-bar') + t.equal(params.p2, 'baz') }) findMyWay.on('GET', '/b/:p1.:p2', (req, res, params) => {
test/optional-params.test.js+2 −2 modified@@ -68,8 +68,8 @@ test('Multi parametric route with optional param', (t) => { findMyWay.on('GET', '/a/:p1-:p2?', (req, res, params) => { if (params.p1 && params.p2) { - t.equal(params.p1, 'foo') - t.equal(params.p2, 'bar-baz') + t.equal(params.p1, 'foo-bar') + t.equal(params.p2, 'baz') } })
test/regex.test.js+14 −0 modified@@ -255,3 +255,17 @@ test('Disable safe regex check', t => { } }) }) + +test('prevent back-tracking', (t) => { + t.plan(0) + t.setTimeout(20) + + const findMyWay = FindMyWay({ + defaultRoute: () => { + t.fail('route not matched') + } + }) + + findMyWay.on('GET', '/:foo-:bar-', (req, res, params) => {}) + findMyWay.find('GET', '/' + '-'.repeat(16_000) + 'a', { host: 'fastify.io' }) +})
17fae694dcefMerge commit from fork
4 files changed · +31 −8
index.js+13 −4 modified@@ -192,6 +192,8 @@ Router.prototype._on = function _on (method, path, opts, handler, store) { if (isParametricNode) { let isRegexNode = false + let isParamSafe = true + let backtrack = '' const regexps = [] let lastParamStartIndex = i + 1 @@ -219,8 +221,10 @@ Router.prototype._on = function _on (method, path, opts, handler, store) { regexps.push(trimRegExpStartAndEnd(regexString)) j = endOfRegexIndex + 1 + isParamSafe = true } else { - regexps.push('(.*?)') + regexps.push(isParamSafe ? '(.*?)' : `(${backtrack}|(?:(?!${backtrack}).)*)`) + isParamSafe = false } const staticPartStartIndex = j @@ -238,7 +242,7 @@ Router.prototype._on = function _on (method, path, opts, handler, store) { if (staticPart) { staticPart = staticPart.split('::').join(':') staticPart = staticPart.split('%').join('%25') - regexps.push(escapeRegExp(staticPart)) + regexps.push(backtrack = escapeRegExp(staticPart)) } lastParamStartIndex = j + 1 @@ -335,6 +339,8 @@ Router.prototype.findRoute = function findNode (method, path, constraints = {}) if (isParametricNode) { let isRegexNode = false + let isParamSafe = true + let backtrack = '' const regexps = [] let lastParamStartIndex = i + 1 @@ -344,6 +350,7 @@ Router.prototype.findRoute = function findNode (method, path, constraints = {}) const isRegexParam = charCode === 40 const isStaticPart = charCode === 45 || charCode === 46 const isEndOfNode = charCode === 47 || j === pattern.length + if (isRegexParam || isStaticPart || isEndOfNode) { const paramName = pattern.slice(lastParamStartIndex, j) params.push(paramName) @@ -361,8 +368,10 @@ Router.prototype.findRoute = function findNode (method, path, constraints = {}) regexps.push(trimRegExpStartAndEnd(regexString)) j = endOfRegexIndex + 1 + isParamSafe = false } else { - regexps.push('(.*?)') + regexps.push(isParamSafe ? '(.*?)' : `(${backtrack}|(?:(?!${backtrack}).)*)`) + isParamSafe = false } const staticPartStartIndex = j @@ -380,7 +389,7 @@ Router.prototype.findRoute = function findNode (method, path, constraints = {}) if (staticPart) { staticPart = staticPart.split('::').join(':') staticPart = staticPart.split('%').join('%25') - regexps.push(escapeRegExp(staticPart)) + regexps.push(backtrack = escapeRegExp(staticPart)) } lastParamStartIndex = j + 1
test/issue-17.test.js+2 −2 modified@@ -132,8 +132,8 @@ test('Multi parametric route / 2', t => { }) findMyWay.on('GET', '/a/:p1-:p2', (req, res, params) => { - t.equal(params.p1, 'foo') - t.equal(params.p2, 'bar-baz') + t.equal(params.p1, 'foo-bar') + t.equal(params.p2, 'baz') }) findMyWay.on('GET', '/b/:p1.:p2', (req, res, params) => {
test/optional-params.test.js+2 −2 modified@@ -68,8 +68,8 @@ test('Multi parametric route with optional param', (t) => { findMyWay.on('GET', '/a/:p1-:p2?', (req, res, params) => { if (params.p1 && params.p2) { - t.equal(params.p1, 'foo') - t.equal(params.p2, 'bar-baz') + t.equal(params.p1, 'foo-bar') + t.equal(params.p2, 'baz') } })
test/regex.test.js+14 −0 modified@@ -255,3 +255,17 @@ test('Disable safe regex check', t => { } }) }) + +test('prevent back-tracking', (t) => { + t.plan(0) + t.setTimeout(20) + + const findMyWay = FindMyWay({ + defaultRoute: () => { + t.fail('route not matched') + } + }) + + findMyWay.on('GET', '/:foo-:bar-', (req, res, params) => {}) + findMyWay.find('GET', '/' + '-'.repeat(16_000) + 'a', { host: 'fastify.io' }) +})
66fa03923355Define insert method for each node type (#248)
3 files changed · +100 −109
custom_node.js+71 −49 modified@@ -34,37 +34,6 @@ Object.defineProperty(Node.prototype, 'types', { value: types }) -Node.prototype.addChild = function (node) { - const label = node.prefix[0] - switch (node.kind) { - case this.types.STATIC: - assert( - this.staticChildren[label] === undefined, - `There is already a child with label '${label}'` - ) - this.staticChildren[label] = node - break - case this.types.PARAM: - case this.types.REGEX: - assert(this.parametricChild === null, 'There is already a parametric child') - this.parametricChild = node - break - case this.types.MATCH_ALL: - assert(this.wildcardChild === null, 'There is already a wildcard child') - this.wildcardChild = node - break - default: - throw new Error(`Unknown node kind: ${node.kind}`) - } - - this.numberOfChildren++ - - this._saveParametricBrother() - this._saveWildcardBrother() - - return this -} - Node.prototype._saveParametricBrother = function () { let parametricBrother = this.parametricBrother if (this.parametricChild !== null) { @@ -76,7 +45,7 @@ Node.prototype._saveParametricBrother = function () { if (parametricBrother) { for (const child of Object.values(this.staticChildren)) { child.parametricBrother = parametricBrother - child._saveParametricBrother(parametricBrother) + child._saveParametricBrother() } } } @@ -92,7 +61,7 @@ Node.prototype._saveWildcardBrother = function () { if (wildcardBrother) { for (const child of Object.values(this.staticChildren)) { child.wildcardBrother = wildcardBrother - child._saveWildcardBrother(wildcardBrother) + child._saveWildcardBrother() } if (this.parametricChild !== null) { this.parametricChild.wildcardBrother = wildcardBrother @@ -116,7 +85,7 @@ Node.prototype.reset = function (prefix) { } Node.prototype.split = function (length) { - const newChild = new Node( + const staticChild = new Node( { prefix: this.prefix.slice(length), staticChildren: this.staticChildren, @@ -131,32 +100,85 @@ Node.prototype.split = function (length) { ) if (this.wildcardChild !== null) { - newChild.wildcardChild = this.wildcardChild + staticChild.wildcardChild = this.wildcardChild } if (this.parametricChild !== null) { - newChild.parametricChild = this.parametricChild + staticChild.parametricChild = this.parametricChild } this.reset(this.prefix.slice(0, length)) - this.addChild(newChild) - return newChild + + const label = staticChild.prefix.charAt(0) + this.staticChildren[label] = staticChild + + this.numberOfChildren++ + + this._saveParametricBrother() + this._saveWildcardBrother() + + return staticChild } -Node.prototype.getChildByLabel = function (label, kind) { - if (label.length === 0) { - return null +Node.prototype.insertStaticNode = function (path) { + if (path.length === 0) { + return this } - switch (kind) { - case this.types.STATIC: - return this.staticChildren[label] - case this.types.MATCH_ALL: - return this.wildcardChild - case this.types.PARAM: - case this.types.REGEX: - return this.parametricChild + let staticChild = this.staticChildren[path.charAt(0)] + if (staticChild) { + let i = 0 + for (; i < staticChild.prefix.length; i++) { + if (path.charCodeAt(i) !== staticChild.prefix.charCodeAt(i)) { + staticChild.split(i) + break + } + } + return staticChild.insertStaticNode(path.slice(i)) } + + staticChild = new Node({ method: this.method, prefix: path, kind: types.STATIC, constrainer: this.constrainer }) + + const label = path.charAt(0) + this.staticChildren[label] = staticChild + this.numberOfChildren++ + + this._saveParametricBrother() + this._saveWildcardBrother() + + return staticChild +} + +Node.prototype.insertParametricNode = function (regex) { + if (this.parametricChild) { + return this.parametricChild + } + + const kind = regex ? types.REGEX : types.PARAM + const parametricChild = new Node({ method: this.method, prefix: ':', kind, regex, constrainer: this.constrainer }) + + this.parametricChild = parametricChild + this.numberOfChildren++ + + this._saveParametricBrother() + this._saveWildcardBrother() + + return parametricChild +} + +Node.prototype.insertWildcardNode = function () { + if (this.wildcardChild) { + return this.wildcardChild + } + + const wildcardChild = new Node({ method: this.method, prefix: '*', kind: types.MATCH_ALL, constrainer: this.constrainer }) + + this.wildcardChild = wildcardChild + this.numberOfChildren++ + + this._saveWildcardBrother() + + return wildcardChild } Node.prototype.findStaticMatchingChild = function (path, pathIndex) {
index.js+26 −54 modified@@ -166,29 +166,34 @@ Router.prototype._on = function _on (method, path, opts, handler, store) { const params = [] for (let i = 0; i <= path.length; i++) { - // search for parametric or wildcard routes - // parametric route - if (path.charCodeAt(i) === 58) { - if (path.charCodeAt(i + 1) === 58) { - // It's a double colon. Let's just replace it with a single colon and go ahead - path = path.slice(0, i) + path.slice(i + 1) - continue - } + if (path.charCodeAt(i) === 58 && path.charCodeAt(i + 1) === 58) { + // It's a double colon. Let's just replace it with a single colon and go ahead + path = path.slice(0, i) + path.slice(i + 1) + continue + } - // add the static part of the route to the tree - currentNode = this._insert(currentNode, method, path.slice(parentNodePathIndex, i), NODE_TYPES.STATIC, null) + const isParametricNode = path.charCodeAt(i) === 58 + const isWildcardNode = path.charCodeAt(i) === 42 - const paramStartIndex = i + 1 + if (isParametricNode || isWildcardNode || (i === path.length && i !== parentNodePathIndex)) { + let staticNodePath = path.slice(parentNodePathIndex, i) + if (!this.caseSensitive) { + staticNodePath = staticNodePath.toLowerCase() + } + // add the static part of the route to the tree + currentNode = currentNode.insertStaticNode(staticNodePath) + } + if (isParametricNode) { + let isRegexNode = false const regexps = [] - let nodeType = NODE_TYPES.PARAM - let lastParamStartIndex = paramStartIndex - for (let j = paramStartIndex; ; j++) { + let lastParamStartIndex = i + 1 + for (let j = lastParamStartIndex; ; j++) { const charCode = path.charCodeAt(j) if (charCode === 40 || charCode === 45 || charCode === 46) { - nodeType = NODE_TYPES.REGEX + isRegexNode = true const paramName = path.slice(lastParamStartIndex, j) params.push(paramName) @@ -233,63 +238,30 @@ Router.prototype._on = function _on (method, path, opts, handler, store) { } if (path.charCodeAt(j) === 47 || j === path.length) { - path = path.slice(0, paramStartIndex) + path.slice(j) + path = path.slice(0, i + 1) + path.slice(j) break } } let regex = null - if (nodeType === NODE_TYPES.REGEX) { + if (isRegexNode) { regex = new RegExp('^' + regexps.join('') + '$') } - currentNode = this._insert(currentNode, method, ':', nodeType, regex) + currentNode = currentNode.insertParametricNode(regex) parentNodePathIndex = i + 1 - // wildcard route - } else if (path.charCodeAt(i) === 42) { - currentNode = this._insert(currentNode, method, path.slice(parentNodePathIndex, i), NODE_TYPES.STATIC, null) + } else if (isWildcardNode) { // add the wildcard parameter params.push('*') - currentNode = this._insert(currentNode, method, path.slice(i), NODE_TYPES.MATCH_ALL, null) - break - } else if (i === path.length && i !== parentNodePathIndex) { - currentNode = this._insert(currentNode, method, path.slice(parentNodePathIndex), NODE_TYPES.STATIC, null) + currentNode = currentNode.insertWildcardNode() + parentNodePathIndex = i + 1 } } assert(!currentNode.getHandler(constraints), `Method '${method}' already declared for route '${path}' with constraints '${JSON.stringify(constraints)}'`) currentNode.addHandler(handler, params, store, constraints) } -Router.prototype._insert = function _insert (currentNode, method, path, kind, regex) { - if (!this.caseSensitive) { - path = path.toLowerCase() - } - - let childNode = currentNode.getChildByLabel(path.charAt(0), kind) - while (childNode) { - currentNode = childNode - - let i = 0 - for (; i < currentNode.prefix.length; i++) { - if (path.charCodeAt(i) !== currentNode.prefix.charCodeAt(i)) { - currentNode.split(i) - break - } - } - path = path.slice(i) - childNode = currentNode.getChildByLabel(path.charAt(0), kind) - } - - if (path.length > 0) { - const node = new Node({ method, prefix: path, kind, handlers: null, regex, constrainer: this.constrainer }) - currentNode.addChild(node) - currentNode = node - } - - return currentNode -} - Router.prototype.reset = function reset () { this.trees = {} this.routes = []
test/issue-104.test.js+3 −6 modified@@ -210,17 +210,14 @@ test('Mixed parametric routes, with last defined route being static', t => { test('parametricBrother of Parent Node, with a parametric child', t => { t.plan(1) const parent = new Node({ prefix: '/a' }) - const parametricChild = new Node({ prefix: ':id', kind: parent.types.PARAM }) - parent.addChild(parametricChild) + parent.insertParametricNode() t.equal(parent.parametricBrother, null) }) test('parametricBrother of Parent Node, with a parametric child and a static child', t => { t.plan(1) const parent = new Node({ prefix: '/a' }) - const parametricChild = new Node({ prefix: ':id', kind: parent.types.PARAM }) - const staticChild = new Node({ prefix: '/b', kind: parent.types.STATIC }) - parent.addChild(parametricChild) - parent.addChild(staticChild) + parent.insertParametricNode() + parent.insertStaticNode('/b') t.equal(parent.parametricBrother, null) })
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
8- github.com/advisories/GHSA-9wv6-86v2-598jghsaADVISORY
- github.com/advisories/GHSA-rrr8-f88r-h8q6ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-45813ghsaADVISORY
- blakeembrey.com/posts/2024-09-web-redosnvdWEB
- github.com/delvedor/find-my-way/commit/17fae694dcefc056045da201681c1530f0f80518ghsaWEB
- github.com/delvedor/find-my-way/commit/5e9e0eb5d8d438e06a185d5e536a896572dd0440nvdWEB
- github.com/delvedor/find-my-way/commit/66fa03923355b8da1db4ba572d66a4fee4a57cf5ghsaWEB
- github.com/delvedor/find-my-way/security/advisories/GHSA-rrr8-f88r-h8q6nvdWEB
News mentions
0No linked articles in our index yet.