diff --git a/packages/playwright-core/src/server/fetch.ts b/packages/playwright-core/src/server/fetch.ts index 00d7aa3f34..58921d11f4 100644 --- a/packages/playwright-core/src/server/fetch.ts +++ b/packages/playwright-core/src/server/fetch.ts @@ -39,6 +39,7 @@ import { ProgressController } from './progress'; import { Tracing } from './trace/recorder/tracing'; import type * as types from './types'; import type { HeadersArray, ProxySettings } from './types'; +import { kMaxCookieExpiresDateInSeconds } from './network'; type FetchRequestOptions = { userAgent: string; @@ -606,13 +607,25 @@ function parseCookie(header: string): channels.NetworkCookie | null { switch (name.toLowerCase()) { case 'expires': const expiresMs = (+new Date(value)); - if (isFinite(expiresMs)) - cookie.expires = expiresMs / 1000; + // https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.1 + if (isFinite(expiresMs)) { + if (expiresMs <= 0) + cookie.expires = 0; + else + cookie.expires = Math.min(expiresMs / 1000, kMaxCookieExpiresDateInSeconds); + } break; case 'max-age': const maxAgeSec = parseInt(value, 10); - if (isFinite(maxAgeSec)) - cookie.expires = Date.now() / 1000 + maxAgeSec; + if (isFinite(maxAgeSec)) { + // From https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.2 + // If delta-seconds is less than or equal to zero (0), let expiry-time + // be the earliest representable date and time. + if (maxAgeSec <= 0) + cookie.expires = 0; + else + cookie.expires = Math.min(Date.now() / 1000 + maxAgeSec, kMaxCookieExpiresDateInSeconds); + } break; case 'domain': cookie.domain = value.toLocaleLowerCase() || ''; diff --git a/packages/playwright-core/src/server/firefox/ffBrowser.ts b/packages/playwright-core/src/server/firefox/ffBrowser.ts index 9cb79034f9..d6d22b3eae 100644 --- a/packages/playwright-core/src/server/firefox/ffBrowser.ts +++ b/packages/playwright-core/src/server/firefox/ffBrowser.ts @@ -283,7 +283,7 @@ export class FFBrowserContext extends BrowserContext { async addCookies(cookies: channels.SetNetworkCookie[]) { const cc = network.rewriteCookies(cookies).map(c => ({ ...c, - expires: c.expires && c.expires !== -1 ? c.expires : undefined, + expires: c.expires === -1 ? undefined : c.expires, })); await this._browser._connection.send('Browser.setCookies', { browserContextId: this._browserContextId, cookies: cc }); } diff --git a/packages/playwright-core/src/server/network.ts b/packages/playwright-core/src/server/network.ts index 0ec0ea6efa..fdf0573ac3 100644 --- a/packages/playwright-core/src/server/network.ts +++ b/packages/playwright-core/src/server/network.ts @@ -52,7 +52,7 @@ export function filterCookies(cookies: channels.NetworkCookie[], urls: string[]) // Rollover to 5-digit year: // 253402300799 == Fri, 31 Dec 9999 23:59:59 +0000 (UTC) // 253402300800 == Sat, 1 Jan 1000 00:00:00 +0000 (UTC) -const kMaxCookieExpiresDateInSeconds = 253402300799; +export const kMaxCookieExpiresDateInSeconds = 253402300799; export function rewriteCookies(cookies: channels.SetNetworkCookie[]): channels.SetNetworkCookie[] { return cookies.map(c => { diff --git a/tests/library/browsercontext-fetch.spec.ts b/tests/library/browsercontext-fetch.spec.ts index a44ab8a941..913c21c664 100644 --- a/tests/library/browsercontext-fetch.spec.ts +++ b/tests/library/browsercontext-fetch.spec.ts @@ -270,6 +270,43 @@ it('should not lose body while handling Set-Cookie header', async ({ context, se expect(await response.text()).toBe('text content'); }); +it('should remove cookie with negative max-age', async ({ page, server }) => { + server.setRoute('/setcookie.html', (req, res) => { + res.setHeader('Set-Cookie', ['a=v; max-age=100000', `b=v; max-age=100000`, 'c=v']); + res.end(); + }); + server.setRoute('/removecookie.html', (req, res) => { + const maxAge = -2 * Date.now(); + res.setHeader('Set-Cookie', [`a=v; max-age=${maxAge}`, `b=v; max-age=-1`]); + res.end(); + }); + await page.request.get(`${server.PREFIX}/setcookie.html`); + await page.request.get(`${server.PREFIX}/removecookie.html`); + const [serverRequest] = await Promise.all([ + server.waitForRequest('/empty.html'), + page.request.get(server.EMPTY_PAGE) + ]); + expect(serverRequest.headers.cookie).toBe('c=v'); +}); + +it('should remove cookie with expires far in the past', async ({ page, server }) => { + server.setRoute('/setcookie.html', (req, res) => { + res.setHeader('Set-Cookie', ['a=v; max-age=1000000']); + res.end(); + }); + server.setRoute('/removecookie.html', (req, res) => { + res.setHeader('Set-Cookie', [`a=v; expires=Wed, 01 Jan 1000 00:00:00 GMT`]); + res.end(); + }); + await page.request.get(`${server.PREFIX}/setcookie.html`); + await page.request.get(`${server.PREFIX}/removecookie.html`); + const [serverRequest] = await Promise.all([ + server.waitForRequest('/empty.html'), + page.request.get(server.EMPTY_PAGE) + ]); + expect(serverRequest.headers.cookie).toBeFalsy(); +}); + it('should handle cookies on redirects', async ({ context, server, browserName, isWindows }) => { server.setRoute('/redirect1', (req, res) => { res.setHeader('Set-Cookie', 'r1=v1;SameSite=Lax'); diff --git a/tests/library/global-fetch-cookie.spec.ts b/tests/library/global-fetch-cookie.spec.ts index 25532d2f91..374d6eef90 100644 --- a/tests/library/global-fetch-cookie.spec.ts +++ b/tests/library/global-fetch-cookie.spec.ts @@ -168,6 +168,43 @@ it('should remove expired cookies', async ({ request, server }) => { expect(serverRequest.headers.cookie).toBe('a=v'); }); +it('should remove cookie with negative max-age', async ({ request, server }) => { + server.setRoute('/setcookie.html', (req, res) => { + res.setHeader('Set-Cookie', ['a=v; max-age=100000', `b=v; max-age=100000`, 'c=v']); + res.end(); + }); + server.setRoute('/removecookie.html', (req, res) => { + const maxAge = -2 * Date.now(); + res.setHeader('Set-Cookie', [`a=v; max-age=${maxAge}`, `b=v; max-age=-1`]); + res.end(); + }); + await request.get(`${server.PREFIX}/setcookie.html`); + await request.get(`${server.PREFIX}/removecookie.html`); + const [serverRequest] = await Promise.all([ + server.waitForRequest('/empty.html'), + request.get(server.EMPTY_PAGE) + ]); + expect(serverRequest.headers.cookie).toBe('c=v'); +}); + +it('should remove cookie with expires far in the past', async ({ request, server }) => { + server.setRoute('/setcookie.html', (req, res) => { + res.setHeader('Set-Cookie', ['a=v; max-age=1000000']); + res.end(); + }); + server.setRoute('/removecookie.html', (req, res) => { + res.setHeader('Set-Cookie', [`a=v; expires=1 Jan 1000 00:00:00 +0000 (UTC)`]); + res.end(); + }); + await request.get(`${server.PREFIX}/setcookie.html`); + await request.get(`${server.PREFIX}/removecookie.html`); + const [serverRequest] = await Promise.all([ + server.waitForRequest('/empty.html'), + request.get(server.EMPTY_PAGE) + ]); + expect(serverRequest.headers.cookie).toBeFalsy(); +}); + it('should store cookie from Set-Cookie header even if it contains equal signs', async ({ request, server }) => { it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/11612' });