Skip to content

Commit 5086956

Browse files
EdamAme-xusualoma
andauthored
fix(accept): replace regex split to mitigate ReDoS (#4758)
* fix(accept): replace regex split to mitigate ReDoS * test(accept): cover escaped quote and backslash parsing paths * fix(accept): handle quoted params and comma safely * refactor: improve accept header parsing --------- Co-authored-by: Taku Amano <taku@taaas.jp>
1 parent 18cc595 commit 5086956

2 files changed

Lines changed: 328 additions & 45 deletions

File tree

src/utils/accept.test.ts

Lines changed: 107 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ describe('parseAccept Comprehensive Tests', () => {
1717
// eslint-disable-next-line @typescript-eslint/no-explicit-any
1818
expect(parseAccept(null as any)).toEqual([])
1919
})
20+
21+
test('handles whitespace-only header', () => {
22+
expect(parseAccept(' ')).toEqual([])
23+
expect(parseAccept(' \t\n ')).toEqual([])
24+
})
2025
})
2126

2227
describe('Quality Values', () => {
@@ -46,11 +51,9 @@ describe('parseAccept Comprehensive Tests', () => {
4651
const result = parseAccept(header)
4752
expect(result[0].params).toEqual({
4853
a: '1',
49-
b: '"2"',
50-
54+
b: '2',
5155
c: "'3'",
52-
d: '"semi;colon"',
53-
e: '"nested"quoted""',
56+
d: 'semi;colon',
5457
})
5558
})
5659

@@ -105,6 +108,100 @@ describe('parseAccept Comprehensive Tests', () => {
105108
const result = parseAccept(header)
106109
expect(result.map((x) => x.type)).toEqual(['b', 'a'])
107110
})
111+
112+
test('handles comma inside quoted parameter value', () => {
113+
const header = 'text/plain;meta="a,b";q=0.8,application/json;q=0.7'
114+
const result = parseAccept(header)
115+
expect(result).toEqual([
116+
{
117+
type: 'text/plain',
118+
params: {
119+
meta: 'a,b',
120+
q: '0.8',
121+
},
122+
q: 0.8,
123+
},
124+
{
125+
type: 'application/json',
126+
params: {
127+
q: '0.7',
128+
},
129+
q: 0.7,
130+
},
131+
])
132+
})
133+
134+
test('handles escaped quote and semicolon inside quoted parameter', () => {
135+
const header = 'text/plain;meta="a\\\";b";q=0.5'
136+
const result = parseAccept(header)
137+
expect(result).toEqual([
138+
{
139+
type: 'text/plain',
140+
params: {
141+
meta: 'a";b',
142+
q: '0.5',
143+
},
144+
q: 0.5,
145+
},
146+
])
147+
})
148+
149+
test('handles escaped character inside quoted parameter', () => {
150+
const header = 'text/plain;meta="a\\\\z;c";q=0.3'
151+
const result = parseAccept(header)
152+
expect(result).toEqual([
153+
{
154+
type: 'text/plain',
155+
params: {
156+
meta: 'a\\z;c',
157+
q: '0.3',
158+
},
159+
q: 0.3,
160+
},
161+
])
162+
})
163+
164+
test('skips invalid param without swallowing next media type', () => {
165+
const header = 'a;foo, b;q=0.5'
166+
const result = parseAccept(header)
167+
expect(result).toEqual([
168+
{ type: 'a', params: {}, q: 1 },
169+
{ type: 'b', params: { q: '0.5' }, q: 0.5 },
170+
])
171+
})
172+
173+
test('skips malformed quoted param tail without creating bogus media type', () => {
174+
const header = 'a;foo="x"bar,b'
175+
const result = parseAccept(header)
176+
expect(result).toEqual([
177+
{ type: 'a', params: {}, q: 1 },
178+
{ type: 'b', params: {}, q: 1 },
179+
])
180+
})
181+
182+
test('parses params after quoted value with trailing whitespace', () => {
183+
const header = 'a;foo="x" ;q=0.5,b;q=0.4'
184+
const result = parseAccept(header)
185+
expect(result).toEqual([
186+
{ type: 'a', params: { foo: 'x', q: '0.5' }, q: 0.5 },
187+
{ type: 'b', params: { q: '0.4' }, q: 0.4 },
188+
])
189+
})
190+
191+
test('handles quoted param followed immediately by comma', () => {
192+
const header = 'a;foo="x",b'
193+
const result = parseAccept(header)
194+
expect(result).toEqual([
195+
{ type: 'a', params: { foo: 'x' }, q: 1 },
196+
{ type: 'b', params: {}, q: 1 },
197+
])
198+
})
199+
200+
test('skips empty media type that starts with semicolon', () => {
201+
const header = ';q=0.5,b;q=0.4'
202+
const result = parseAccept(header)
203+
expect(result).toEqual([{ type: 'b', params: { q: '0.4' }, q: 0.4 }])
204+
})
108205
})
109206

110207
describe('Security Cases', () => {
@@ -121,6 +218,12 @@ describe('parseAccept Comprehensive Tests', () => {
121218
})
122219
})
123220

221+
test('handles many semicolons with an unbalanced quote', () => {
222+
const header = `text/plain;${'a;'.repeat(8000)}"`
223+
const result = parseAccept(header)
224+
expect(result[0].type).toBe('text/plain')
225+
})
226+
124227
test('handles extremely large input', () => {
125228
const header = 'a;q=0.9,'.repeat(100000)
126229
expect(() => parseAccept(header)).not.toThrow()

0 commit comments

Comments
 (0)