Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: duplicate and expired cookies #25761

Merged
merged 47 commits into from
Feb 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
d13c0f6
chore: add regression tests for duplicate cookies and bad expiry times
AtofStryker Feb 9, 2023
2b97638
avoid prepending domain with dot for cookies that are set with the se…
AtofStryker Feb 9, 2023
9b6db60
feat: use cookie.toString() in the cookie patch to more accurately se…
AtofStryker Feb 8, 2023
3a05675
fix: add logic to handle expired cookies in the document.cookie patch…
AtofStryker Jan 23, 2023
27c6076
chore: build binary for cookie fixes for users to test
AtofStryker Feb 9, 2023
70b709c
chore: change name of fixture to something more accurate
AtofStryker Feb 9, 2023
91c3c7c
chore: comment why we are using the toughCookie toString method in th…
AtofStryker Feb 9, 2023
dcdb11c
[run ci]
AtofStryker Feb 9, 2023
844c831
chore: add changelog entry
AtofStryker Feb 9, 2023
bd62d4f
Merge branch 'develop' of github.com:cypress-io/cypress into fix-dupl…
AtofStryker Feb 9, 2023
38a65a6
[run ci]
AtofStryker Feb 9, 2023
7b90551
Merge branch 'develop' of github.com:cypress-io/cypress into fix-dupl…
AtofStryker Feb 13, 2023
4a9cfb5
fix: revert back to key=value when getting document.cookie as those a…
AtofStryker Feb 13, 2023
8927a3c
[run ci]
AtofStryker Feb 13, 2023
864baff
chore: make compatible with cypress.require
AtofStryker Feb 13, 2023
868c52c
fix: add tests for hostOnly/non hostOnly cookies to make sure propert…
AtofStryker Feb 13, 2023
0d0dbd6
Merge branch 'develop' of github.com:cypress-io/cypress into fix-dupl…
AtofStryker Feb 13, 2023
f545ab1
[run ci]
AtofStryker Feb 13, 2023
5b0a9bf
fix: stale unit test
AtofStryker Feb 13, 2023
ea31a02
chore: adjust comments
AtofStryker Feb 13, 2023
b93ce75
[run ci]
AtofStryker Feb 13, 2023
48b9550
fix: bad domain logic
AtofStryker Feb 13, 2023
2df76b7
[run ci]
AtofStryker Feb 13, 2023
3431437
chore: remove irrelevant comment
AtofStryker Feb 13, 2023
c1d8360
[run ci]
AtofStryker Feb 13, 2023
22c46f3
fix: adjust cookie login text to spec hostOnly cookie within the cook…
AtofStryker Feb 14, 2023
fd23fb1
[run ci]
AtofStryker Feb 14, 2023
913e317
Merge branch 'develop' of github.com:cypress-io/cypress into fix-dupl…
AtofStryker Feb 14, 2023
331dd69
[run ci]
AtofStryker Feb 14, 2023
17de188
fix: allow for cookies on request of same key to take precedence over…
AtofStryker Feb 14, 2023
c3c1088
chore: fix cookie misc tests for cy.origin (dont run cy.origin)
AtofStryker Feb 14, 2023
d44b202
[run ci]
AtofStryker Feb 14, 2023
173bda0
chore: skip misc cookie tests in webkit as headless behavior doesn't …
AtofStryker Feb 14, 2023
dca9773
Revert "fix: allow for cookies on request of same key to take precede…
AtofStryker Feb 14, 2023
86a8cd8
[run ci]
AtofStryker Feb 14, 2023
ea29a59
Merge branch 'develop' of github.com:cypress-io/cypress into fix-dupl…
AtofStryker Feb 15, 2023
4352ef5
chore: split changelog entry into two parts
AtofStryker Feb 15, 2023
a67c06d
chore: update logic to remove else statement and add comments
AtofStryker Feb 15, 2023
dff7db1
Merge branch 'develop' of github.com:cypress-io/cypress into fix-dupl…
AtofStryker Feb 15, 2023
6106b1f
[run ci]
AtofStryker Feb 15, 2023
0b9e237
chore: readd windows snapshot branch in workflows
AtofStryker Feb 15, 2023
95589fd
[run ci]
AtofStryker Feb 15, 2023
ef902f1
chore: fix workflows from bad merge
AtofStryker Feb 15, 2023
d5a2876
[run ci]
AtofStryker Feb 15, 2023
3a676ab
Merge branch 'develop' of github.com:cypress-io/cypress into fix-dupl…
AtofStryker Feb 16, 2023
8567812
Revert "chore: split changelog entry into two parts"
AtofStryker Feb 16, 2023
3ec78f1
[run ci]
AtofStryker Feb 16, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .circleci/workflows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ mainBuildFilters: &mainBuildFilters
only:
- develop
- /^release\/\d+\.\d+\.\d+$/
- 'tgriesser/spike/spike'
- 'fix-duplicate-and-expired-cookies'

