feat: add experimentalModifyObstructiveThirdPartyCode flag for regex rewriter (#22568)

This commit is contained in:
Bill Glesias
2022-07-22 10:30:20 -04:00
committed by GitHub
parent f0d3a48679
commit 5ff15046e2
33 changed files with 1122 additions and 96 deletions
+9
View File
@@ -2849,6 +2849,15 @@ declare namespace Cypress {
* @default false
*/
experimentalSessionAndOrigin: boolean
/**
* Whether Cypress will search for and replace obstructive code in third party .js or .html files.
* NOTE: Setting this flag to true removes Subresource Integrity (SRI).
* Please see https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity.
* This option has no impact on experimentalSourceRewriting and is only used with the
* non-experimental source rewriter.
* @see https://on.cypress.io/configuration#experimentalModifyObstructiveThirdPartyCode
*/
experimentalModifyObstructiveThirdPartyCode: boolean
/**
* Generate and save commands directly to your test suite by interacting with your app as an end user would.
* @default false
@@ -37,6 +37,7 @@ exports['config/src/index .getDefaultValues returns list of public config keys 1
"experimentalFetchPolyfill": false,
"experimentalInteractiveRunEvents": false,
"experimentalSessionAndOrigin": false,
"experimentalModifyObstructiveThirdPartyCode": false,
"experimentalSourceRewriting": false,
"fileServerFolder": "",
"fixturesFolder": "cypress/fixtures",
@@ -115,6 +116,7 @@ exports['config/src/index .getDefaultValues returns list of public config keys f
"experimentalFetchPolyfill": false,
"experimentalInteractiveRunEvents": false,
"experimentalSessionAndOrigin": false,
"experimentalModifyObstructiveThirdPartyCode": false,
"experimentalSourceRewriting": false,
"fileServerFolder": "",
"fixturesFolder": "cypress/fixtures",
@@ -190,6 +192,7 @@ exports['config/src/index .getPublicConfigKeys returns list of public config key
"experimentalFetchPolyfill",
"experimentalInteractiveRunEvents",
"experimentalSessionAndOrigin",
"experimentalModifyObstructiveThirdPartyCode",
"experimentalSourceRewriting",
"fileServerFolder",
"fixturesFolder",
+7
View File
@@ -211,6 +211,13 @@ const resolvedOptions: Array<ResolvedConfigOption> = [
validation: validate.isBoolean,
isExperimental: true,
canUpdateDuringTestTime: false,
}, {
name: 'experimentalModifyObstructiveThirdPartyCode',
defaultValue: false,
validation: validate.isBoolean,
isExperimental: true,
canUpdateDuringTestTime: false,
requireRestartOnChange: 'server',
}, {
name: 'experimentalSourceRewriting',
defaultValue: false,
@@ -0,0 +1,154 @@
import CryptoJS from 'crypto-js'
import type { TemplateExecutor } from 'lodash'
// NOTE: in order to run these tests, the following config flags need to be set
// experimentalSessionAndOrigin=true
// experimentalModifyObstructiveThirdPartyCode=true
describe('Integrity Preservation', () => {
// Add common SRI hashes used when setting script/link integrity.
// These are the ones supported by SRI (see https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity#using_subresource_integrity)
// For our tests, we will use CryptoJS to calculate these hashes as they can regenerate the integrity without us having to do it manually every
// single time the file changes. But if needed, this can be generated manually in the console by running simply run:
// cat integrity.js|css | openssl dgst -sha384 -binary | openssl base64 -A
// the outputted hash is appended to the algorithm name, all lowercase with a trailing dash. For example:
// sha256-MGkilwijzWAi/LutxKC+CWhsXXc6t1tXTMqY1zakP8c=
// See https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity on SRI integrity.
const availableDigests = ['SHA256', 'SHA384', 'SHA512']
const integrityJSDigests: {[key: string]: string} = {}
const integrityCSSDigests: {[key: string]: string} = {}
let templateExecutor: TemplateExecutor
before(() => {
// Before running our tests, we need to build out digests to inject into our HTML ejs template
// so we can set the integrity tag appropriately for the digest.
// This requires building digests for the integrity JS file that the regex-rewriter will rewrite.
cy.readFile('cypress/fixtures/integrity.js').then((integrityJS) => {
availableDigests.forEach((algo) => {
const hash = CryptoJS[algo](integrityJS)
const stringifiedBase64 = hash.toString(CryptoJS.enc.Base64)
integrityJSDigests[algo] = stringifiedBase64
})
})
// And building digests for the integrity CSS file that SHOULDN'T be impacted, but important to test against.
cy.readFile('cypress/fixtures/integrity.css').then((integrityCSS) => {
availableDigests.forEach((algo) => {
const hash = CryptoJS[algo](integrityCSS)
const stringifiedBase64 = hash.toString(CryptoJS.enc.Base64)
integrityCSSDigests[algo] = stringifiedBase64
})
})
cy.fixture('scripts-with-integrity').then((integrityTemplate) => {
templateExecutor = Cypress._.template(integrityTemplate, { variable: 'data' })
})
})
describe('<script> tags', () => {
availableDigests.forEach((algo) => {
it(`preserves integrity with static <script> in HTML with ${algo} integrity.`, () => {
cy.then(() => {
const compiledTemplate = templateExecutor({
staticScriptInjection: true,
integrityValue: `${algo.toLowerCase()}-${integrityJSDigests[algo]}`,
})
cy.intercept('http://www.foobar.com:3500/fixtures/scripts-with-integrity.html', compiledTemplate)
})
cy.visit('fixtures/primary-origin.html')
cy.get('[data-cy="integrity-link"]').click()
cy.origin('http://foobar.com:3500', () => {
// The added script, if integrity matches, should execute and
// add a <p> element with 'integrity script loaded' as the text
cy.get('#integrity', {
timeout: 1000,
}).should('contain', 'integrity script loaded')
cy.get('#static-set-integrity-script').should('have.attr', 'cypress-stripped-integrity')
})
})
it(`preserves integrity with dynamically added <script> in HTML with ${algo} integrity.`, () => {
cy.then(() => {
const compiledTemplate = templateExecutor({
dynamicScriptInjection: true,
integrityValue: `${algo.toLowerCase()}-${integrityJSDigests[algo]}`,
})
cy.intercept('http://www.foobar.com:3500/fixtures/scripts-with-integrity.html', compiledTemplate)
})
cy.visit('fixtures/primary-origin.html')
cy.get('[data-cy="integrity-link"]').click()
cy.origin('http://foobar.com:3500', () => {
// The added script, if integrity matches, should execute and
// add a <p> element with 'integrity script loaded' as the text
cy.get('#integrity', {
timeout: 1000,
}).should('contain', 'integrity script loaded')
cy.get('#dynamic-set-integrity-script').should('have.attr', 'cypress-stripped-integrity')
})
})
})
})
describe('<link> tags', () => {
availableDigests.forEach((algo) => {
it(`preserves integrity with static <link> in HTML with ${algo} integrity.`, () => {
cy.then(() => {
const compiledTemplate = templateExecutor({
staticLinkInjection: true,
integrityValue: `${algo.toLowerCase()}-${integrityCSSDigests[algo]}`,
})
cy.intercept('http://www.foobar.com:3500/fixtures/scripts-with-integrity.html', compiledTemplate)
})
cy.visit('fixtures/primary-origin.html')
cy.get('[data-cy="integrity-link"]').click()
cy.origin('http://foobar.com:3500', () => {
cy.get('[data-cy="integrity-header"]', {
timeout: 1000,
}).then((integrityHeader) => {
// The added link, if integrity matches, should execute and
// add a color 'red' to the data-cy="integrity-header" element
expect(window.getComputedStyle(integrityHeader[0]).getPropertyValue('color')).to.equal('rgb(255, 0, 0)')
})
cy.get('#static-set-integrity-link').should('have.attr', 'cypress-stripped-integrity')
})
})
it(`preserves integrity with dynamically added <link> in HTML with ${algo} integrity.`, () => {
cy.then(() => {
const compiledTemplate = templateExecutor({
dynamicLinkInjection: true,
integrityValue: `${algo.toLowerCase()}-${integrityCSSDigests[algo]}`,
})
cy.intercept('http://www.foobar.com:3500/fixtures/scripts-with-integrity.html', compiledTemplate)
})
cy.visit('fixtures/primary-origin.html')
cy.get('[data-cy="integrity-link"]').click()
cy.origin('http://foobar.com:3500', () => {
cy.get('[data-cy="integrity-header"]', {
timeout: 1000,
}).then((integrityHeader) => {
// The added link, if integrity matches, should execute and
// add a color 'red' to the data-cy="integrity-header" element
expect(window.getComputedStyle(integrityHeader[0]).getPropertyValue('color')).to.equal('rgb(255, 0, 0)')
})
cy.get('#dynamic-set-integrity-link').should('have.attr', 'cypress-stripped-integrity')
})
})
})
})
})
@@ -0,0 +1,55 @@
describe('src/cross-origin/patches', () => {
beforeEach(() => {
cy.visit('/fixtures/primary-origin.html')
cy.get('a[data-cy="cross-origin-secondary-link"]').click()
})
context('submit', () => {
it('correctly submits a form when the target is _top for HTMLFormElement', () => {
cy.origin('http://www.foobar.com:3500', () => {
cy.get('form').then(($form) => {
expect($form.attr('target')).to.equal('_top')
$form[0].submit()
})
cy.contains('Some generic content')
})
})
})
context('setAttribute', () => {
it('renames integrity to cypress-stripped-integrity for HTMLScriptElement', () => {
cy.origin('http://www.foobar.com:3500', () => {
cy.window().then((win: Window) => {
const script = win.document.createElement('script')
script.setAttribute('integrity', 'sha-123')
expect(script.getAttribute('integrity')).to.be.null
expect(script.getAttribute('cypress-stripped-integrity')).to.equal('sha-123')
})
})
})
it('renames integrity to cypress-stripped-integrity for HTMLLinkElement', () => {
cy.origin('http://www.foobar.com:3500', () => {
cy.window().then((win: Window) => {
const script = win.document.createElement('link')
script.setAttribute('integrity', 'sha-123')
expect(script.getAttribute('integrity')).to.be.null
expect(script.getAttribute('cypress-stripped-integrity')).to.equal('sha-123')
})
})
})
it('doesn\'t rename integrity for other elements', () => {
cy.origin('http://www.foobar.com:3500', () => {
cy.get('button[data-cy="alert"]').then(($button) => {
$button.attr('integrity', 'sha-123')
expect($button.attr('integrity')).to.equal('sha-123')
expect($button.attr('cypress-stripped-integrity')).to.be.undefined
})
})
})
})
})
@@ -0,0 +1,3 @@
[data-cy="integrity-header"] {
color: rgb(255, 0, 0);
}
@@ -0,0 +1,4 @@
var paragraph = document.createElement('p')
paragraph.id = 'integrity'
paragraph.textContent = 'integrity script loaded'
document.querySelector('body').appendChild(paragraph)
@@ -13,6 +13,7 @@
<li><a data-cy="files-form-link" href="http://www.foobar.com:3500/fixtures/files-form.html">http://www.foobar.com:3500/fixtures/files-form.html</a></li>
<li><a data-cy="errors-link" href="http://www.foobar.com:3500/fixtures/errors.html">http://www.foobar.com:3500/fixtures/errors.html</a></li>
<li><a data-cy="screenshots-link" href="http://www.foobar.com:3500/fixtures/screenshots.html">http://www.foobar.com:3500/fixtures/screenshots.html</a></li>
<li><a data-cy="integrity-link" href="http://www.foobar.com:3500/fixtures/scripts-with-integrity.html">http://www.foobar.com:3500/fixtures/scripts-with-integrity.html</a></li>
<li><a data-cy="cookie-login">Login with Social</a></li>
<li><a data-cy="cookie-login-https">Login with Social (https)</a></li>
<li><a data-cy="cookie-login-subdomain">Login with Social (subdomain)</a></li>
@@ -0,0 +1,53 @@
<!DOCTYPE html>
<!-- NOTE: This is an EJS template used by the origin/integrity.cy.ts to test regex rewriting integrity -->
<!-- using this fixture without compiling and rendering the template will cause errors -->
<html>
<head>
<title>DOM Fixture</title>
</head>
<body>
<h1 data-cy="integrity-header">Integrity Scripts</h1>
</body>
<% if(data && data.staticLinkInjection) { %>
<!-- static link injection -->
<!-- the actual integrity of the file is: <%=data.integrityValue%> -->
<link id="static-set-integrity-link" rel="stylesheet" href="integrity.css" integrity="<%=data.integrityValue%>">
<% } %>
<% if(data && data.staticScriptInjection) { %>
<!-- static script injection -->
<!-- the actual integrity of the file is: <%=data.integrityValue%> -->
<script id="static-set-integrity-script" type="text/javascript" src="integrity.js" data-script-type="static" crossorigin="anonymous" integrity="<%=data.integrityValue%>"></script>
<% } %>
<% if(data && data.dynamicScriptInjection) { %>
<!-- dynamic script injection-->
<script type="text/javascript">
const dynamicIntegrityScript = document.createElement('script')
dynamicIntegrityScript.id = 'dynamic-set-integrity-script'
dynamicIntegrityScript.type = 'text/javascript'
dynamicIntegrityScript.src = 'integrity.js'
dynamicIntegrityScript.setAttribute('crossorigin', "anonymous")
dynamicIntegrityScript.setAttribute('data-script-type', 'dynamic')
// the actual integrity of the file is: <%=data.integrityValue%>
dynamicIntegrityScript.setAttribute('integrity', "<%=data.integrityValue%>")
document.querySelector('head').appendChild(dynamicIntegrityScript)
</script>
<% } %>
<% if(data && data.dynamicLinkInjection) { %>
<!-- dynamic link injection -->
<script id="dynamic-link-injection" type="text/javascript">
const dynamicIntegrityScript = document.createElement('link')
dynamicIntegrityScript.id = 'dynamic-set-integrity-link'
dynamicIntegrityScript.rel = "stylesheet"
dynamicIntegrityScript.href = 'integrity.css'
dynamicIntegrityScript.setAttribute('crossorigin', "anonymous")
// the actual integrity of the file is: <%=data.integrityValue%>
dynamicIntegrityScript.setAttribute('integrity', "<%=data.integrityValue%>")
document.querySelector('head').appendChild(dynamicIntegrityScript)
</script>
<% } %>
</html>
@@ -7,7 +7,7 @@
<p data-cy="cypress-check"></p>
<p data-cy="window-before-load"></p>
<input data-cy="text-input" type="text"/>
<form>
<form action="/fixtures/generic.html" method="get" target="_top">
<button type="submit">Submit</button>
</form>
<button data-cy="alert">Alert</button>
+4 -2
View File
@@ -6,8 +6,8 @@
"clean-deps": "rimraf node_modules",
"cypress:open": "node ../../scripts/cypress open",
"cypress:run": "node ../../scripts/cypress run --spec \"cypress/e2e/*/*\",\"cypress/e2e/*/!(origin|sessions)/**/*\"",
"cypress:open-experimentalSessionAndOrigin": "node ../../scripts/cypress open --config experimentalSessionAndOrigin=true",
"cypress:run-experimentalSessionAndOrigin": "node ../../scripts/cypress run --config experimentalSessionAndOrigin=true",
"cypress:open-experimentalSessionAndOrigin": "node ../../scripts/cypress open --config experimentalSessionAndOrigin=true,experimentalModifyObstructiveThirdPartyCode=true",
"cypress:run-experimentalSessionAndOrigin": "node ../../scripts/cypress run --config experimentalSessionAndOrigin=true,experimentalModifyObstructiveThirdPartyCode=true",
"postinstall": "patch-package",
"start": "node -e 'console.log(require(`chalk`).red(`\nError:\n\tRunning \\`yarn start\\` is no longer needed for driver/cypress tests.\n\tWe now automatically spawn the server in e2e.setupNodeEvents config.\n\tChanges to the server will be watched and reloaded automatically.`))'"
},
@@ -20,6 +20,7 @@
"@cypress/what-is-circular": "1.0.1",
"@packages/config": "0.0.0-development",
"@packages/network": "0.0.0-development",
"@packages/rewriter": "0.0.0-development",
"@packages/runner": "0.0.0-development",
"@packages/runner-shared": "0.0.0-development",
"@packages/server": "0.0.0-development",
@@ -45,6 +46,7 @@
"cookie-parser": "1.4.5",
"core-js-pure": "3.21.0",
"cors": "2.8.5",
"crypto-js": "4.1.1",
"cypress-multi-reporters": "1.4.0",
"dayjs": "^1.10.3",
"debug": "^4.3.2",
@@ -20,6 +20,9 @@ import { handleScreenshots } from './events/screenshots'
import { handleTestEvents } from './events/test'
import { handleMiscEvents } from './events/misc'
import { handleUnsupportedAPIs } from './unsupported_apis'
import { patchDocumentCookie } from './patches/cookies'
import { patchFormElementSubmit } from './patches/submit'
import { patchElementIntegrity } from './patches/setAttribute'
import $Mocha from '../cypress/mocha'
import * as cors from '@packages/network/lib/cors'
@@ -105,6 +108,13 @@ const onBeforeAppWindowLoad = (Cypress: Cypress.Cypress, cy: $Cy) => (autWindow:
Cypress.state('window', autWindow)
Cypress.state('document', autWindow.document)
if (Cypress && Cypress.config('experimentalModifyObstructiveThirdPartyCode')) {
patchFormElementSubmit(autWindow)
patchElementIntegrity(autWindow)
}
patchDocumentCookie(Cypress, autWindow)
// This is typically called by the cy function `urlNavigationEvent` but it is private. For the primary origin this is called in 'onBeforeAppWindowLoad'.
Cypress.action('app:navigation:changed', 'page navigation event (\'before:load\')')
@@ -1,5 +1,3 @@
/* global document */
// document.cookie monkey-patching
// -------------------------------
// We monkey-patch document.cookie when in a cross-origin injection, because
@@ -15,7 +13,7 @@
// - On an interval, get the browser's cookies for the given domain, so that
// updates to the cookie jar (via http requests, cy.setCookie, etc) are
// reflected in the document.cookie value.
export const patchDocumentCookie = (Cypress) => {
export const patchDocumentCookie = (Cypress, window) => {
const setAutomationCookie = (toughCookie) => {
const { superDomain } = Cypress.Location.create(window.location.href)
const automationCookie = Cypress.Cookies.toughCookieToAutomationCookie(toughCookie, superDomain)
@@ -29,7 +27,7 @@ export const patchDocumentCookie = (Cypress) => {
let documentCookieValue = ''
Object.defineProperty(document, 'cookie', {
Object.defineProperty(window.document, 'cookie', {
get () {
return documentCookieValue
},
@@ -0,0 +1,23 @@
import { STRIPPED_INTEGRITY_TAG } from '@packages/rewriter/lib/constants.json'
export const patchElementIntegrity = (window: Window) => {
const originalFormElementSetAttribute = window.HTMLScriptElement.prototype.setAttribute
window.HTMLScriptElement.prototype.setAttribute = function (qualifiedName, value) {
if (qualifiedName === 'integrity') {
qualifiedName = STRIPPED_INTEGRITY_TAG
}
return originalFormElementSetAttribute.apply(this, [qualifiedName, value])
}
const originalAnchorElementSetAttribute = window.HTMLLinkElement.prototype.setAttribute
window.HTMLLinkElement.prototype.setAttribute = function (qualifiedName, value) {
if (qualifiedName === 'integrity') {
qualifiedName = STRIPPED_INTEGRITY_TAG
}
return originalAnchorElementSetAttribute.apply(this, [qualifiedName, value])
}
}
@@ -0,0 +1,10 @@
import { handleInvalidTarget } from '../../cy/top_attr_guards'
export const patchFormElementSubmit = (window: Window) => {
const originalSubmit = window.HTMLFormElement.prototype.submit
window.HTMLFormElement.prototype.submit = function () {
handleInvalidTarget(this)
originalSubmit.apply(this)
}
}
+34 -26
View File
@@ -5,21 +5,44 @@ const invalidTargets = new Set(['_parent', '_top'])
export type GuardedEvent = Event & {target: HTMLFormElement | HTMLAnchorElement}
/**
* Guard against target beting set to something other than blank or self, while trying
* Guard against target being set to something other than blank or self, while trying
* to preserve the appearance of having the correct target value.
*/
export function handleInvalidEventTarget (e: GuardedEvent) {
let targetValue = e.target.target
let targetSet = e.target.hasAttribute('target')
handleInvalidTarget(e.target)
}
if (invalidTargets.has(e.target.target)) {
e.target.target = ''
export type GuardedAnchorEvent = Event & {target: HTMLAnchorElement}
/**
* We need to listen to all click events on the window, but only handle anchor elements,
* as those might be the ones where we have an incorrect "target" attr, or could have one
* dynamically set in subsequent event bubbling.
*
* @param e
*/
export function handleInvalidAnchorTarget (e: GuardedAnchorEvent) {
if (e.target.tagName === 'A') {
handleInvalidTarget(e.target)
}
}
/**
* Guard against target being set to something other than blank or self, while trying
* to preserve the appearance of having the correct target value.
*/
export function handleInvalidTarget (el: HTMLFormElement | HTMLAnchorElement) {
let targetValue = el.target
let targetSet = el.hasAttribute('target')
if (invalidTargets.has(el.target)) {
el.target = ''
}
const { getAttribute, setAttribute, removeAttribute } = e.target
const targetDescriptor = Object.getOwnPropertyDescriptor(e.target, 'target')
const { getAttribute, setAttribute, removeAttribute } = el
const targetDescriptor = Object.getOwnPropertyDescriptor(el, 'target')
e.target.getAttribute = function (k) {
el.getAttribute = function (k) {
if (k === 'target') {
// https://github.com/cypress-io/cypress/issues/17512
// When the target attribute doesn't exist, it should return null.
@@ -34,7 +57,7 @@ export function handleInvalidEventTarget (e: GuardedEvent) {
return getAttribute.call(this, k)
}
e.target.setAttribute = function (k, v) {
el.setAttribute = function (k, v) {
if (k === 'target') {
targetSet = true
targetValue = v
@@ -45,7 +68,7 @@ export function handleInvalidEventTarget (e: GuardedEvent) {
return setAttribute.call(this, k, v)
}
e.target.removeAttribute = function (k) {
el.removeAttribute = function (k) {
if (k === 'target') {
targetSet = false
targetValue = ''
@@ -56,7 +79,7 @@ export function handleInvalidEventTarget (e: GuardedEvent) {
}
if (!targetDescriptor) {
Object.defineProperty(e.target, 'target', {
Object.defineProperty(el, 'target', {
configurable: false,
set (value) {
return targetValue = value
@@ -67,18 +90,3 @@ export function handleInvalidEventTarget (e: GuardedEvent) {
})
}
}
export type GuardedAnchorEvent = Event & {target: HTMLAnchorElement}
/**
* We need to listen to all click events on the window, but only handle anchor elements,
* as those might be the ones where we have an incorrect "target" attr, or could have one
* dynamically set in subsequent event bubbling.
*
* @param e
*/
export function handleInvalidAnchorTarget (e: GuardedAnchorEvent) {
if (e.target.tagName === 'A') {
handleInvalidEventTarget(e)
}
}
@@ -2,6 +2,9 @@ declare global {
interface Window {
Element: typeof Element
HTMLElement: typeof HTMLElement
HTMLFormElement: typeof HTMLFormElement
HTMLLinkElement: typeof HTMLLinkElement
HTMLScriptElement: typeof HTMLScriptElement
HTMLInputElement: typeof HTMLInputElement
HTMLSelectElement: typeof HTMLSelectElement
HTMLButtonElement: typeof HTMLButtonElement
+11 -5
View File
@@ -264,6 +264,7 @@ const MaybeDelayForCrossOrigin: ResponseMiddleware = function () {
const SetInjectionLevel: ResponseMiddleware = function () {
this.res.isInitial = this.req.cookies['__cypress.initial'] === 'true'
const isHTML = resContentTypeIs(this.incomingRes, 'text/html')
const isRenderedHTML = reqWillRenderHtml(this.req)
if (isRenderedHTML) {
@@ -283,7 +284,6 @@ const SetInjectionLevel: ResponseMiddleware = function () {
}
const isSecondaryOrigin = this.remoteStates.isSecondaryOrigin(this.req.proxiedUrl)
const isHTML = resContentTypeIs(this.incomingRes, 'text/html')
const isAUTFrame = this.req.isAUTFrame
if (this.config.experimentalSessionAndOrigin && isSecondaryOrigin && isAUTFrame && (isHTML || isRenderedHTML)) {
@@ -334,10 +334,14 @@ const SetInjectionLevel: ResponseMiddleware = function () {
this.res.setHeader('Origin-Agent-Cluster', '?0')
}
this.res.wantsSecurityRemoved = this.config.modifyObstructiveCode && isReqMatchOriginPolicy && (
(this.res.wantsInjection === 'full')
|| resContentTypeIsJavaScript(this.incomingRes)
)
this.res.wantsSecurityRemoved = (this.config.modifyObstructiveCode || this.config.experimentalModifyObstructiveThirdPartyCode) &&
// if experimentalModifyObstructiveThirdPartyCode is enabled, we want to modify all framebusting code that is html or javascript that passes through the proxy
((this.config.experimentalModifyObstructiveThirdPartyCode
&& (isHTML || isRenderedHTML || resContentTypeIsJavaScript(this.incomingRes))) ||
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))
this.debug('injection levels: %o', _.pick(this.res, 'isInitial', 'wantsInjection', 'wantsSecurityRemoved'))
@@ -554,6 +558,7 @@ const MaybeInjectHtml: ResponseMiddleware = function () {
wantsSecurityRemoved: this.res.wantsSecurityRemoved,
isHtml: isHtml(this.incomingRes),
useAstSourceRewriting: this.config.experimentalSourceRewriting,
modifyObstructiveThirdPartyCode: this.config.experimentalModifyObstructiveThirdPartyCode && !this.remoteStates.isPrimaryOrigin(this.req.proxiedUrl),
url: this.req.proxiedUrl,
deferSourceMapRewrite: this.deferSourceMapRewrite,
})
@@ -582,6 +587,7 @@ const MaybeRemoveSecurity: ResponseMiddleware = function () {
this.incomingResStream = this.incomingResStream.pipe(rewriter.security({
isHtml: isHtml(this.incomingRes),
useAstSourceRewriting: this.config.experimentalSourceRewriting,
modifyObstructiveThirdPartyCode: this.config.experimentalModifyObstructiveThirdPartyCode && !this.remoteStates.isPrimaryOrigin(this.req.proxiedUrl),
url: this.req.proxiedUrl,
deferSourceMapRewrite: this.deferSourceMapRewrite,
})).on('error', this.onError)
+34 -7
View File
@@ -1,32 +1,56 @@
import { STRIPPED_INTEGRITY_TAG } from '@packages/rewriter'
import type { SecurityOpts } from './rewriter'
const pumpify = require('pumpify')
const { replaceStream } = require('./replace_stream')
const utf8Stream = require('utf8-stream')
const topOrParentEqualityBeforeRe = /((?:\bwindow\b|\bself\b)(?:\.|\[['"](?:top|self)['"]\])?\s*[!=]==?\s*(?:(?:window|self)(?:\.|\[['"]))?)(top|parent)(?![\w])/g
const topOrParentEqualityAfterRe = /(top|parent)((?:["']\])?\s*[!=]==?\s*(?:\bwindow\b|\bself\b))/g
// expand the equality checks to also look for patterns similar to e.self === e.top
const topOrParentExpandedEqualityBeforeRe = /((?:\bwindow\b|\bself\b|\b[a-zA-z]\.\b)(?:\.|\[['"](?:top|self)['"]\])?\s*[!=]==?\s*(?:(?:window|self|[a-zA-z])(?:\.|\[['"]))?)(top|parent)(?![\w])/g
const topOrParentExpandedEqualityAfterRe = /(top|parent)((?:["']\])?\s*[!=]==?\s*(?:\bwindow\b|\b(?:[a-zA-z]\.)?self\b))/g
const topOrParentLocationOrFramesRe = /([^\da-zA-Z\(\)])?(\btop\b|\bparent\b)([.])(\blocation\b|\bframes\b)/g
const jiraTopWindowGetterRe = /(!function\s*\((\w{1})\)\s*{\s*return\s*\w{1}\s*(?:={2,})\s*\w{1}\.parent)(\s*}\(\w{1}\))/g
const jiraTopWindowGetterUnMinifiedRe = /(function\s*\w{1,}\s*\((\w{1})\)\s*{\s*return\s*\w{1}\s*(?:={2,})\s*\w{1}\.parent)(\s*;\s*})/g
export function strip (html: string) {
return html
.replace(topOrParentEqualityBeforeRe, '$1self')
.replace(topOrParentEqualityAfterRe, 'self$2')
const integrityTagReplacementRe = new RegExp(`(${STRIPPED_INTEGRITY_TAG}|integrity)(=(?:\"|\')sha(?:256|384|512)-.*?(?:\"|\'))`, 'g')
export function strip (html: string, { modifyObstructiveThirdPartyCode }: Partial<SecurityOpts> = {
modifyObstructiveThirdPartyCode: false,
}) {
let rewrittenHTML = html
.replace(modifyObstructiveThirdPartyCode ? topOrParentExpandedEqualityBeforeRe : topOrParentEqualityBeforeRe, '$1self')
.replace(modifyObstructiveThirdPartyCode ? topOrParentExpandedEqualityAfterRe : topOrParentEqualityAfterRe, 'self$2')
.replace(topOrParentLocationOrFramesRe, '$1self$3$4')
.replace(jiraTopWindowGetterRe, '$1 || $2.parent.__Cypress__$3')
.replace(jiraTopWindowGetterUnMinifiedRe, '$1 || $2.parent.__Cypress__$3')
if (modifyObstructiveThirdPartyCode) {
rewrittenHTML = rewrittenHTML.replace(integrityTagReplacementRe, `${STRIPPED_INTEGRITY_TAG}$2`)
}
return rewrittenHTML
}
export function stripStream () {
export function stripStream ({ modifyObstructiveThirdPartyCode }: Partial<SecurityOpts> = {
modifyObstructiveThirdPartyCode: false,
}) {
return pumpify(
utf8Stream(),
replaceStream(
[
topOrParentEqualityBeforeRe,
topOrParentEqualityAfterRe,
modifyObstructiveThirdPartyCode ? topOrParentExpandedEqualityBeforeRe : topOrParentEqualityBeforeRe,
modifyObstructiveThirdPartyCode ? topOrParentExpandedEqualityAfterRe : topOrParentEqualityAfterRe,
topOrParentLocationOrFramesRe,
jiraTopWindowGetterRe,
jiraTopWindowGetterUnMinifiedRe,
...(modifyObstructiveThirdPartyCode ? [
integrityTagReplacementRe,
] : []),
],
[
'$1self',
@@ -34,6 +58,9 @@ export function stripStream () {
'$1self$3$4',
'$1 || $2.parent.__Cypress__$3',
'$1 || $2.parent.__Cypress__$3',
...(modifyObstructiveThirdPartyCode ? [
`${STRIPPED_INTEGRITY_TAG}$2`,
] : []),
],
),
)
+1
View File
@@ -7,6 +7,7 @@ export type SecurityOpts = {
isHtml?: boolean
url: string
useAstSourceRewriting: boolean
modifyObstructiveThirdPartyCode: boolean
deferSourceMapRewrite: (opts: any) => string
}
+1
View File
@@ -30,6 +30,7 @@
"@cypress/request-promise": "4.2.6",
"@cypress/sinon-chai": "2.9.1",
"@packages/resolve-dist": "0.0.0-development",
"@packages/rewriter": "0.0.0-development",
"@packages/server": "0.0.0-development",
"@types/express": "4.17.2",
"@types/supertest": "2.0.10",
@@ -6,6 +6,8 @@ import sinon from 'sinon'
import { testMiddleware } from './helpers'
import { RemoteStates } from '@packages/server/lib/remote_states'
import EventEmitter from 'events'
import { Readable } from 'stream'
import * as rewriter from '../../../lib/http/util/rewriter'
describe('http/response-middleware', function () {
it('exports the members in the correct order', function () {
@@ -586,6 +588,145 @@ describe('http/response-middleware', function () {
})
})
describe('wantsSecurityRemoved', () => {
it('removes security if full injection is requested', () => {
prepareContext({
res: {
wantsInjection: 'full',
},
config: {
modifyObstructiveCode: true,
},
})
return testMiddleware([SetInjectionLevel], ctx)
.then(() => {
expect(ctx.res.wantsSecurityRemoved).to.be.true
})
})
it('removes security if fullCrossOrigin injection is requested', () => {
prepareContext({
res: {
wantsInjection: 'fullCrossOrigin',
},
config: {
modifyObstructiveCode: true,
},
})
return testMiddleware([SetInjectionLevel], ctx)
.then(() => {
expect(ctx.res.wantsSecurityRemoved).to.be.true
})
})
;['application/javascript', 'application/x-javascript', 'text/javascript'].forEach((javascriptMIME) => {
it(`removes security if the MIME type is ${javascriptMIME} and is on the currently active remote state`, () => {
prepareContext({
incomingRes: {
headers: {
'content-type': `${javascriptMIME}`,
},
},
config: {
modifyObstructiveCode: true,
},
})
return testMiddleware([SetInjectionLevel], ctx)
.then(() => {
expect(ctx.res.wantsSecurityRemoved).to.be.true
})
})
})
it('otherwise, does not try to remove security on other MIME Types', () => {
prepareContext({
incomingRes: {
headers: {
'content-type': 'application/xml',
},
},
config: {
modifyObstructiveCode: true,
},
})
return testMiddleware([SetInjectionLevel], ctx)
.then(() => {
expect(ctx.res.wantsSecurityRemoved).to.be.false
})
})
describe('experimentalModifyObstructiveThirdPartyCode', () => {
it('continues to "modifyObstructiveCode" when "experimentalModifyObstructiveThirdPartyCode" is true, even if "modifyObstructiveCode" is set to false.', () => {
prepareContext({
res: {
wantsInjection: 'full',
},
config: {
modifyObstructiveCode: false,
experimentalModifyObstructiveThirdPartyCode: true,
},
})
return testMiddleware([SetInjectionLevel], ctx)
.then(() => {
expect(ctx.res.wantsSecurityRemoved).to.be.true
})
})
;['text/html', 'application/javascript', 'application/x-javascript', 'text/javascript'].forEach((MIMEType) => {
it(`removes security for ${MIMEType} MIME when "experimentalModifyObstructiveThirdPartyCode" is true, regardless of injection or request origin.`, () => {
prepareContext({
req: {
proxiedUrl: 'http://www.some-third-party-script-or-html.com/',
isAUTFrame: false,
},
incomingRes: {
headers: {
'content-type': `${MIMEType}`,
},
},
config: {
experimentalModifyObstructiveThirdPartyCode: true,
},
})
return testMiddleware([SetInjectionLevel], ctx)
.then(() => {
expect(ctx.res.wantsSecurityRemoved).to.be.true
})
})
})
it(`removes security when the request will render html when "experimentalModifyObstructiveThirdPartyCode" is true, regardless of injection or request origin.`, () => {
prepareContext({
renderedHTMLOrigins: {},
getRenderedHTMLOrigins () {
return this.renderedHTMLOrigins
},
req: {
proxiedUrl: 'http://www.some-third-party-script-or-html.com/',
isAUTFrame: false,
headers: {
'accept': ['text/html', 'application/xhtml+xml'],
},
},
config: {
experimentalModifyObstructiveThirdPartyCode: true,
},
})
return testMiddleware([SetInjectionLevel], ctx)
.then(() => {
expect(ctx.res.wantsSecurityRemoved).to.be.true
})
})
})
})
function prepareContext (props) {
const remoteStates = new RemoteStates(() => {})
const eventEmitter = new EventEmitter()
@@ -849,4 +990,233 @@ describe('http/response-middleware', function () {
}
}
})
describe('MaybeInjectHtml', function () {
const { MaybeInjectHtml } = ResponseMiddleware
let ctx
let htmlStub
beforeEach(() => {
htmlStub = sinon.spy(rewriter, 'html')
})
afterEach(() => {
htmlStub.restore()
})
it('modifyObstructiveThirdPartyCode is true for secondary requests', function () {
prepareContext({
req: {
proxiedUrl: 'http://www.foobar.com:3501/primary-origin.html',
},
})
return testMiddleware([MaybeInjectHtml], ctx)
.then(() => {
expect(htmlStub).to.be.calledOnce
expect(htmlStub).to.be.calledWith('foo', {
'deferSourceMapRewrite': undefined,
'domainName': 'foobar.com',
'isHtml': true,
'modifyObstructiveThirdPartyCode': true,
'url': 'http://www.foobar.com:3501/primary-origin.html',
'useAstSourceRewriting': undefined,
'wantsInjection': 'full',
'wantsSecurityRemoved': true,
})
})
})
it('modifyObstructiveThirdPartyCode is false for primary requests', function () {
prepareContext({})
return testMiddleware([MaybeInjectHtml], ctx)
.then(() => {
expect(htmlStub).to.be.calledOnce
expect(htmlStub).to.be.calledWith('foo', {
'deferSourceMapRewrite': undefined,
'domainName': '127.0.0.1',
'isHtml': true,
'modifyObstructiveThirdPartyCode': false,
'url': 'http://127.0.0.1:3501/primary-origin.html',
'useAstSourceRewriting': undefined,
'wantsInjection': 'full',
'wantsSecurityRemoved': true,
})
})
})
it('modifyObstructiveThirdPartyCode is false when experimental flag is false', function () {
prepareContext({
req: {
proxiedUrl: 'http://www.foobar.com:3501/primary-origin.html',
},
config: {
experimentalModifyObstructiveThirdPartyCode: false,
},
})
return testMiddleware([MaybeInjectHtml], ctx)
.then(() => {
expect(htmlStub).to.be.calledOnce
expect(htmlStub).to.be.calledWith('foo', {
'deferSourceMapRewrite': undefined,
'domainName': 'foobar.com',
'isHtml': true,
'modifyObstructiveThirdPartyCode': false,
'url': 'http://www.foobar.com:3501/primary-origin.html',
'useAstSourceRewriting': undefined,
'wantsInjection': 'full',
'wantsSecurityRemoved': true,
})
})
})
function prepareContext (props) {
const remoteStates = new RemoteStates(() => {})
const stream = Readable.from(['foo'])
// set the primary remote state
remoteStates.set('http://127.0.0.1:3501')
ctx = {
incomingRes: {
headers: {},
...props.incomingRes,
},
res: {
wantsInjection: 'full',
wantsSecurityRemoved: true,
...props.res,
},
req: {
proxiedUrl: 'http://127.0.0.1:3501/primary-origin.html',
...props.req,
},
makeResStreamPlainText () {},
incomingResStream: stream,
config: {
experimentalModifyObstructiveThirdPartyCode: true,
},
remoteStates,
debug: (formatter, ...args) => {
debugVerbose(`%s %s %s ${formatter}`, ctx.req.method, ctx.req.proxiedUrl, ctx.stage, ...args)
},
onError (error) {
throw error
},
..._.omit(props, 'incomingRes', 'res', 'req'),
}
}
})
describe('MaybeRemoveSecurity', function () {
const { MaybeRemoveSecurity } = ResponseMiddleware
let ctx
let securityStub
beforeEach(() => {
securityStub = sinon.spy(rewriter, 'security')
})
afterEach(() => {
securityStub.restore()
})
it('modifyObstructiveThirdPartyCode is true for secondary requests', function () {
prepareContext({
req: {
proxiedUrl: 'http://www.foobar.com:3501/primary-origin.html',
},
})
return testMiddleware([MaybeRemoveSecurity], ctx)
.then(() => {
expect(securityStub).to.be.calledOnce
expect(securityStub).to.be.calledWith({
'deferSourceMapRewrite': undefined,
'isHtml': true,
'modifyObstructiveThirdPartyCode': true,
'url': 'http://www.foobar.com:3501/primary-origin.html',
'useAstSourceRewriting': undefined,
})
})
})
it('modifyObstructiveThirdPartyCode is false for primary requests', function () {
prepareContext({})
return testMiddleware([MaybeRemoveSecurity], ctx)
.then(() => {
expect(securityStub).to.be.calledOnce
expect(securityStub).to.be.calledWith({
'deferSourceMapRewrite': undefined,
'isHtml': true,
'modifyObstructiveThirdPartyCode': false,
'url': 'http://127.0.0.1:3501/primary-origin.html',
'useAstSourceRewriting': undefined,
})
})
})
it('modifyObstructiveThirdPartyCode is false when experimental flag is false', function () {
prepareContext({
req: {
proxiedUrl: 'http://www.foobar.com:3501/primary-origin.html',
},
config: {
experimentalModifyObstructiveThirdPartyCode: false,
},
})
return testMiddleware([MaybeRemoveSecurity], ctx)
.then(() => {
expect(securityStub).to.be.calledOnce
expect(securityStub).to.be.calledWith({
'deferSourceMapRewrite': undefined,
'isHtml': true,
'modifyObstructiveThirdPartyCode': false,
'url': 'http://www.foobar.com:3501/primary-origin.html',
'useAstSourceRewriting': undefined,
})
})
})
function prepareContext (props) {
const remoteStates = new RemoteStates(() => {})
const stream = Readable.from(['foo'])
// set the primary remote state
remoteStates.set('http://127.0.0.1:3501')
ctx = {
incomingRes: {
headers: {},
...props.incomingRes,
},
res: {
wantsInjection: 'full',
wantsSecurityRemoved: true,
...props.res,
},
req: {
proxiedUrl: 'http://127.0.0.1:3501/primary-origin.html',
...props.req,
},
makeResStreamPlainText () {},
incomingResStream: stream,
config: {
experimentalModifyObstructiveThirdPartyCode: true,
},
remoteStates,
debug: (formatter, ...args) => {
debugVerbose(`%s %s %s ${formatter}`, ctx.req.method, ctx.req.proxiedUrl, ctx.stage, ...args)
},
onError (error) {
throw error
},
..._.omit(props, 'incomingRes', 'res', 'req'),
}
}
})
})
@@ -168,12 +168,248 @@ const expected = `\
</html>\
`
const originalWithModifyObstructiveThirdPartyCode = `\
<html>
<body>
top1
settop
settopbox
parent1
grandparent
grandparents
myself
mywindow
selfVar
fooparent
windowFile
topFoo
topFoo.window
topFoo.window != topFoo
parentFoo
parentFoo.window
parentFoo.window != parentFoo
<div style="left: 1500px; top: 0px;"></div>
<div style="left: 1500px; top : 0px;"></div>
<div style="left: 1500px; top : 0px;"></div>
parent()
foo.parent()
top()
foo.top()
foo("parent")
foo("top")
const parent = () => { bar: 'bar', framesStyle: 'foo' }
const loadStop = { locationExists = true }
parent.bar
<script type="text/javascript">
if (top != self) run()
if (top!=self) run()
if (self !== top) run()
if (self!==top) run()
if (self === top) return
if (myself !== top) runs()
if (mywindow !== top) runs()
if (top.location!=self.location&&(top.location.href=self.location.href)) run()
if (top.location != self.location) run()
if (top.location != location) run()
if (self.location != top.location) run()
if (loadStop.locationExists) run()
if (!top.locationExists) run()
if (parent.frames.length > 0) run()
if (parent.framesStyle) run()
if (window != top) run()
if (window.top !== window.self) run()
if (window.top!==window.self) run()
if (window.self != window.top) run()
if (window.top != window.self) run()
if (window["top"] != window["parent"]) run()
if (window['top'] != window['parent']) run()
if (window["top"] != self['parent']) run()
if (parent && parent != window) run()
if (parent && parent != self) run()
if (parent && window != parent) run()
if (parent && self != parent) run()
if (myself != parent) run()
if (parent && parent.frames && parent.frames.length > 0) run()
if ((self.parent && !(self.parent === self)) && (self.parent.frames.length != 0)) run()
if (parent !== null && parent.tag !== 'HostComponent' && parent.tag !== 'HostRoot') { }
if (null !== parent && parent.tag !== 'HostComponent' && parent.tag !== 'HostRoot') { }
if (top===self) return
if (top==self) return
if (loadStop===selfVar) return
if (fooparent===selfVar) return
if (loadStop===windowFile) return
if (fooparent===windowFile) return
if (e.self == e.top) run()
if (a.self===a.top) run()
if (f.top===g.self) run()
if (g.top==g.self) run()
if (e.self != e.top) run()
if (a.self!==a.top) run()
if (f.top!==g.self) run()
if (g.top!=g.self) run()
if (h.foo === h.top) run()
if (i.top === i.foo) run()
</script>
<script type="text/javascript" src="integrity.js" data-script-type="static" crossorigin="anonymous" integrity="sha256-MGkilwijzWAi/LutxKC+CWhsXXc6t1tXTMqY1zakP8c="></script>
<script type="text/javascript" integrity="sha512-8hir+1oK8qTZ/CCayBgHoCqQwzgG+pV925Uu02EW0QHAFQenB03kMWrzdpZWMVKCOy/vhmR2CMGMfDlzrYrViQ==" src="integrity.js" data-script-type="static" crossorigin="anonymous"></script>
<script type="text/javascript" integrity="non-legitimate-integrity-value" src="integrity.js" data-script-type="static" crossorigin="anonymous"></script>
<script type="text/javascript">
const dynamicIntegrityScript = document.createElement('script')
dynamicIntegrityScript.id = 'dynamic-set-integrity'
dynamicIntegrityScript.type = 'text/javascript'
dynamicIntegrityScript.src = 'integrity.js'
dynamicIntegrityScript.setAttribute('crossorigin', "anonymous")
dynamicIntegrityScript.setAttribute('data-script-type', 'dynamic')
dynamicIntegrityScript.setAttribute('integrity', "sha384-XiV6bRRw9OEpsWSumtD1J7rElgTrNQro4MY/O4IYjhH+YGCf1dHaNGZ3A2kzYi/C"
document.querySelector('head').appendChild(dynamicIntegrityScript)
</script>
<link id="static-set-integrity-link" rel="stylesheet" href="integrity.css" integrity="sha256-MGkilwijzWAi/LutxKC+CWhsXXc6t1tXTMqY1zakP8c=">
<link integrity="sha512-8hir+1oK8qTZ/CCayBgHoCqQwzgG+pV925Uu02EW0QHAFQenB03kMWrzdpZWMVKCOy/vhmR2CMGMfDlzrYrViQ==" id="static-set-integrity-link" rel="stylesheet" href="integrity.css">
<script id="dynamic-link-injection" type="text/javascript">
const dynamicIntegrityScript = document.createElement('link')
dynamicIntegrityScript.id = 'dynamic-set-integrity-link'
dynamicIntegrityScript.rel = "stylesheet"
dynamicIntegrityScript.href = 'integrity.css'
dynamicIntegrityScript.setAttribute('crossorigin', "anonymous")
dynamicIntegrityScript.setAttribute('integrity', "sha384-XiV6bRRw9OEpsWSumtD1J7rElgTrNQro4MY/O4IYjhH+YGCf1dHaNGZ3A2kzYi/C")
document.querySelector('head').appendChild(dynamicIntegrityScript)
</script>
</body>
</html>\
`
const expectedWithModifyObstructiveThirdPartyCode = `\
<html>
<body>
top1
settop
settopbox
parent1
grandparent
grandparents
myself
mywindow
selfVar
fooparent
windowFile
topFoo
topFoo.window
topFoo.window != topFoo
parentFoo
parentFoo.window
parentFoo.window != parentFoo
<div style="left: 1500px; top: 0px;"></div>
<div style="left: 1500px; top : 0px;"></div>
<div style="left: 1500px; top : 0px;"></div>
parent()
foo.parent()
top()
foo.top()
foo("parent")
foo("top")
const parent = () => { bar: 'bar', framesStyle: 'foo' }
const loadStop = { locationExists = true }
parent.bar
<script type="text/javascript">
if (self != self) run()
if (self!=self) run()
if (self !== self) run()
if (self!==self) run()
if (self === self) return
if (myself !== top) runs()
if (mywindow !== top) runs()
if (self.location!=self.location&&(self.location.href=self.location.href)) run()
if (self.location != self.location) run()
if (self.location != location) run()
if (self.location != self.location) run()
if (loadStop.locationExists) run()
if (!top.locationExists) run()
if (self.frames.length > 0) run()
if (parent.framesStyle) run()
if (window != self) run()
if (window.self !== window.self) run()
if (window.self!==window.self) run()
if (window.self != window.self) run()
if (window.self != window.self) run()
if (window["self"] != window["self"]) run()
if (window['self'] != window['self']) run()
if (window["self"] != self['self']) run()
if (parent && self != window) run()
if (parent && self != self) run()
if (parent && window != self) run()
if (parent && self != self) run()
if (myself != parent) run()
if (parent && self.frames && self.frames.length > 0) run()
if ((self.parent && !(self.self === self)) && (self.self.frames.length != 0)) run()
if (parent !== null && parent.tag !== 'HostComponent' && parent.tag !== 'HostRoot') { }
if (null !== parent && parent.tag !== 'HostComponent' && parent.tag !== 'HostRoot') { }
if (self===self) return
if (self==self) return
if (loadStop===selfVar) return
if (fooparent===selfVar) return
if (loadStop===windowFile) return
if (fooparent===windowFile) return
if (e.self == e.self) run()
if (a.self===a.self) run()
if (f.self===g.self) run()
if (g.self==g.self) run()
if (e.self != e.self) run()
if (a.self!==a.self) run()
if (f.self!==g.self) run()
if (g.self!=g.self) run()
if (h.foo === h.top) run()
if (i.top === i.foo) run()
</script>
<script type="text/javascript" src="integrity.js" data-script-type="static" crossorigin="anonymous" cypress-stripped-integrity="sha256-MGkilwijzWAi/LutxKC+CWhsXXc6t1tXTMqY1zakP8c="></script>
<script type="text/javascript" cypress-stripped-integrity="sha512-8hir+1oK8qTZ/CCayBgHoCqQwzgG+pV925Uu02EW0QHAFQenB03kMWrzdpZWMVKCOy/vhmR2CMGMfDlzrYrViQ==" src="integrity.js" data-script-type="static" crossorigin="anonymous"></script>
<script type="text/javascript" integrity="non-legitimate-integrity-value" src="integrity.js" data-script-type="static" crossorigin="anonymous"></script>
<script type="text/javascript">
const dynamicIntegrityScript = document.createElement('script')
dynamicIntegrityScript.id = 'dynamic-set-integrity'
dynamicIntegrityScript.type = 'text/javascript'
dynamicIntegrityScript.src = 'integrity.js'
dynamicIntegrityScript.setAttribute('crossorigin', "anonymous")
dynamicIntegrityScript.setAttribute('data-script-type', 'dynamic')
dynamicIntegrityScript.setAttribute('integrity', "sha384-XiV6bRRw9OEpsWSumtD1J7rElgTrNQro4MY/O4IYjhH+YGCf1dHaNGZ3A2kzYi/C"
document.querySelector('head').appendChild(dynamicIntegrityScript)
</script>
<link id="static-set-integrity-link" rel="stylesheet" href="integrity.css" cypress-stripped-integrity="sha256-MGkilwijzWAi/LutxKC+CWhsXXc6t1tXTMqY1zakP8c=">
<link cypress-stripped-integrity="sha512-8hir+1oK8qTZ/CCayBgHoCqQwzgG+pV925Uu02EW0QHAFQenB03kMWrzdpZWMVKCOy/vhmR2CMGMfDlzrYrViQ==" id="static-set-integrity-link" rel="stylesheet" href="integrity.css">
<script id="dynamic-link-injection" type="text/javascript">
const dynamicIntegrityScript = document.createElement('link')
dynamicIntegrityScript.id = 'dynamic-set-integrity-link'
dynamicIntegrityScript.rel = "stylesheet"
dynamicIntegrityScript.href = 'integrity.css'
dynamicIntegrityScript.setAttribute('crossorigin', "anonymous")
dynamicIntegrityScript.setAttribute('integrity', "sha384-XiV6bRRw9OEpsWSumtD1J7rElgTrNQro4MY/O4IYjhH+YGCf1dHaNGZ3A2kzYi/C")
document.querySelector('head').appendChild(dynamicIntegrityScript)
</script>
</body>
</html>\
`
describe('http/util/regex-rewriter', () => {
context('.strip', () => {
it('replaces obstructive code', () => {
expect(regexRewriter.strip(original)).to.eq(expected)
})
it('replaces additional obstructive code with the "modifyObstructiveThirdPartyCode" set', () => {
expect(regexRewriter.strip(originalWithModifyObstructiveThirdPartyCode, {
modifyObstructiveThirdPartyCode: true,
})).to.eq(expectedWithModifyObstructiveThirdPartyCode)
})
it('replaces jira window getter', () => {
const jira = `\
for (; !function (n) {
@@ -298,46 +534,50 @@ while (!isTopMostWindow(parentOf) && satisfiesSameOrigin(parentOf.parent)) {
.value() as unknown as typeof libs
_.each(libs, (url, lib) => {
it(`does not alter code from: '${lib}'`, function () {
this.timeout(10000)
[false, true].forEach((modifyObstructiveThirdPartyCode) => {
it(`does not alter code from: '${lib}', with modifyObstructiveThirdPartyCode set to ${modifyObstructiveThirdPartyCode}`, function () {
this.timeout(10000)
const pathToLib = `/tmp/${lib}`
const pathToLib = `/tmp/${lib}`
const downloadFile = () => {
return rp(url)
.then((resp) => {
return Promise.fromCallback((cb) => {
fs.writeFile(pathToLib, resp, cb)
const downloadFile = () => {
return rp(url)
.then((resp) => {
return Promise.fromCallback((cb) => {
fs.writeFile(pathToLib, resp, cb)
})
.return(resp)
})
.return(resp)
}
return Promise.fromCallback((cb) => {
fs.readFile(pathToLib, 'utf8', cb)
})
}
.catch(downloadFile)
.then((libCode: string) => {
let stripped = regexRewriter.strip(libCode, {
modifyObstructiveThirdPartyCode,
})
// nothing should have changed!
return Promise.fromCallback((cb) => {
fs.readFile(pathToLib, 'utf8', cb)
})
.catch(downloadFile)
.then((libCode: string) => {
let stripped = regexRewriter.strip(libCode)
// nothing should have changed!
// TODO: this is currently failing but we're
// going to accept this for now and make this
// test pass, but need to refactor to using
// inline expressions and change the strategy
// for removing obstructive code
if (lib === 'hugeApp') {
stripped = stripped.replace(
'window.self !== window.self',
'window.self !== window.top',
)
}
// TODO: this is currently failing but we're
// going to accept this for now and make this
// test pass, but need to refactor to using
// inline expressions and change the strategy
// for removing obstructive code
if (lib === 'hugeApp') {
stripped = stripped.replace(
'window.self !== window.self',
'window.self !== window.top',
)
}
try {
expect(stripped).to.eq(libCode)
} catch (err) {
throw new Error(`code from '${lib}' was different`)
}
try {
expect(stripped).to.eq(libCode)
} catch (err) {
throw new Error(`code from '${lib}' was different`)
}
})
})
})
})
@@ -368,5 +608,31 @@ while (!isTopMostWindow(parentOf) && satisfiesSameOrigin(parentOf.parent)) {
replacer.end()
})
it('replaces additional obstructive code with the "modifyObstructiveThirdPartyCode" set', (done) => {
const haystacks = originalWithModifyObstructiveThirdPartyCode.split('\n')
const replacer = regexRewriter.stripStream({
modifyObstructiveThirdPartyCode: true,
})
replacer.pipe(concatStream({ encoding: 'string' }, (str) => {
const string = str.toString().trim()
try {
expect(string).to.eq(expectedWithModifyObstructiveThirdPartyCode)
done()
} catch (err) {
done(err)
}
}))
haystacks.forEach((haystack) => {
replacer.write(`${haystack}\n`)
})
replacer.end()
})
})
})
@@ -1,17 +1,17 @@
exports['html rewriter .rewriteHtmlJs strips SRI 1'] = `
<script type="text/javascript" cypress:stripped-integrity="foo" src="bar">
<script type="text/javascript" cypress-stripped-integrity="foo" src="bar">
`
exports['html rewriter .rewriteHtmlJs strips SRI 2'] = `
<script type="text/javascript" cypress:stripped-integrity="foo" src="bar"/>
<script type="text/javascript" cypress-stripped-integrity="foo" src="bar"/>
`
exports['html rewriter .rewriteHtmlJs strips SRI 3'] = `
<script type="text/javascript" cypress:stripped-integrity="foo" src="bar">foo</script>
<script type="text/javascript" cypress-stripped-integrity="foo" src="bar">foo</script>
`
exports['html rewriter .rewriteHtmlJs strips SRI 4'] = `
<script foo:bar="baz" cypress:stripped-integrity="foo" src="bar">
<script foo:bar="baz" cypress-stripped-integrity="foo" src="bar">
`
exports['html rewriter .rewriteHtmlJs rewrites a real-ish document with sourcemaps for inline js 1'] = {
+3
View File
@@ -0,0 +1,3 @@
{
"STRIPPED_INTEGRITY_TAG": "cypress-stripped-integrity"
}
+2 -1
View File
@@ -1,5 +1,6 @@
import find from 'lodash/find'
import type RewritingStream from 'parse5-html-rewriting-stream'
import { STRIPPED_INTEGRITY_TAG } from './constants.json'
import * as js from './js'
export function install (url: string, rewriter: RewritingStream, deferSourceMapRewrite?: js.DeferSourceMapRewriteFn) {
@@ -29,7 +30,7 @@ export function install (url: string, rewriter: RewritingStream, deferSourceMapR
const sriAttr = find(startTag.attrs, { name: 'integrity' })
if (sriAttr) {
sriAttr.name = 'cypress:stripped-integrity'
sriAttr.name = STRIPPED_INTEGRITY_TAG
}
return rewriter.emitStartTag(startTag)
+2
View File
@@ -5,3 +5,5 @@ export { rewriteJsAsync, rewriteHtmlJsAsync } from './async-rewriters'
export { DeferredSourceMapCache } from './deferred-source-map-cache'
export { createInitialWorkers, terminateAllWorkers } from './threads'
export { STRIPPED_INTEGRITY_TAG } from './constants.json'
@@ -8,7 +8,6 @@
*/
import { createTimers } from './timers'
import { patchDocumentCookie } from './cookies'
const findCypress = () => {
for (let index = 0; index < window.parent.frames.length; index++) {
@@ -41,8 +40,6 @@ const findCypress = () => {
const Cypress = findCypress()
patchDocumentCookie(Cypress)
// the timers are wrapped in the injection code similar to the primary origin
const timers = createTimers()
+1
View File
@@ -21,6 +21,7 @@
"@packages/icons": "0.0.0-development",
"@packages/network": "0.0.0-development",
"@packages/reporter": "0.0.0-development",
"@packages/rewriter": "0.0.0-development",
"@packages/socket": "0.0.0-development",
"@packages/web-config": "0.0.0-development",
"babel-plugin-prismjs": "1.0.2",
+1
View File
@@ -21,6 +21,7 @@ export namespace CyServer {
clientRoute: string
experimentalSourceRewriting: boolean
modifyObstructiveCode: boolean
experimentalModifyObstructiveThirdPartyCode: boolean
/**
* URL to Cypress's runner.
*/
+2
View File
@@ -1485,6 +1485,7 @@ describe('lib/config', () => {
downloadsFolder: { value: 'cypress/downloads', from: 'default' },
env: {},
execTimeout: { value: 60000, from: 'default' },
experimentalModifyObstructiveThirdPartyCode: { value: false, from: 'default' },
experimentalFetchPolyfill: { value: false, from: 'default' },
experimentalInteractiveRunEvents: { value: false, from: 'default' },
experimentalSessionAndOrigin: { value: false, from: 'default' },
@@ -1570,6 +1571,7 @@ describe('lib/config', () => {
defaultCommandTimeout: { value: 4000, from: 'default' },
downloadsFolder: { value: 'cypress/downloads', from: 'default' },
execTimeout: { value: 60000, from: 'default' },
experimentalModifyObstructiveThirdPartyCode: { value: false, from: 'default' },
experimentalFetchPolyfill: { value: false, from: 'default' },
experimentalInteractiveRunEvents: { value: false, from: 'default' },
experimentalSessionAndOrigin: { value: false, from: 'default' },
+2 -1
View File
@@ -54,6 +54,7 @@
/* Experimental Options */
"experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
"importsNotUsedAsValues": "error"
"importsNotUsedAsValues": "error",
"resolveJsonModule": true
}
}
+12 -8
View File
@@ -14714,6 +14714,11 @@ crypto-browserify@^3.0.0, crypto-browserify@^3.11.0:
randombytes "^2.0.0"
randomfill "^1.0.3"
crypto-js@4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.1.1.tgz#9e485bcf03521041bd85844786b83fb7619736cf"
integrity sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==
crypto-random-string@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5"
@@ -15292,7 +15297,7 @@ dayjs@1.10.4, dayjs@^1.10.3, dayjs@^1.10.4, dayjs@^1.9.3:
de-indent@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d"
integrity sha1-sgOOhG3DO6pXlhKNCAS0VbjB4h0=
integrity sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==
debounce@^1.2.0:
version "1.2.1"
@@ -26960,9 +26965,9 @@ node-abi@^2.7.0:
semver "^5.4.1"
node-abi@^3.3.0:
version "3.15.0"
resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.15.0.tgz#cd9ac8c58328129b49998cc6fa16aa5506152716"
integrity sha512-Ic6z/j6I9RLm4ov7npo1I48UQr2BEyFCqh6p7S1dhEx9jPO0GPGq/e2Rb7x7DroQrmiVMz/Bw1vJm9sPAl2nxA==
version "3.22.0"
resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.22.0.tgz#00b8250e86a0816576258227edbce7bbe0039362"
integrity sha512-u4uAs/4Zzmp/jjsD9cyFYDXeISfUWaAVWshPmDZOFOv4Xl4SbzTXm53I04C2uRueYJ+0t5PEtLH/owbn2Npf/w==
dependencies:
semver "^7.3.5"
@@ -30367,9 +30372,9 @@ prebuild-install@^5.2.4, prebuild-install@^5.3.5:
which-pm-runs "^1.0.0"
prebuild-install@^7.0.0:
version "7.1.0"
resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.0.tgz#991b6ac16c81591ba40a6d5de93fb33673ac1370"
integrity sha512-CNcMgI1xBypOyGqjp3wOc8AAo1nMhZS3Cwd3iHIxOdAUbb+YxdNuM4Z5iIrZ8RLvOsf3F3bl7b7xGq6DjQoNYA==
version "7.1.1"
resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.1.tgz#de97d5b34a70a0c81334fd24641f2a1702352e45"
integrity sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==
dependencies:
detect-libc "^2.0.0"
expand-template "^2.0.3"
@@ -30378,7 +30383,6 @@ prebuild-install@^7.0.0:
mkdirp-classic "^0.5.3"
napi-build-utils "^1.0.1"
node-abi "^3.3.0"
npmlog "^4.0.1"
pump "^3.0.0"
rc "^1.2.7"
simple-get "^4.0.0"