Skip to content

Commit 30e9f98

Browse files
authored
fix: prevent cache dedup key collision via unescaped delimiters (#5012) (#5013)
1 parent 85d8368 commit 30e9f98

2 files changed

Lines changed: 67 additions & 7 deletions

File tree

lib/util/cache.js

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -374,9 +374,11 @@ function assertCacheMethods (methods, name = 'CacheMethods') {
374374
* @returns {string}
375375
*/
376376
function makeDeduplicationKey (cacheKey, excludeHeaders) {
377-
// Create a deterministic string key from the cache key
378-
// Include origin, method, path, and sorted headers
379-
let key = `${cacheKey.origin}:${cacheKey.method}:${cacheKey.path}`
377+
// Use JSON.stringify to produce a collision-resistant key.
378+
// Previous format used `:` and `=` delimiters without escaping, which
379+
// allowed different header sets to produce identical keys (e.g.
380+
// {a:"x:b=y"} vs {a:"x", b:"y"}). See: https://github.com/nodejs/undici/issues/5012
381+
const headers = {}
380382

381383
if (cacheKey.headers) {
382384
const sortedHeaders = Object.keys(cacheKey.headers).sort()
@@ -385,12 +387,11 @@ function makeDeduplicationKey (cacheKey, excludeHeaders) {
385387
if (excludeHeaders?.has(header.toLowerCase())) {
386388
continue
387389
}
388-
const value = cacheKey.headers[header]
389-
key += `:${header}=${Array.isArray(value) ? value.join(',') : value}`
390+
headers[header] = cacheKey.headers[header]
390391
}
391392
}
392393

393-
return key
394+
return JSON.stringify([cacheKey.origin, cacheKey.method, cacheKey.path, headers])
394395
}
395396

396397
module.exports = {

test/interceptors/deduplicate.js

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33
const { createServer } = require('node:http')
44
const { describe, test, after } = require('node:test')
55
const { once } = require('node:events')
6-
const { strictEqual } = require('node:assert')
6+
const { strictEqual, notStrictEqual } = require('node:assert')
77
const { setTimeout: sleep } = require('node:timers/promises')
88
const diagnosticsChannel = require('node:diagnostics_channel')
99
const { Client, interceptors } = require('../../index')
10+
const { makeDeduplicationKey } = require('../../lib/util/cache')
1011

1112
describe('Deduplicate Interceptor', () => {
1213
test('deduplicates concurrent requests for the same resource', async () => {
@@ -1291,4 +1292,62 @@ describe('Deduplicate Interceptor', () => {
12911292
message: 'expected opts.excludeHeaderNames to be an array, got string'
12921293
})
12931294
})
1295+
1296+
test('makeDeduplicationKey does not collide when header values contain delimiters', () => {
1297+
// Regression test for https://github.com/nodejs/undici/issues/5012
1298+
// Previously, headers {a:"x:b=y"} and {a:"x", b:"y"} produced the same key
1299+
const key1 = makeDeduplicationKey({
1300+
origin: 'https://example.com',
1301+
method: 'GET',
1302+
path: '/',
1303+
headers: { a: 'x:b=y' }
1304+
})
1305+
1306+
const key2 = makeDeduplicationKey({
1307+
origin: 'https://example.com',
1308+
method: 'GET',
1309+
path: '/',
1310+
headers: { a: 'x', b: 'y' }
1311+
})
1312+
1313+
notStrictEqual(key1, key2)
1314+
})
1315+
1316+
test('makeDeduplicationKey produces same key for identical headers', () => {
1317+
const key1 = makeDeduplicationKey({
1318+
origin: 'https://example.com',
1319+
method: 'GET',
1320+
path: '/',
1321+
headers: { b: '2', a: '1' }
1322+
})
1323+
1324+
const key2 = makeDeduplicationKey({
1325+
origin: 'https://example.com',
1326+
method: 'GET',
1327+
path: '/',
1328+
headers: { a: '1', b: '2' }
1329+
})
1330+
1331+
strictEqual(key1, key2)
1332+
})
1333+
1334+
test('makeDeduplicationKey respects excludeHeaders', () => {
1335+
const excludeHeaders = new Set(['x-request-id'])
1336+
1337+
const key1 = makeDeduplicationKey({
1338+
origin: 'https://example.com',
1339+
method: 'GET',
1340+
path: '/',
1341+
headers: { accept: 'text/html', 'x-request-id': '111' }
1342+
}, excludeHeaders)
1343+
1344+
const key2 = makeDeduplicationKey({
1345+
origin: 'https://example.com',
1346+
method: 'GET',
1347+
path: '/',
1348+
headers: { accept: 'text/html', 'x-request-id': '222' }
1349+
}, excludeHeaders)
1350+
1351+
strictEqual(key1, key2)
1352+
})
12941353
})

0 commit comments

Comments
 (0)