mirror of
https://github.com/cypress-io/cypress.git
synced 2026-05-03 21:40:28 -05:00
feat: add experimentalModifyObstructiveThirdPartyCode flag for regex rewriter (#22568)
This commit is contained in:
Vendored
+9
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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\')')
|
||||
|
||||
|
||||
+2
-4
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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`,
|
||||
] : []),
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
@@ -7,6 +7,7 @@ export type SecurityOpts = {
|
||||
isHtml?: boolean
|
||||
url: string
|
||||
useAstSourceRewriting: boolean
|
||||
modifyObstructiveThirdPartyCode: boolean
|
||||
deferSourceMapRewrite: (opts: any) => string
|
||||
}
|
||||
|
||||
|
||||
@@ -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'] = {
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"STRIPPED_INTEGRITY_TAG": "cypress-stripped-integrity"
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
Vendored
+1
@@ -21,6 +21,7 @@ export namespace CyServer {
|
||||
clientRoute: string
|
||||
experimentalSourceRewriting: boolean
|
||||
modifyObstructiveCode: boolean
|
||||
experimentalModifyObstructiveThirdPartyCode: boolean
|
||||
/**
|
||||
* URL to Cypress's runner.
|
||||
*/
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user