diff --git a/packages/common/http/test/xsrf_spec.ts b/packages/common/http/test/xsrf_spec.ts index 4ed54bf7060..85120081582 100644 --- a/packages/common/http/test/xsrf_spec.ts +++ b/packages/common/http/test/xsrf_spec.ts @@ -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 { diff --git a/packages/common/src/cookie.ts b/packages/common/src/cookie.ts index 67658c9c5b5..f0cb086135f 100644 --- a/packages/common/src/cookie.ts +++ b/packages/common/src/cookie.ts @@ -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; } diff --git a/packages/common/test/cookie_spec.ts b/packages/common/test/cookie_spec.ts index 84cf5f23d75..fbbb892aced 100644 --- a/packages/common/test/cookie_spec.ts +++ b/packages/common/test/cookie_spec.ts @@ -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'); + }); });