# usually we don't build Mac app - it takes a long time
# but sometimes we want to really confirm we are doing the right thing
Expand All @@ -37,7 +37,7 @@ macWorkflowFilters: &darwin-workflow-filters
when:
or:
- equal: [ develop, << pipeline.git.branch >> ]
- equal: [ 'tgriesser/spike/spike', << pipeline.git.branch >> ]
- equal: [ 'fix-duplicate-and-expired-cookies', << pipeline.git.branch >> ]
- matches:
pattern: /^release\/\d+\.\d+\.\d+$/
value: << pipeline.git.branch >>
Expand All @@ -46,7 +46,7 @@ linuxArm64WorkflowFilters: &linux-arm64-workflow-filters
when:
or:
- equal: [ develop, << pipeline.git.branch >> ]
- equal: [ 'tgriesser/spike/spike', << pipeline.git.branch >> ]
- equal: [ 'fix-duplicate-and-expired-cookies', << pipeline.git.branch >> ]
- matches:
pattern: /^release\/\d+\.\d+\.\d+$/
value: << pipeline.git.branch >>
Expand All @@ -65,7 +65,7 @@ windowsWorkflowFilters: &windows-workflow-filters
or:
- equal: [ develop, << pipeline.git.branch >> ]
# use the following branch as well to ensure that v8 snapshot cache updates are fully tested
- equal: [ 'update-v8-snapshot-cache-on-develop', << pipeline.git.branch >> ]
- equal: [ 'update-v8-snapshot-cache-on-develop', 'fix-duplicate-and-expired-cookies', << pipeline.git.branch >> ]
- matches:
pattern: /^release\/\d+\.\d+\.\d+$/
value: << pipeline.git.branch >>
Expand Down Expand Up @@ -131,7 +131,7 @@ commands:
- run:
name: Check current branch to persist artifacts
command: |
if [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "release/"* && "$CIRCLE_BRANCH" != "tgriesser/spike/spike" ]]; then
if [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "release/"* && "$CIRCLE_BRANCH" != "fix-duplicate-and-expired-cookies" ]]; then
echo "Not uploading artifacts or posting install comment for this branch."
circleci-agent step halt
fi
Expand Down
8 changes: 8 additions & 0 deletions cli/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
<!-- See the ../guides/writing-the-cypress-changelog.md for details on writing the changelog. -->
## 12.6.1

_Released 03/1/2023 (PENDING)_

**Bugfixes:**

