Skip to content

Commit

Permalink
fix: Disallow same-superdomain-origin cy.origin blocks (#24569)
Browse files Browse the repository at this point in the history
* fix: throw error if the cy.origin origin is in the same superDomainOrigin as top.

* testing test tweaks

* 'fix' cypress in cypress tests

* Inject cross origin in google subdomains when not same-origin

* style tweaks

* Ensure strict same-origin check works for google.

* test fixes

* we don't need the location object when we just want the href.

* what is in a name?

* Address PR Comments
  • Loading branch information
mjhenkes committed Nov 9, 2022
1 parent 7093072 commit 23299ac
Show file tree
Hide file tree
Showing 20 changed files with 548 additions and 474 deletions.
38 changes: 38 additions & 0 deletions packages/driver/cypress/e2e/e2e/origin/origin.cy.ts
Expand Up @@ -10,6 +10,44 @@ describe('cy.origin', { browser: '!webkit' }, () => {
})
})

it('creates and injects into google subdomains', () => {
// Intercept google to keep our tests independent from google.
cy.intercept('https://www.google.com', {
body: '<html><head><title></title></head><body><p>google.com</p></body></html>',
})

cy.intercept('https://accounts.google.com', {
body: '<html><head><title></title></head><body><p>accounts.google.com</p></body></html>',
})

cy.visit('https://www.google.com')
cy.visit('https://accounts.google.com')
cy.origin('https://accounts.google.com', () => {
cy.window().then((win) => {
expect(win.Cypress).to.exist
})
})
})

it('creates and injects into google subdomains when visiting in an origin block', () => {
// Intercept google to keep our tests independent from google.
cy.intercept('https://www.google.com', {
body: '<html><head><title></title></head><body><p>google.com</p></body></html>',
})

cy.intercept('https://accounts.google.com', {
body: '<html><head><title></title></head><body><p>accounts.google.com</p></body></html>',
})

cy.visit('https://www.google.com')
cy.origin('https://accounts.google.com', () => {
cy.visit('https://accounts.google.com')
cy.window().then((win) => {
expect(win.Cypress).to.exist
})
})
})

