fix(http): strip RFC 6265 DQUOTE characters and handle URIError in parseCookieValue

Previously, `parseCookieValue` did not strip enclosing double quotes (`DQUOTE`) from quoted cookie values as specified in RFC 6265 Section 4.1.1. In addition, malformed percent-encoding in cookie values caused an unhandled `URIError` when calling `decodeURIComponent`.

(cherry picked from commit 280d09b160)
This commit is contained in:
Alan Agius
2026-08-04 17:29:46 +02:00
committed by Jessica Janiuk
parent 688a0a7118
commit 32af9b525e
3 changed files with 56 additions and 2 deletions
+21
View File
@@ -135,6 +135,7 @@ describe('HttpXsrfInterceptor', () => {
expect(req.request.headers.get('X-XSRF-TOKEN')).toEqual('blah');
req.flush({});
});
it('does not set the header for a null token', () => {
TestBed.resetTestingModule();
TestBed.configureTestingModule({
@@ -160,10 +161,12 @@ describe('HttpXsrfInterceptor', () => {
expect(req.request.headers.has('X-XSRF-TOKEN')).toEqual(false);
req.flush({});
});
afterEach(() => {
backend.verify();
});
});
describe('HttpXsrfCookieExtractor', () => {
let document: {[key: string]: string};
let extractor: HttpXsrfCookieExtractor;
@@ -181,20 +184,38 @@ describe('HttpXsrfCookieExtractor', () => {
});
extractor = TestBed.inject(HttpXsrfCookieExtractor);
});
it('parses the cookie from document.cookie', () => {
expect(extractor.getToken()).toEqual('test');
});
it('does not re-parse if document.cookie has not changed', () => {
expect(extractor.getToken()).toEqual('test');
expect(extractor.getToken()).toEqual('test');
expect(getParseCount(extractor)).toEqual(1);
});
it('re-parses if document.cookie changes', () => {
expect(extractor.getToken()).toEqual('test');
document['cookie'] = 'XSRF-TOKEN=blah';
expect(extractor.getToken()).toEqual('blah');
expect(getParseCount(extractor)).toEqual(2);
});
it('extracts token without quotes when value is enclosed in DQUOTE characters', () => {
document['cookie'] = 'XSRF-TOKEN="quoted-token-value"';
expect(extractor.getToken()).toEqual('quoted-token-value');
});
it('extracts token without quotes when value is enclosed in URL-encoded DQUOTE characters (%22)', () => {
document['cookie'] = 'XSRF-TOKEN=%22quoted-token-value%22';
expect(extractor.getToken()).toEqual('quoted-token-value');
});
it('extracts token without crashing when value has malformed percent-encoding', () => {
document['cookie'] = 'XSRF-TOKEN=%ZZ';
expect(extractor.getToken()).toEqual('%ZZ');
});
});
function getParseCount(extractor: HttpXsrfCookieExtractor): number {
+18 -2
View File
@@ -8,13 +8,29 @@
export function parseCookieValue(cookieStr: string, name: string): string | null {
name = encodeURIComponent(name);
for (const cookie of cookieStr.split(';')) {
const eqIndex = cookie.indexOf('=');
const [cookieName, cookieValue]: string[] =
eqIndex == -1 ? [cookie, ''] : [cookie.slice(0, eqIndex), cookie.slice(eqIndex + 1)];
if (cookieName.trim() === name) {
return decodeURIComponent(cookieValue);
if (cookieName.trim() !== name) {
continue;
}
let value = cookieValue;
try {
value = decodeURIComponent(cookieValue);
} catch {
// Fall back to raw cookie value if decoding fails (e.g. malformed percent-encoding).
}
if (value.length > 1 && value[0] === '"' && value[value.length - 1] === '"') {
value = value.slice(1, -1);
}
return value;
}
return null;
}
+17
View File
@@ -21,13 +21,30 @@ describe('cookies', () => {
const cookie = 'other-cookie=false; xsrf-token=token-value; is_awesome=true; ffo=true;';
expect(parseCookieValue(cookie, 'xsrf-token')).toBe('token-value');
});
it('handles encoded keys', () => {
expect(parseCookieValue('whitespace%20token=token-value', 'whitespace token')).toBe(
'token-value',
);
});
it('handles encoded values', () => {
expect(parseCookieValue('token=whitespace%20', 'token')).toBe('whitespace ');
expect(parseCookieValue('token=whitespace%0A', 'token')).toBe('whitespace\n');
});
it('strips DQUOTE characters per RFC 6265 Section 4.1.1', () => {
expect(parseCookieValue('token="abc123"', 'token')).toBe('abc123');
expect(parseCookieValue('token=%22abc123%22', 'token')).toBe('abc123');
expect(parseCookieValue('token="abc=def"', 'token')).toBe('abc=def');
expect(parseCookieValue('token="abc def"', 'token')).toBe('abc def');
expect(parseCookieValue('token=""', 'token')).toBe('');
expect(parseCookieValue('token="abc"', 'token')).toBe('abc');
expect(parseCookieValue('token="', 'token')).toBe('"');
});
it('handles malformed percent-encoding without throwing URIError', () => {
expect(parseCookieValue('token=%ZZ', 'token')).toBe('%ZZ');
expect(parseCookieValue('token="abc%ZZ"', 'token')).toBe('abc%ZZ');
});
});