- Fixed an issue where cookies were being duplicated with the same hostname, but a prepended dot. Fixed an issue where cookies may not be expiring correctly. Fixes [#25174](https://github.com/cypress-io/cypress/issues/25174), [#25205](https://github.com/cypress-io/cypress/issues/25205) and [#25495](https://github.com/cypress-io/cypress/issues/25495).

## 12.6.0

_Released 02/15/2023_
Expand Down
4 changes: 2 additions & 2 deletions packages/driver/cypress/e2e/e2e/origin/cookie_login.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -723,7 +723,7 @@ describe('cy.origin - cookie login', { browser: '!webkit' }, () => {

cy.getCookie('key').then((cookie) => {
expect(Cypress._.omit(cookie, 'expiry')).to.deep.equal({
domain: '.www.foobar.com',
domain: 'www.foobar.com',
httpOnly: false,
name: 'key',
path: '/fixtures',
Expand Down Expand Up @@ -851,7 +851,7 @@ describe('cy.origin - cookie login', { browser: '!webkit' }, () => {
cy.get('[data-cy="doc-cookie"]').invoke('text').should('equal', 'name=value')
cy.getCookie('name').then((cookie) => {
expect(Cypress._.omit(cookie, 'expiry')).to.deep.equal({
domain: '.www.foobar.com',
domain: 'www.foobar.com',
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to change the logic here for the duplicate cookie work to apply the actual cookie domain or default hostOnly domain to the set cookie here. This is to make sure the cookie doesn't double up with a non host only equivalent in CDP. This should be fine for two reasons:

  • it's the specification default
  • the spec bridge is contained to itself in the same origin, which will contain the fully qualified hostname.

httpOnly: false,
name: 'name',
path: '/',
Expand Down
208 changes: 208 additions & 0 deletions packages/driver/cypress/e2e/e2e/origin/cookie_misc.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
// FIXME: currently cookies aren't cleared properly in headless mode with webkit between tests, as the below tests (excluding cy.origin) pass headfully locally.
describe('misc cookie tests', { browser: '!webkit' }, () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason we can add these tests to the existing e2e/cookie_behavior spec?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

main reason is just how large the spec already is. I almost think we should rename it, just not sold on something yet

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess I'd rather have one place to look for tests related to a topic so it's easier to see if we have coverage rather than searching multiple files 🤷🏻‍♀️

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, but considering how flaky that spec currently is, I don't want real issues to get lost in the noise. Not a great reason, but I also think the two specs are accomplishing two different things. This one is more for expiry and duplication bug fixes, and the cookie behavior tests are mostly checking attachment for xhr/fetch requests.

Would it be better to break the misc spec into two specs with the issues they are solving to show they serve a purpose for a regression test?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe group the various cookie tests under a cookie directory?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm gonna have a follow up to this PR that is going to address types, as well as refactoring some of these tests and addressing some of the cookie behavior flake

// NOTE: For this test to work correctly, we need to have a FQDN, not localhost (www.foobar.com).
// FIXES: https://github.com/cypress-io/cypress/issues/25174 (cookies are duplicated with prepended dot (.))
it('does not duplicate cookies with a prepended dot for cookies that are stored inside the server side cookie jar (host only)', () => {
cy.visit('https://www.foobar.com:3502/fixtures/trigger-cross-origin-redirect-to-self.html')

// does a 302 redirect back to www.foobar.com primary-origin page, but sets a sameSite=None cookie
cy.get('[data-cy="cookie-cross-origin-redirects-host-only"]').click()

cy.getCookies({ domain: 'www.foobar.com' }).then((cookies) => {
expect(cookies).to.have.length(1)

const singleCookie = cookies[0]

expect(singleCookie).to.have.property('name', 'foo')
expect(singleCookie).to.have.property('value', 'bar')
expect(singleCookie).to.have.property('domain', 'www.foobar.com')
})
})

it('does not duplicate cookies with a prepended dot for cookies that are stored inside the server side cookie jar (non-host only)', () => {
cy.visit('https://www.foobar.com:3502/fixtures/trigger-cross-origin-redirect-to-self.html')

// does a 302 redirect back to www.foobar.com primary-origin page, but sets a sameSite=None cookie
cy.get('[data-cy="cookie-cross-origin-redirects"]').click()

cy.getCookies({ domain: 'www.foobar.com' }).then((cookies) => {
expect(cookies).to.have.length(1)

const singleCookie = cookies[0]

expect(singleCookie).to.have.property('name', 'foo')
expect(singleCookie).to.have.property('value', 'bar')
expect(singleCookie).to.have.property('domain', '.www.foobar.com')
})
})

/**
* FIXES:
* https://github.com/cypress-io/cypress/issues/25205 (cookies set with expired time with value deleted show up as set with value deleted)
* https://github.com/cypress-io/cypress/issues/25495 (session cookies set with expired time with value deleted show up as set with value deleted)
* https://github.com/cypress-io/cypress/issues/25148 (cannot log into azure, shows cookies are disabled/blocked)
*/
describe('expiring cookies', { browser: '!webkit' }, () => {
before(() => {
cy.origin(`https://app.foobar.com:3503`, () => {
window.makeRequest = Cypress.require('../../../support/utils').makeRequestForCookieBehaviorTests
})
})

describe('removes cookies that are set with an expired expiry time from the server side cookie jar / browser via CDP', () => {
it('works with Max-Age=0', () => {
cy.visit(`https://www.foobar.com:3502/fixtures/primary-origin.html`)

cy.visit(`https://app.foobar.com:3503/fixtures/secondary-origin.html`)
cy.origin(`https://app.foobar.com:3503`, () => {
cy.window().then((win) => {
return cy.wrap(window.makeRequest(win, `/set-cookie?cookie=foo=bar; Domain=.foobar.com;`))
})

cy.getCookie('foo').its('value').should('eq', 'bar')

cy.window().then((win) => {
return cy.wrap(window.makeRequest(win, `/set-cookie?cookie=foo=deleted; Domain=.foobar.com; Max-Age=0;`))
})

cy.getCookie('foo').should('eq', null)
})
})

it('works with expires=Thu, 01-Jan-1970 00:00:01 GMT', () => {
cy.visit(`https://www.foobar.com:3502/fixtures/primary-origin.html`)

cy.visit(`https://app.foobar.com:3503/fixtures/secondary-origin.html`)
cy.origin(`https://app.foobar.com:3503`, () => {
cy.window().then((win) => {
return cy.wrap(window.makeRequest(win, `/set-cookie?cookie=foo=bar; Domain=.foobar.com;`))
})

cy.getCookie('foo').its('value').should('eq', 'bar')

cy.window().then((win) => {
return cy.wrap(window.makeRequest(win, `/set-cookie?cookie=foo=deleted; Domain=.foobar.com; expires=Thu, 01-Jan-1970 00:00:01 GMT;`))
})

cy.getCookie('foo').should('eq', null)
})
})

it('works with expires=Tues, 01-Jan-1980 00:00:01 GMT', () => {
cy.visit(`https://www.foobar.com:3502/fixtures/primary-origin.html`)

cy.visit(`https://app.foobar.com:3503/fixtures/secondary-origin.html`)
cy.origin(`https://app.foobar.com:3503`, () => {
cy.window().then((win) => {
return cy.wrap(window.makeRequest(win, `/set-cookie?cookie=foo=bar; Domain=.foobar.com;`))
})

cy.getCookie('foo').its('value').should('eq', 'bar')

cy.window().then((win) => {
return cy.wrap(window.makeRequest(win, `/set-cookie?cookie=foo=deleted; Domain=.foobar.com; expires=Tues, 01-Jan-1980 00:00:01 GMT; Max-Age=0;`))
})

cy.getCookie('foo').should('eq', null)
})
})

it('work with expires=Thu, 01-Jan-1970 00:00:01 GMT and Max-Age=0', () => {
cy.visit(`https://www.foobar.com:3502/fixtures/primary-origin.html`)

cy.visit(`https://app.foobar.com:3503/fixtures/secondary-origin.html`)
cy.origin(`https://app.foobar.com:3503`, () => {
cy.window().then((win) => {
return cy.wrap(window.makeRequest(win, `/set-cookie?cookie=foo=bar; Domain=.foobar.com;`))
})

cy.getCookie('foo').its('value').should('eq', 'bar')

cy.window().then((win) => {
return cy.wrap(window.makeRequest(win, `/set-cookie?cookie=foo=deleted; Domain=.foobar.com; expires=Thu, 01-Jan-1970 00:00:01 GMT; Max-Age=0;`))
})

cy.getCookie('foo').should('eq', null)
})
})
})

describe('removes cookies that are set with an expired expiry time from the document.cookie patch / browser via CDP', () => {
it('works with Max-Age=0', () => {
cy.visit(`https://www.foobar.com:3502/fixtures/primary-origin.html`)

cy.visit(`https://app.foobar.com:3503/fixtures/secondary-origin.html`)
cy.origin(`https://app.foobar.com:3503`, () => {
cy.window().then((win) => {
win.document.cookie = 'foo=bar'
})

cy.getCookie('foo').its('value').should('eq', 'bar')

cy.window().then((win) => {
win.document.cookie = 'foo=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; Max-Age=0;'
})

cy.getCookie('foo').should('eq', null)
})
})

it('works with expires=Thu, 01-Jan-1970 00:00:01 GMT', () => {
cy.visit(`https://www.foobar.com:3502/fixtures/primary-origin.html`)

cy.visit(`https://app.foobar.com:3503/fixtures/secondary-origin.html`)
cy.origin(`https://app.foobar.com:3503`, () => {
cy.window().then((win) => {
win.document.cookie = 'foo=bar'
})

cy.getCookie('foo').its('value').should('eq', 'bar')

cy.window().then((win) => {
win.document.cookie = 'foo=deleted; Max-Age=0'
})

cy.getCookie('foo').should('eq', null)
})
})

it('works with expires=Tues, 01-Jan-1980 00:00:01 GMT', () => {
cy.visit(`https://www.foobar.com:3502/fixtures/primary-origin.html`)

cy.visit(`https://app.foobar.com:3503/fixtures/secondary-origin.html`)
cy.origin(`https://app.foobar.com:3503`, () => {
cy.window().then((win) => {
win.document.cookie = 'foo=bar'
})

cy.getCookie('foo').its('value').should('eq', 'bar')

cy.window().then((win) => {
win.document.cookie = 'foo=deleted; expires=Tues, 01-Jan-1980 00:00:01 GMT'
})

cy.getCookie('foo').should('eq', null)
})
})

it('expires=Thu, 01-Jan-1970 00:00:01 GMT; Max-Age=0', () => {
cy.visit(`https://www.foobar.com:3502/fixtures/primary-origin.html`)

cy.visit(`https://app.foobar.com:3503/fixtures/secondary-origin.html`)
cy.origin(`https://app.foobar.com:3503`, () => {
cy.window().then((win) => {
win.document.cookie = 'foo=bar'
})

cy.getCookie('foo').its('value').should('eq', 'bar')

cy.window().then((win) => {
win.document.cookie = 'foo=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; Max-Age=0'
})

cy.getCookie('foo').should('eq', null)
})
})
})
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<a data-cy="cookie-cross-origin-redirects-host-only">Trigger Cross Origin Redirect Loop that sets host only cookie</a>
<a data-cy="cookie-cross-origin-redirects">Trigger Cross Origin Redirect Loop that sets non host only cookie</a>
<script>
function setHref (dataCy, redirect, cookie) {
const redirectEncoded = encodeURIComponent(redirect)
const cookieEncoded = encodeURIComponent(cookie)

document.querySelector(`[data-cy="${dataCy}"]`).href = (
`/set-same-site-none-cookie-on-redirect?redirect=${redirectEncoded}&cookie=${cookieEncoded}`
)
}
setHref('cookie-cross-origin-redirects-host-only', `${window.location.origin}/fixtures/primary-origin.html`, 'foo=bar')
setHref('cookie-cross-origin-redirects', `${window.location.origin}/fixtures/primary-origin.html`, `foo=bar; domain=.${window.location.hostname}`)
</script>
</body>
</html>
11 changes: 11 additions & 0 deletions packages/driver/cypress/plugins/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,17 @@ const createApp = (port) => {
.sendStatus(200)
})

app.get('/set-same-site-none-cookie-on-redirect', (req, res) => {
const { redirect, cookie } = req.query
const cookieDecoded = decodeURIComponent(cookie)

const cookieVal = `${cookieDecoded}; SameSite=None; Secure`

res
.header('Set-Cookie', cookieVal)
.redirect(302, redirect)
})

app.get('/test-request-credentials', (req, res) => {
const origin = cors.getOrigin(req['headers']['referer'])

Expand Down
33 changes: 32 additions & 1 deletion packages/runner/injection/patches/cookies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,25 @@ import {
import { Cookie as ToughCookie } from 'tough-cookie'
import type { AutomationCookie } from '@packages/server/lib/automation/cookies'

function isHostOnlyCookie (domain) {
return domain[0] !== '.'
}

const parseDocumentCookieString = (documentCookieString: string): AutomationCookie[] => {
if (!documentCookieString || !documentCookieString.trim().length) return []

return documentCookieString.split(';').map((cookieString) => {
const [name, value] = cookieString.split('=')

let cookieDomain = location.hostname

return {
name: name.trim(),
value: value.trim(),
domain: location.hostname,
domain: cookieDomain,
expiry: null,
httpOnly: false,
hostOnly: isHostOnlyCookie(cookieDomain),
maxAge: null,
path: null,
sameSite: 'lax',
Expand Down Expand Up @@ -88,9 +95,33 @@ export const patchDocumentCookie = (requestCookies: AutomationCookie[]) => {
const stringValue = `${newValue}`
const parsedCookie = CookieJar.parse(stringValue)

if (parsedCookie?.hostOnly === null) {
// we want to make sure the hostOnly property is respected when syncing with CDP/extension to prevent duplicates.
// in the case it is not set, we need to calculate it
parsedCookie.hostOnly = isHostOnlyCookie(parsedCookie.domain || domain)
}

// if result is undefined, it was invalid and couldn't be parsed
if (!parsedCookie) return getDocumentCookieValue()

// if the cookie is expired, remove it in our cookie jar
// and via setting it inside our automation client with the correct expiry.
// This will have the effect of removing the cookie
if (parsedCookie.expiryTime() < Date.now()) {
cookieJar.removeCookie({
name: parsedCookie.key,
path: parsedCookie.path || '/',
domain: parsedCookie.domain as string,
})

// send the cookie to the server so it can be removed from the browser
// via aututomation. If the cookie expiry is set inside the server-side cookie jar,
// the cookie will be automatically removed.
sendCookieToServer(toughCookieToAutomationCookie(parsedCookie, domain))

return getDocumentCookieValue()
}

// we should be able to pass in parsedCookie here instead of the string
// value, but tough-cookie doesn't recognize it using an instanceof
// check and throws an error. because we can't, we have to massage
Expand Down
Loading