it('passes viewportWidth/Height state to the secondary origin', () => {
const expectedViewport = [320, 480]

Expand Down
84 changes: 84 additions & 0 deletions packages/driver/cypress/e2e/e2e/origin/validation.cy.ts
@@ -1,4 +1,8 @@
describe('cy.origin', { browser: '!webkit' }, () => {
beforeEach(() => {
cy.visit('')
})

describe('successes', () => {
it('succeeds on a localhost domain name', () => {
cy.origin('localhost', () => undefined)
Expand Down Expand Up @@ -357,3 +361,83 @@ describe('cy.origin', { browser: '!webkit' }, () => {
})
})
})

describe('cy.origin - external hosts', { browser: '!webkit' }, () => {
describe('successes', () => {
it('succeeds on a complete origin from https using https', () => {
cy.visit('https://www.foobar.com:3502/fixtures/primary-origin.html')
cy.origin('https://www.idp.com:3502', () => undefined)
cy.then(() => {
const expectedSrc = `https://www.idp.com:3502/__cypress/spec-bridge-iframes`
const iframe = window.top?.document.getElementById('Spec\ Bridge:\ https://www.idp.com:3502') as HTMLIFrameElement

expect(iframe.src).to.equal(expectedSrc)
})
})

it('succeeds if url is the super domain as top but the super domain is excepted and must be strictly same origin', () => {
// Intercept google to keep our tests independent from google.
cy.intercept('https://www.google.com', {
body: '<html><head><title></title></head><body><p></body></html>',
})

cy.visit('https://www.google.com')
cy.origin('accounts.google.com', () => undefined)
cy.then(() => {
const expectedSrc = `https://accounts.google.com/__cypress/spec-bridge-iframes`
const iframe = window.top?.document.getElementById('Spec\ Bridge:\ https://accounts.google.com') as HTMLIFrameElement

expect(iframe.src).to.equal(expectedSrc)
})
})
})

describe('errors', () => {
it('errors if the url param is same superDomainOrigin as top', (done) => {
cy.on('fail', (err) => {
expect(err.message).to.include('`cy.origin()` requires the first argument to be a different domain than top. You passed `http://app.foobar.com` to the origin command, while top is at `http://www.foobar.com`.')

done()
})

cy.intercept('http://www.foobar.com', {
body: '<html><head><title></title></head><body><p></body></html>',
})

cy.intercept('http://app.foobar.com', {
body: '<html><head><title></title></head><body><p></body></html>',
})

cy.visit('http://www.foobar.com')

cy.origin('http://app.foobar.com', () => undefined)
})

it('errors if the url param is same origin as top', (done) => {
cy.on('fail', (err) => {
expect(err.message).to.include('`cy.origin()` requires the first argument to be a different origin than top. You passed `https://www.google.com` to the origin command, while top is at `https://www.google.com`.')

done()
})

// Intercept google to keep our tests independent from google.
cy.intercept('https://www.google.com', {
body: '<html><head><title></title></head><body><p></body></html>',
})

cy.visit('https://www.google.com')
cy.origin('https://www.google.com', () => undefined)
})

it('errors and does not hang when throwing a mixed content error creating the spec bridge', { defaultCommandTimeout: 50 }, (done) => {
cy.on('fail', (err) => {
expect(err.message).to.include(`\`cy.origin()\` failed to create a spec bridge to communicate with the specified origin. This can happen when you attempt to create a spec bridge to an insecure (http) frame from a secure (https) frame.`)

done()
})

cy.visit('https://www.foobar.com:3502/fixtures/primary-origin.html')
cy.origin('http://www.foobar.com:3500', () => {})
})
})
})
30 changes: 16 additions & 14 deletions packages/driver/src/cy/commands/origin/index.ts
Expand Up @@ -38,7 +38,7 @@ export default (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, state: State
communicator.userInvocationStack = userInvocationStack

// this command runs for as long as the commands in the secondary
// origin run, so it can't have its own timeout
// origin run, so it can't have its own timeout except in the case where we're creating the spec bridge.
cy.clearTimeout()

if (!config('experimentalSessionAndOrigin')) {
Expand All @@ -47,6 +47,7 @@ export default (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, state: State

let options
let callbackFn
const timeout = Cypress.config('defaultCommandTimeout')

if (fn) {
callbackFn = fn
Expand All @@ -64,7 +65,7 @@ export default (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, state: State
name: 'origin',
type: 'parent',
message: urlOrDomain,
timeout: 0,
timeout,
// @ts-ignore TODO: revisit once log-grouping has more implementations
}, (_log) => {
log = _log
Expand All @@ -84,15 +85,15 @@ export default (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, state: State
const url = new URL(normalizeOrigin(urlOrDomain)).toString()
const location = $Location.create(url)

validator.validateLocation(location, urlOrDomain)
validator.validateLocation(location, urlOrDomain, window.location.href)

const origin = location.origin

// This is set while IN the cy.origin command.
cy.state('currentActiveOrigin', origin)

return new Bluebird((resolve, reject, onCancel) => {
const cleanup = ({ readyForOriginFailed }: {readyForOriginFailed?: boolean} = {}): void => {
const cleanup = (): void => {
cy.state('currentActiveOrigin', undefined)

communicator.off('queue:finished', onQueueFinished)
Expand All @@ -108,8 +109,10 @@ export default (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, state: State
resolve(unserializableSubjectType ? createUnserializableSubjectProxy(unserializableSubjectType) : subject)
}

const _reject = (err, cleanupOptions: {readyForOriginFailed?: boolean} = {}) => {
cleanup(cleanupOptions)
const _reject = (err) => {
// Prevent cypress from trying to add the function to the error log
err.onFail = () => {}
cleanup()
log?.error(err)
reject(err)
}
Expand Down Expand Up @@ -141,9 +144,6 @@ export default (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, state: State
wrappedErr.name = err.name
wrappedErr.stack = $stackUtils.replacedStack(wrappedErr, err.stack)

// Prevent cypress from trying to add the function to the error log
wrappedErr.onFail = () => {}

return _reject(wrappedErr)
}

Expand All @@ -168,9 +168,15 @@ export default (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, state: State
})
}

// If the spec bridge isn't created in time, it likely failed and we shouldn't hang the test.
const timeoutId = setTimeout(() => {
_reject($errUtils.errByPath('origin.failed_to_create_spec_bridge'))
}, timeout)

// fired once the spec bridge is set up and ready to receive messages
communicator.once('bridge:ready', async (_data, { origin: specBridgeOrigin }) => {
if (specBridgeOrigin === origin) {
clearTimeout(timeoutId)
// now that the spec bridge is ready, instantiate Cypress with the current app config and environment variables for initial sync when creating the instance
communicator.toSpecBridge(origin, 'initialize:cypress', {
config: preprocessConfig(Cypress.config()),
Expand Down Expand Up @@ -229,11 +235,7 @@ export default (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, state: State
// It tries to add a bunch of stuff that's not useful and ends up
// messing up the stack that we want on the error
wrappedErr.__stackCleaned__ = true

// Prevent cypress from trying to add the function to the error log
wrappedErr.onFail = () => {}

_reject(wrappedErr, { readyForOriginFailed: true })
_reject(wrappedErr)
}
}
})
Expand Down
28 changes: 26 additions & 2 deletions packages/driver/src/cy/commands/origin/validator.ts
@@ -1,6 +1,8 @@
import $utils from '../../../cypress/utils'
import $errUtils from '../../../cypress/error_utils'
import { difference, isPlainObject, isString } from 'lodash'
import type { LocationObject } from '../../../cypress/location'
import * as cors from '@packages/network/lib/cors'

const validOptionKeys = Object.freeze(['args'])

Expand Down Expand Up @@ -67,13 +69,35 @@ export class Validator {
return false
}

validateLocation (location, urlOrDomain) {
/**
* Validates the location parameter of the cy.origin call.
* @param originLocation - the location passed into the cy.origin command.
* @param urlOrDomain - the original string param passed in.
* @param specHref - the address of the current spec.
*/
validateLocation (originLocation: LocationObject, urlOrDomain: string, specHref: string): void {
// we don't support query params
if (location.search.length > 0) {
if (originLocation.search.length > 0) {
$errUtils.throwErrByPath('origin.invalid_url_argument', {
onFail: this.log,
args: { arg: $utils.stringify(urlOrDomain) },
})
}

// Users would be better off not using cy.origin if the origin is part of the same super domain.
if (cors.urlMatchesPolicyBasedOnDomain(originLocation.href, specHref)) {
// this._isSameSuperDomainOriginWithExceptions({ originLocation, specLocation })) {

const policy = cors.policyForDomain(originLocation.href)

$errUtils.throwErrByPath('origin.invalid_url_argument_same_origin', {
onFail: this.log,
args: {
originUrl: $utils.stringify(urlOrDomain),
topOrigin: (window.location.origin),
policy,
},
})
}
}
}
14 changes: 14 additions & 0 deletions packages/driver/src/cypress/error_messages.ts
Expand Up @@ -1191,6 +1191,13 @@ export default {
invalid_url_argument: {
message: `${cmd('origin')} requires the first argument to be either a url (\`https://www.example.com/path\`) or a domain name (\`example.com\`). Query parameters are not allowed. You passed: \`{{arg}}\``,
},
invalid_url_argument_same_origin ({ originUrl, topOrigin, policy }) {
return stripIndent`\
${cmd('origin')} requires the first argument to be a different ${policy === 'same-origin' ? 'origin' : 'domain' } than top. You passed \`${originUrl}\` to the origin command, while top is at \`${topOrigin}\`.
Either the intended page was not visited prior to running the cy.origin block or the cy.origin block may not be needed at all.
`
},
invalid_options_argument: {
message: `${cmd('origin')} requires the 'options' argument to be an object. You passed: \`{{arg}}\``,
},
Expand Down Expand Up @@ -1260,6 +1267,13 @@ export default {
message: stripIndent`\
${cmd('origin')} could not serialize the thrown value. Please make sure the value being thrown is supported by the structured clone algorithm.`,
},
failed_to_create_spec_bridge: {
message: stripIndent`\
${cmd('origin')} failed to create a spec bridge to communicate with the specified origin. This can happen when you attempt to create a spec bridge to an insecure (http) frame from a secure (https) frame.
Check your Developer Tools Console for the actual error - it should be printed there.
`,
},
unsupported: {
route: {
message: `${cmd('route')} has been deprecated and its use is not supported in the ${cmd('origin')} callback. Consider using ${cmd('intercept')} (outside of the callback) instead.`,
Expand Down

5 comments on commit 23299ac

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 23299ac Nov 9, 2022

Choose a reason for hiding this comment

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

Circle has built the linux arm64 version of the Test Runner.

Learn more about this pre-release platform-specific build at https://on.cypress.io/installing-cypress#Install-pre-release-version.

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/11.0.1/linux-arm64/develop-23299acc88ba8a8c14dfb3df6aa9ef0f4c1b4063/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 23299ac Nov 9, 2022

Choose a reason for hiding this comment

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

Circle has built the linux x64 version of the Test Runner.

Learn more about this pre-release platform-specific build at https://on.cypress.io/installing-cypress#Install-pre-release-version.

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/11.0.1/linux-x64/develop-23299acc88ba8a8c14dfb3df6aa9ef0f4c1b4063/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 23299ac Nov 9, 2022

Choose a reason for hiding this comment

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

Circle has built the darwin x64 version of the Test Runner.

Learn more about this pre-release platform-specific build at https://on.cypress.io/installing-cypress#Install-pre-release-version.

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/11.0.1/darwin-x64/develop-23299acc88ba8a8c14dfb3df6aa9ef0f4c1b4063/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 23299ac Nov 9, 2022

Choose a reason for hiding this comment

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

Circle has built the win32 x64 version of the Test Runner.

Learn more about this pre-release platform-specific build at https://on.cypress.io/installing-cypress#Install-pre-release-version.

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/11.0.1/win32-x64/develop-23299acc88ba8a8c14dfb3df6aa9ef0f4c1b4063/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 23299ac Nov 9, 2022

Choose a reason for hiding this comment

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

Circle has built the darwin arm64 version of the Test Runner.

Learn more about this pre-release platform-specific build at https://on.cypress.io/installing-cypress#Install-pre-release-version.

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/11.0.1/darwin-arm64/develop-23299acc88ba8a8c14dfb3df6aa9ef0f4c1b4063/cypress.tgz

Please sign in to comment.