From cb8c26dffdfdd2a2637da26064147e3773a684c1 Mon Sep 17 00:00:00 2001 From: Bill Glesias Date: Mon, 12 Sep 2022 17:05:32 -0400 Subject: [PATCH] feat: allow for spec bridge to either match specific origin policy or super domain origin policy --- packages/network/lib/cors.ts | 12 +++++++----- packages/proxy/lib/http/response-middleware.ts | 12 ++++++------ packages/runner/injection/cross-origin.js | 13 ++++++++++++- packages/server/lib/remote_states.ts | 10 +++++----- 4 files changed, 30 insertions(+), 17 deletions(-) diff --git a/packages/network/lib/cors.ts b/packages/network/lib/cors.ts index 95dc47699052..1decd4bb1cb8 100644 --- a/packages/network/lib/cors.ts +++ b/packages/network/lib/cors.ts @@ -74,18 +74,20 @@ export function getDomainNameFromParsedHost (parsedHost: ParsedHost) { return _.compact([parsedHost.domain, parsedHost.tld]).join('.') } -export function urlMatchesOriginPolicyProps (urlStr, props) { +export function urlMatchesSuperDomainOriginPolicyProps (urlStr, props) { // take a shortcut here in the case // where remoteHostAndPort is null if (!props) { return false } - const parsedUrl = parseUrlIntoHostProtocolDomainTldPort(urlStr) + const { subdomain: sub1, ...parsedUrl } = parseUrlIntoHostProtocolDomainTldPort(urlStr) + const { subdomain: sub2, ...propsOmittedSubDomain } = props - // does the parsedUrl match the parsedHost? - // To fully match origin policy, the full host (including subdomain) and port is required to match. @see https://developer.mozilla.org/en-US/docs/Glossary/Origin - return _.isEqual(parsedUrl, props) + // To fully match the super domain origin policy, the full host (excluding subdomain) and port is required to match. @see https://developer.mozilla.org/en-US/docs/Glossary/Origin + const doSubDomainsFitSuperDomainPolicy = (sub1 === null || sub2 === null) ? true : sub1 === sub2 + + return _.isEqual(parsedUrl, propsOmittedSubDomain) && doSubDomainsFitSuperDomainPolicy } export function urlMatchesSameSitePolicyProps (urlStr, props) { diff --git a/packages/proxy/lib/http/response-middleware.ts b/packages/proxy/lib/http/response-middleware.ts index 83754ccb6b4b..ba1268ba31a9 100644 --- a/packages/proxy/lib/http/response-middleware.ts +++ b/packages/proxy/lib/http/response-middleware.ts @@ -54,9 +54,9 @@ function getNodeCharsetFromResponse (headers: IncomingHttpHeaders, body: Buffer, return 'latin1' } -function reqMatchesOriginPolicy (req: CypressIncomingRequest, remoteState) { +function reqMatchesSuperDomainOriginPolicy (req: CypressIncomingRequest, remoteState) { if (remoteState.strategy === 'http') { - return cors.urlMatchesOriginPolicyProps(req.proxiedUrl, remoteState.props) + return cors.urlMatchesSuperDomainOriginPolicyProps(req.proxiedUrl, remoteState.props) } if (remoteState.strategy === 'file') { @@ -236,7 +236,7 @@ const PatchExpressSetHeader: ResponseMiddleware = function () { } const MaybeDelayForCrossOrigin: ResponseMiddleware = function () { - const isCrossOrigin = !reqMatchesOriginPolicy(this.req, this.remoteStates.current()) + const isCrossOrigin = !reqMatchesSuperDomainOriginPolicy(this.req, this.remoteStates.current()) const isPreviousOrigin = this.remoteStates.isInOriginStack(this.req.proxiedUrl) const isHTML = resContentTypeIs(this.incomingRes, 'text/html') const isRenderedHTML = reqWillRenderHtml(this.req) @@ -276,7 +276,7 @@ const SetInjectionLevel: ResponseMiddleware = function () { this.debug('determine injection') - const isReqMatchOriginPolicy = reqMatchesOriginPolicy(this.req, this.remoteStates.current()) + const isReqMatchSuperDomainOriginPolicy = reqMatchesSuperDomainOriginPolicy(this.req, this.remoteStates.current()) const getInjectionLevel = () => { if (this.incomingRes.headers['x-cypress-file-server-error'] && !this.res.isInitial) { this.debug('- partial injection (x-cypress-file-server-error)') @@ -293,7 +293,7 @@ const SetInjectionLevel: ResponseMiddleware = function () { return 'fullCrossOrigin' } - if (!isHTML || (!isReqMatchOriginPolicy && !isAUTFrame)) { + if (!isHTML || (!isReqMatchSuperDomainOriginPolicy && !isAUTFrame)) { this.debug('- no injection (not html)') return false @@ -342,7 +342,7 @@ const SetInjectionLevel: ResponseMiddleware = function () { this.res.wantsInjection === 'full' || this.res.wantsInjection === 'fullCrossOrigin' || // only modify JavasScript if matching the current origin policy or if experimentalModifyObstructiveThirdPartyCode is enabled (above) - (resContentTypeIsJavaScript(this.incomingRes) && isReqMatchOriginPolicy)) + (resContentTypeIsJavaScript(this.incomingRes) && isReqMatchSuperDomainOriginPolicy)) this.debug('injection levels: %o', _.pick(this.res, 'isInitial', 'wantsInjection', 'wantsSecurityRemoved')) diff --git a/packages/runner/injection/cross-origin.js b/packages/runner/injection/cross-origin.js index 9222130408a3..60bd04538093 100644 --- a/packages/runner/injection/cross-origin.js +++ b/packages/runner/injection/cross-origin.js @@ -10,6 +10,8 @@ import { createTimers } from './timers' const findCypress = () => { + let mostSpecificSpecBridgeCypress = undefined + for (let index = 0; index < window.parent.frames.length; index++) { const frame = window.parent.frames[index] @@ -27,7 +29,14 @@ const findCypress = () => { if (window.location.port === frame.location.port && window.location.protocol === frame.location.protocol && frameHostRegex.test(window.location.host)) { - return frame.Cypress + // we found a matching cypress instance. If we have a spec bridge containing a sub domain that is specific to the injection, use that spec bridge + // and overwrite and preexisting Cypress reference if applicable + if (window.location.host === frame.location.host) { + mostSpecificSpecBridgeCypress = frame.Cypress + } else if (!mostSpecificSpecBridgeCypress) { + // otherwise, set the spec bridge Cypress + mostSpecificSpecBridgeCypress = frame.Cypress + } } } } catch (error) { @@ -37,6 +46,8 @@ const findCypress = () => { } } } + + return mostSpecificSpecBridgeCypress } const Cypress = findCypress() diff --git a/packages/server/lib/remote_states.ts b/packages/server/lib/remote_states.ts index 01d63d1c0133..7c3af5671854 100644 --- a/packages/server/lib/remote_states.ts +++ b/packages/server/lib/remote_states.ts @@ -52,7 +52,7 @@ export class RemoteStates { } get (url: string) { - const state = this.remoteStates.get(cors.getOriginPolicy(url)) + const state = this.remoteStates.get(cors.getSuperDomainOriginPolicy(url)) debug('getting remote state: %o for: %s', state, url) @@ -68,16 +68,16 @@ export class RemoteStates { } isInOriginStack (url: string): boolean { - return this.originStack.includes(cors.getOriginPolicy(url)) + return this.originStack.includes(cors.getOriginPolicy(url)) || this.originStack.includes(cors.getSuperDomainOriginPolicy(url)) } isSecondaryOrigin (url: string): boolean { // start at 1 to exclude the primary origin - return this.originStack.indexOf(cors.getOriginPolicy(url), 1) !== -1 + return this.originStack.indexOf(cors.getOriginPolicy(url), 1) !== -1 || this.originStack.indexOf(cors.getSuperDomainOriginPolicy(url), 1) !== -1 } isPrimaryOrigin (url: string): boolean { - return this.originStack[0] === cors.getOriginPolicy(url) + return this.originStack[0] === cors.getOriginPolicy(url) || this.originStack[0] === cors.getSuperDomainOriginPolicy(url) } reset () { @@ -124,7 +124,7 @@ export class RemoteStates { state = urlOrState } - const remoteOriginPolicy = cors.getOriginPolicy(state.origin) + const remoteOriginPolicy = cors.getSuperDomainOriginPolicy(state.origin) if (options.isCrossOrigin) { this.remoteStates.set(remoteOriginPolicy, state)