Skip to content

Commit

Permalink
feat: Selective CSP header stripping from HTTPResponse (#26483)
Browse files Browse the repository at this point in the history
* feat: Selective CSP header directive stripping from HTTPResponse
- uses `stripCspDirectives` config option

* feat: Selective CSP header directive permission from HTTPResponse
- uses `experimentalCspAllowList` config option

* Address Review Comments:
- Add i18n for `experimentalCspAllowList`
- Remove PR link in changelog
- Fix docs link in changelog
- Remove extra typedef additions
- Update validation error message and snapshot
- Fix middleware negated conditional

* chore: refactor driver test into system tests to get better test
coverage on experimentalCspAllowList options

* Address Review Comments:
- Remove legacyOption for `experimentalCspAllowList`
- Update App desc for `experimentalCspAllowList` to include "Content-Security-Policy-Report-Only"
- Modify CHANGELOG wording
- Specify “never” overrideLevel
- Remove unused validator (+2 squashed commits)
- Add "Addresses" note in CHANGELOG to satisfy automation
- Set `canUpdateDuringTestTime` to `false` to prevent confusion

* chore: Add `frame-src` and `child-src` to conditional CSP directives

* chore: Rename `isSubsetOf` to `isArrayIncludingAny`

* chore: fix CLI linting types

* chore: fix server unit tests

* chore: fix system tests within firefox and webkit

* chore: add form-action test

* chore: update system test snapshots

* chore: skip tests in webkit due to form-action flakiness

* chore: Move 'sandbox' and 'navigate-to' into `unsupportedCSPDirectives`
- Add additional system tests
- Update snapshots and unit test

* chore: update system test snapshots

* chore: fix system tests

* chore: do not run csp tests within firefox or webkit due to flake issues in CI

* chore: attempt to increase intercept delay to avoid race condition

* chore: update new snapshots with video defaults work

* chore: update changelog

---------

Co-authored-by: Bill Glesias <bglesias@gmail.com>
Co-authored-by: Matt Schile <mschile@cypress.io>
  • Loading branch information
3 people committed Jun 14, 2023
1 parent c720569 commit 71c5b86
Show file tree
Hide file tree
Showing 38 changed files with 2,421 additions and 15 deletions.
1 change: 1 addition & 0 deletions cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ _Released 06/20/2023 (PENDING)_
**Features:**

- Added support for running Cypress tests with [Chrome's new `--headless=new` flag](https://developer.chrome.com/articles/new-headless/). Chrome versions 112 and above will now be run in the `headless` mode that matches the `headed` browser implementation. Addresses [#25972](https://github.com/cypress-io/cypress/issues/25972).
- Cypress can now test pages with targeted `Content-Security-Policy` and `Content-Security-Policy-Report-Only` header directives by specifying the allow list via the [`experimentalCspAllowList`](https://docs.cypress.io/guides/references/configuration#Experimental-Csp-Allow-List) configuration option. Addresses [#1030](https://github.com/cypress-io/cypress/issues/1030). Addressed in [#26483](https://github.com/cypress-io/cypress/pull/26483)
- The [`videoCompression`](https://docs.cypress.io/guides/references/configuration#Videos) configuration option now accepts both a boolean or a Constant Rate Factor (CRF) number between `1` and `51`. The `videoCompression` default value is still `32` CRF and when `videoCompression` is set to `true` the default of `32` CRF will be used. Addresses [#26658](https://github.com/cypress-io/cypress/issues/26658).

**Bugfixes:**
Expand Down
15 changes: 15 additions & 0 deletions cli/types/cypress.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2672,6 +2672,8 @@ declare namespace Cypress {
force: boolean
}

type experimentalCspAllowedDirectives = 'default-src' | 'child-src' | 'frame-src' | 'script-src' | 'script-src-elem' | 'form-action'

type scrollBehaviorOptions = false | 'center' | 'top' | 'bottom' | 'nearest'

/**
Expand Down Expand Up @@ -3051,6 +3053,19 @@ declare namespace Cypress {
* @default 'top'
*/
scrollBehavior: scrollBehaviorOptions
/**
* Indicates whether Cypress should allow CSP header directives from the application under test.
* - When this option is set to `false`, Cypress will strip the entire CSP header.
* - When this option is set to `true`, Cypress will only to strip directives that would interfere
* with or inhibit Cypress functionality.
* - When this option to an array of allowable directives (`[ 'default-src', ... ]`), the directives
* specified will remain in the response headers.
*
* Please see the documentation for more information.
* @see https://on.cypress.io/configuration#experimentalCspAllowList
* @default false
*/
experimentalCspAllowList: boolean | experimentalCspAllowedDirectives[],
/**
* Allows listening to the `before:run`, `after:run`, `before:spec`, and `after:spec` events in the plugins file during interactive mode.
* @default false
Expand Down
1 change: 1 addition & 0 deletions packages/app/cypress.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export default defineConfig({
reporterOptions: {
configFile: '../../mocha-reporter-config.json',
},
experimentalCspAllowList: false,
experimentalInteractiveRunEvents: true,
component: {
experimentalSingleTabRunMode: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@
"from": "default",
"field": "execTimeout"
},
{
"value": false,
"from": "default",
"field": "experimentalCspAllowList"
},
{
"value": false,
"from": "default",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@
"from": "default",
"field": "execTimeout"
},
{
"value": false,
"from": "default",
"field": "experimentalCspAllowList"
},
{
"value": false,
"from": "default",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,11 @@
"from": "default",
"field": "execTimeout"
},
{
"value": false,
"from": "default",
"field": "experimentalCspAllowList"
},
{
"value": false,
"from": "default",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@
"from": "default",
"field": "execTimeout"
},
{
"value": false,
"from": "default",
"field": "experimentalCspAllowList"
},
{
"value": false,
"from": "default",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@
"from": "default",
"field": "execTimeout"
},
{
"value": false,
"from": "default",
"field": "experimentalCspAllowList"
},
{
"value": false,
"from": "default",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1445,6 +1445,11 @@
"from": "default",
"field": "execTimeout"
},
{
"value": false,
"from": "default",
"field": "experimentalCspAllowList"
},
{
"value": false,
"from": "default",
Expand Down
3 changes: 3 additions & 0 deletions packages/config/__snapshots__/index.spec.ts.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ exports['config/src/index .getDefaultValues returns list of public config keys 1
},
'env': {},
'execTimeout': 60000,
'experimentalCspAllowList': false,
'experimentalFetchPolyfill': false,
'experimentalInteractiveRunEvents': false,
'experimentalRunAllSpecs': false,
Expand Down Expand Up @@ -121,6 +122,7 @@ exports['config/src/index .getDefaultValues returns list of public config keys f
},
'env': {},
'execTimeout': 60000,
'experimentalCspAllowList': false,
'experimentalFetchPolyfill': false,
'experimentalInteractiveRunEvents': false,
'experimentalRunAllSpecs': false,
Expand Down Expand Up @@ -204,6 +206,7 @@ exports['config/src/index .getPublicConfigKeys returns list of public config key
'e2e',
'env',
'execTimeout',
'experimentalCspAllowList',
'experimentalFetchPolyfill',
'experimentalInteractiveRunEvents',
'experimentalRunAllSpecs',
Expand Down
25 changes: 25 additions & 0 deletions packages/config/__snapshots__/validation.spec.ts.js
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,31 @@ exports['config/src/validation .isStringOrFalse returns error message when value
'type': 'a string or false',
}

exports['not an array error message'] = {
'key': 'fakeKey',
'value': 'fakeValue',
'type': 'an array including any of these values: [true, false]',
}

exports['not a subset of error message'] = {
'key': 'fakeKey',
'value': [
null,
],
'type': 'an array including any of these values: ["fakeValue", "fakeValue1", "fakeValue2"]',
}

exports['not all in subset error message'] = {
'key': 'fakeKey',
'value': [
'fakeValue',
'fakeValue1',
'fakeValue2',
'fakeValue3',
],
'type': 'an array including any of these values: ["fakeValue", "fakeValue1", "fakeValue2"]',
}

exports['invalid lower bound'] = {
'key': 'test',
'value': -1,
Expand Down
6 changes: 6 additions & 0 deletions packages/config/src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,12 @@ const driverConfigOptions: Array<DriverConfigOption> = [
defaultValue: 60000,
validation: validate.isNumber,
overrideLevel: 'any',
}, {
name: 'experimentalCspAllowList',
defaultValue: false,
validation: validate.validateAny(validate.isBoolean, validate.isArrayIncludingAny('script-src-elem', 'script-src', 'default-src', 'form-action', 'child-src', 'frame-src')),
overrideLevel: 'never',
requireRestartOnChange: 'server',
}, {
name: 'experimentalFetchPolyfill',
defaultValue: false,
Expand Down
44 changes: 41 additions & 3 deletions packages/config/src/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,29 @@ const _isFullyQualifiedUrl = (value: any): ErrResult | boolean => {
return _.isString(value) && /^https?\:\/\//.test(value)
}

const isArrayOfStrings = (value: any): ErrResult | boolean => {
const isStringArray = (value: any): ErrResult | boolean => {
return _.isArray(value) && _.every(value, _.isString)
}

const isFalse = (value: any): boolean => {
return value === false
}

type ValidationResult = ErrResult | boolean | string;
type ValidationFn = (key: string, value: any) => ValidationResult

export const validateAny = (...validations: ValidationFn[]): ValidationFn => {
return (key: string, value: any): ValidationResult => {
return validations.reduce((result: ValidationResult, validation: ValidationFn) => {
if (result === true) {
return result
}

return validation(key, value)
}, false)
}
}

/**
* Validates a single browser object.
* @returns {string|true} Returns `true` if the object is matching browser object schema. Returns an error message if it does not.
Expand Down Expand Up @@ -148,6 +163,29 @@ export const isOneOf = (...values: any[]): ((key: string, value: any) => ErrResu
}
}

/**
* Checks if given array value for a key includes only members of the provided values.
* @example
```
validate = v.isArrayIncludingAny("foo", "bar", "baz")
validate("example", ["foo"]) // true
validate("example", ["bar", "baz"]) // true
validate("example", ["foo", "else"]) // error message string
validate("example", ["foo", "bar", "baz", "else"]) // error message string
```
*/
export const isArrayIncludingAny = (...values: any[]): ((key: string, value: any) => ErrResult | true) => {
const validValues = values.map((a) => str(a)).join(', ')

return (key, value) => {
if (!Array.isArray(value) || !value.every((v) => values.includes(v))) {
return errMsg(key, value, `an array including any of these values: [${validValues}]`)
}

return true
}
}

/**
* Validates whether the supplied set of cert information is valid
* @returns {string|true} Returns `true` if the information set is valid. Returns an error message if it is not.
Expand Down Expand Up @@ -332,15 +370,15 @@ export function isFullyQualifiedUrl (key: string, value: any): ErrResult | true
}

export function isStringOrArrayOfStrings (key: string, value: any): ErrResult | true {
if (_.isString(value) || isArrayOfStrings(value)) {
if (_.isString(value) || isStringArray(value)) {
return true
}

return errMsg(key, value, 'a string or an array of strings')
}

export function isNullOrArrayOfStrings (key: string, value: any): ErrResult | true {
if (_.isNull(value) || isArrayOfStrings(value)) {
if (_.isNull(value) || isStringArray(value)) {
return true
}

Expand Down
30 changes: 30 additions & 0 deletions packages/config/test/project/utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -859,6 +859,34 @@ describe('config/src/project/utils', () => {
})
})

it('experimentalCspAllowList=false', function () {
return this.defaults('experimentalCspAllowList', false)
})

it('experimentalCspAllowList=true', function () {
return this.defaults('experimentalCspAllowList', true, {
experimentalCspAllowList: true,
})
})

it('experimentalCspAllowList=[]', function () {
return this.defaults('experimentalCspAllowList', [], {
experimentalCspAllowList: [],
})
})

it('experimentalCspAllowList=default-src|script-src', function () {
return this.defaults('experimentalCspAllowList', ['default-src', 'script-src'], {
experimentalCspAllowList: ['default-src', 'script-src'],
})
})

it('experimentalCspAllowList=["default-src","script-src"]', function () {
return this.defaults('experimentalCspAllowList', ['default-src', 'script-src'], {
experimentalCspAllowList: ['default-src', 'script-src'],
})
})

it('resets numTestsKeptInMemory to 0 when runMode', function () {
return mergeDefaults({ projectRoot: '/foo/bar/', supportFile: false }, { isTextTerminal: true }, {}, this.getFilesByGlob)
.then((cfg) => {
Expand Down Expand Up @@ -1053,6 +1081,7 @@ describe('config/src/project/utils', () => {
execTimeout: { value: 60000, from: 'default' },
experimentalModifyObstructiveThirdPartyCode: { value: false, from: 'default' },
experimentalSkipDomainInjection: { value: null, from: 'default' },
experimentalCspAllowList: { value: false, from: 'default' },
experimentalFetchPolyfill: { value: false, from: 'default' },
experimentalInteractiveRunEvents: { value: false, from: 'default' },
experimentalMemoryManagement: { value: false, from: 'default' },
Expand Down Expand Up @@ -1150,6 +1179,7 @@ describe('config/src/project/utils', () => {
execTimeout: { value: 60000, from: 'default' },
experimentalModifyObstructiveThirdPartyCode: { value: false, from: 'default' },
experimentalSkipDomainInjection: { value: null, from: 'default' },
experimentalCspAllowList: { value: false, from: 'default' },
experimentalFetchPolyfill: { value: false, from: 'default' },
experimentalInteractiveRunEvents: { value: false, from: 'default' },
experimentalMemoryManagement: { value: false, from: 'default' },
Expand Down
Loading

5 comments on commit 71c5b86

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 71c5b86 Jun 14, 2023

Choose a reason for hiding this comment

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

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

Learn more about this pre-release build at https://on.cypress.io/advanced-installation#Install-pre-release-version

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/12.15.0/linux-arm64/develop-71c5b864ea84c73b561ffaa15eadb94cb7de6422/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 71c5b86 Jun 14, 2023

Choose a reason for hiding this comment

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

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

Learn more about this pre-release build at https://on.cypress.io/advanced-installation#Install-pre-release-version

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/12.15.0/linux-x64/develop-71c5b864ea84c73b561ffaa15eadb94cb7de6422/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 71c5b86 Jun 14, 2023

Choose a reason for hiding this comment

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

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

Learn more about this pre-release build at https://on.cypress.io/advanced-installation#Install-pre-release-version

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/12.15.0/darwin-x64/develop-71c5b864ea84c73b561ffaa15eadb94cb7de6422/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 71c5b86 Jun 14, 2023

Choose a reason for hiding this comment

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

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

Learn more about this pre-release build at https://on.cypress.io/advanced-installation#Install-pre-release-version

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/12.15.0/win32-x64/develop-71c5b864ea84c73b561ffaa15eadb94cb7de6422/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 71c5b86 Jun 14, 2023

Choose a reason for hiding this comment

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

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

Learn more about this pre-release build at https://on.cypress.io/advanced-installation#Install-pre-release-version

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/12.15.0/darwin-arm64/develop-71c5b864ea84c73b561ffaa15eadb94cb7de6422/cypress.tgz

Please sign in to comment.