import { safeReturnTo, DEFAULT_RETURN_TO } from '../../src/google/returnTo'; /** * The start route takes this from a query parameter, which means an attacker * chooses it. Unchecked, that turns a link on our own domain, with our own * certificate, into one that deposits the customer somewhere else entirely — * an open redirect wearing a sign-in flow as a disguise. * * So these are almost all about what must be refused, and each named case is a * different way of writing "somewhere else" that still looks local. */ describe('safeReturnTo', () => { it('keeps a path inside the site', () => { expect(safeReturnTo('/cart')).toBe('/cart'); }); it('keeps a query string, which is where browsing state lives', () => { // The auth modal returns customers to a filtered storefront, so dropping // the query would quietly undo what they were doing. expect(safeReturnTo('/?max_price=50000')).toBe('/?max_price=50000'); }); it.each([ ['a protocol-relative URL, which browsers treat as absolute', '//evil.test'], ['the backslash spelling of the same trick', '/\\evil.test'], ['an absolute https URL', 'https://evil.test'], ['an absolute http URL', 'http://evil.test'], ['a scheme with no slashes at all', 'javascript:alert(1)'], ['a bare path with no leading slash', 'evil.test'], ['an empty string', ''] ])('refuses %s', (_label, value) => { expect(safeReturnTo(value)).toBe(DEFAULT_RETURN_TO); }); it.each([ ['null', null], ['undefined', undefined], ['a number', 42], ['an array, which is what Express gives for a repeated parameter', ['/a', '/b']] ])('refuses %s', (_label, value) => { expect(safeReturnTo(value)).toBe(DEFAULT_RETURN_TO); }); it('refuses a value carrying a control character', () => { // A newline can split the Location header a browser reads, which is worth // refusing even though Express would very likely reject it first. expect(safeReturnTo('/cart\r\nSet-Cookie: rd_session=stolen')).toBe(DEFAULT_RETURN_TO); expect(safeReturnTo('/cart')).toBe(DEFAULT_RETURN_TO); }); it('does not refuse ordinary paths as a side effect of that check', () => { // The control-character guard was once written as a regex that inverted its // own character class and rejected everything, which passed every test // above and broke every real sign-in. expect(safeReturnTo('/orders')).toBe('/orders'); expect(safeReturnTo('/items/a-side-table-1920')).toBe('/items/a-side-table-1920'); }); });