csp_evaluator
Advanced tools
Comparing version 1.0.7 to 1.1.0
@@ -21,442 +21,449 @@ /** | ||
import {Directive, Version} from '../csp'; | ||
import {Finding, Severity} from '../finding'; | ||
import {CspParser} from '../parser'; | ||
import {CheckerFunction} from './checker'; | ||
import * as securityChecks from './security_checks'; | ||
/** | ||
* Helper function for running a check on a CSP string. | ||
* | ||
* @param test CSP string. | ||
* @param checkFunction check. | ||
*/ | ||
function checkCsp(test: string, checkFunction: CheckerFunction): Finding[] { | ||
const parsedCsp = (new CspParser(test)).csp; | ||
return checkFunction(parsedCsp); | ||
} | ||
describe('Test security checks', () => { | ||
/** Tests for csp.securityChecks.checkScriptUnsafeInline */ | ||
it('CheckScriptUnsafeInlineInScriptSrc', () => { | ||
const test = 'default-src https:; script-src \'unsafe-inline\''; | ||
const violations = checkCsp(test, securityChecks.checkScriptUnsafeInline); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].severity).toBe(Severity.HIGH); | ||
}); | ||
it('CheckScriptUnsafeInlineInDefaultSrc', () => { | ||
const test = 'default-src \'unsafe-inline\''; | ||
const violations = checkCsp(test, securityChecks.checkScriptUnsafeInline); | ||
expect(violations.length).toBe(1); | ||
}); | ||
it('CheckScriptUnsafeInlineInDefaultSrcAndNotInScriptSrc', () => { | ||
const test = 'default-src \'unsafe-inline\'; script-src https:'; | ||
const violations = checkCsp(test, securityChecks.checkScriptUnsafeInline); | ||
expect(violations.length).toBe(0); | ||
}); | ||
it('CheckScriptUnsafeInlineWithNonce', () => { | ||
const test = 'script-src \'unsafe-inline\' \'nonce-foobar\''; | ||
const parsedCsp = (new CspParser(test)).csp; | ||
let effectiveCsp = parsedCsp.getEffectiveCsp(Version.CSP1); | ||
let violations = securityChecks.checkScriptUnsafeInline(effectiveCsp); | ||
expect(violations.length).toBe(1); | ||
effectiveCsp = parsedCsp.getEffectiveCsp(Version.CSP3); | ||
violations = securityChecks.checkScriptUnsafeInline(effectiveCsp); | ||
expect(violations.length).toBe(0); | ||
}); | ||
/** Tests for csp.securityChecks.checkScriptUnsafeEval */ | ||
it('CheckScriptUnsafeEvalInScriptSrc', () => { | ||
const test = 'default-src https:; script-src \'unsafe-eval\''; | ||
const violations = checkCsp(test, securityChecks.checkScriptUnsafeEval); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].severity).toBe(Severity.MEDIUM_MAYBE); | ||
}); | ||
it('CheckScriptUnsafeEvalInDefaultSrc', () => { | ||
const test = 'default-src \'unsafe-eval\''; | ||
const violations = checkCsp(test, securityChecks.checkScriptUnsafeEval); | ||
expect(violations.length).toBe(1); | ||
}); | ||
/** Tests for csp.securityChecks.checkPlainUrlSchemes */ | ||
it('CheckPlainUrlSchemesInScriptSrc', () => { | ||
const test = 'script-src data: http: https: sthInvalid:'; | ||
const violations = checkCsp(test, securityChecks.checkPlainUrlSchemes); | ||
expect(violations.length).toBe(3); | ||
expect(violations.every((v) => v.severity === Severity.HIGH)).toBeTrue(); | ||
}); | ||
it('CheckPlainUrlSchemesInObjectSrc', () => { | ||
const test = 'object-src data: http: https: sthInvalid:'; | ||
const violations = checkCsp(test, securityChecks.checkPlainUrlSchemes); | ||
expect(violations.length).toBe(3); | ||
expect(violations.every((v) => v.severity === Severity.HIGH)).toBeTrue(); | ||
}); | ||
it('CheckPlainUrlSchemesInBaseUri', () => { | ||
const test = 'base-uri data: http: https: sthInvalid:'; | ||
const violations = checkCsp(test, securityChecks.checkPlainUrlSchemes); | ||
expect(violations.length).toBe(3); | ||
expect(violations.every((v) => v.severity === Severity.HIGH)).toBeTrue(); | ||
}); | ||
it('CheckPlainUrlSchemesMixed', () => { | ||
const test = 'default-src https:; object-src data: sthInvalid:'; | ||
const violations = checkCsp(test, securityChecks.checkPlainUrlSchemes); | ||
expect(violations.length).toBe(2); | ||
expect(violations.every((v) => v.severity === Severity.HIGH)).toBeTrue(); | ||
expect(violations[0].directive).toBe(Directive.DEFAULT_SRC); | ||
expect(violations[1].directive).toBe(Directive.OBJECT_SRC); | ||
}); | ||
it('CheckPlainUrlSchemesDangerousDirectivesOK', () => { | ||
const test = | ||
'default-src https:; object-src \'none\'; script-src \'none\'; ' + | ||
'base-uri \'none\''; | ||
const violations = checkCsp(test, securityChecks.checkPlainUrlSchemes); | ||
expect(violations.length).toBe(0); | ||
}); | ||
/** Tests for csp.securityChecks.checkWildcards */ | ||
it('CheckWildcardsInScriptSrc', () => { | ||
const test = 'script-src * http://* //*'; | ||
const violations = checkCsp(test, securityChecks.checkWildcards); | ||
expect(violations.length).toBe(3); | ||
expect(violations.every((v) => v.severity === Severity.HIGH)).toBeTrue(); | ||
}); | ||
it('CheckWildcardsInObjectSrc', () => { | ||
const test = 'object-src * http://* //*'; | ||
const violations = checkCsp(test, securityChecks.checkWildcards); | ||
expect(violations.length).toBe(3); | ||
expect(violations.every((v) => v.severity === Severity.HIGH)).toBeTrue(); | ||
}); | ||
it('CheckWildcardsInBaseUri', () => { | ||
const test = 'base-uri * http://* //*'; | ||
const violations = checkCsp(test, securityChecks.checkWildcards); | ||
expect(violations.length).toBe(3); | ||
expect(violations.every((v) => v.severity === Severity.HIGH)).toBeTrue(); | ||
}); | ||
it('CheckWildcardsSchemesMixed', () => { | ||
const test = 'default-src *; object-src * ignore.me.com'; | ||
const violations = checkCsp(test, securityChecks.checkWildcards); | ||
expect(violations.length).toBe(2); | ||
expect(violations.every((v) => v.severity === Severity.HIGH)).toBeTrue(); | ||
expect(violations[0].directive).toBe(Directive.DEFAULT_SRC); | ||
expect(violations[1].directive).toBe(Directive.OBJECT_SRC); | ||
}); | ||
it('CheckWildcardsDangerousDirectivesOK', () => { | ||
const test = 'default-src *; object-src *.foo.bar; script-src \'none\'; ' + | ||
'base-uri \'none\''; | ||
const violations = checkCsp(test, securityChecks.checkWildcards); | ||
expect(violations.length).toBe(0); | ||
}); | ||
/** Tests for csp.securityChecks.checkMissingDirectives */ | ||
it('CheckMissingDirectivesMissingObjectSrc', () => { | ||
const test = 'script-src \'none\''; | ||
const violations = checkCsp(test, securityChecks.checkMissingDirectives); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].severity).toBe(Severity.HIGH); | ||
}); | ||
it('CheckMissingDirectivesMissingScriptSrc', () => { | ||
const test = 'object-src \'none\''; | ||
const violations = checkCsp(test, securityChecks.checkMissingDirectives); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].severity).toBe(Severity.HIGH); | ||
}); | ||
it('CheckMissingDirectivesMissingBaseUriInNonceCsp', () => { | ||
const test = 'script-src \'nonce-123\'; object-src \'none\''; | ||
const violations = checkCsp(test, securityChecks.checkMissingDirectives); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].severity).toBe(Severity.HIGH); | ||
}); | ||
it('CheckMissingDirectivesMissingBaseUriInHashWStrictDynamicCsp', () => { | ||
const test = | ||
'script-src \'sha256-123456\' \'strict-dynamic\'; object-src \'none\''; | ||
const violations = checkCsp(test, securityChecks.checkMissingDirectives); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].severity).toBe(Severity.HIGH); | ||
}); | ||
it('CheckMissingDirectivesMissingBaseUriInHashCsp', () => { | ||
const test = 'script-src \'sha256-123456\'; object-src \'none\''; | ||
const violations = checkCsp(test, securityChecks.checkMissingDirectives); | ||
expect(violations.length).toBe(0); | ||
}); | ||
it('CheckMissingDirectivesScriptAndObjectSrcSet', () => { | ||
const test = 'script-src \'none\'; object-src \'none\''; | ||
const violations = checkCsp(test, securityChecks.checkMissingDirectives); | ||
expect(violations.length).toBe(0); | ||
}); | ||
it('CheckMissingDirectivesDefaultSrcSet', () => { | ||
const test = 'default-src https:;'; | ||
const violations = checkCsp(test, securityChecks.checkMissingDirectives); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].severity).toBe(Severity.HIGH); | ||
}); | ||
it('CheckMissingDirectivesDefaultSrcSetToNone', () => { | ||
const test = 'default-src \'none\';'; | ||
const violations = checkCsp(test, securityChecks.checkMissingDirectives); | ||
expect(violations.length).toBe(0); | ||
}); | ||
/** Tests for csp.securityChecks.checkScriptAllowlistBypass */ | ||
it('checkScriptAllowlistBypassJSONPBypass', () => { | ||
const test = 'script-src *.google.com'; | ||
const violations = | ||
checkCsp(test, securityChecks.checkScriptAllowlistBypass); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].severity).toBe(Severity.HIGH); | ||
expect(violations[0].description.includes( | ||
'www.google.com is known to host JSONP endpoints which')) | ||
.toBeTrue(); | ||
}); | ||
it('checkScriptAllowlistBypassWithNoneAndJSONPBypass', () => { | ||
const test = 'script-src *.google.com \'none\''; | ||
const violations = | ||
checkCsp(test, securityChecks.checkScriptAllowlistBypass); | ||
expect(violations.length).toBe(0); | ||
}); | ||
it('checkScriptAllowlistBypassJSONPBypassEvalRequired', () => { | ||
const test = 'script-src https://googletagmanager.com \'unsafe-eval\''; | ||
const violations = | ||
checkCsp(test, securityChecks.checkScriptAllowlistBypass); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].severity).toBe(Severity.HIGH); | ||
}); | ||
it('checkScriptAllowlistBypassJSONPBypassEvalRequiredNotPresent', () => { | ||
const test = 'script-src https://googletagmanager.com'; | ||
const violations = | ||
checkCsp(test, securityChecks.checkScriptAllowlistBypass); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].severity).toBe(Severity.MEDIUM_MAYBE); | ||
}); | ||
it('checkScriptAllowlistBypassAngularBypass', () => { | ||
const test = 'script-src gstatic.com'; | ||
const violations = | ||
checkCsp(test, securityChecks.checkScriptAllowlistBypass); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].severity).toBe(Severity.HIGH); | ||
expect(violations[0].description.includes( | ||
'gstatic.com is known to host Angular libraries which')) | ||
.toBeTrue(); | ||
}); | ||
it('checkScriptAllowlistBypassNoBypassWarningOnly', () => { | ||
const test = 'script-src foo.bar'; | ||
const violations = | ||
checkCsp(test, securityChecks.checkScriptAllowlistBypass); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].severity).toBe(Severity.MEDIUM_MAYBE); | ||
}); | ||
it('checkScriptAllowlistBypassNoBypassSelfWarningOnly', () => { | ||
const test = 'script-src \'self\''; | ||
const violations = | ||
checkCsp(test, securityChecks.checkScriptAllowlistBypass); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].severity).toBe(Severity.MEDIUM_MAYBE); | ||
}); | ||
/** Tests for csp.securityChecks.checkFlashObjectAllowlistBypass */ | ||
it('checkFlashObjectAllowlistBypassFlashBypass', () => { | ||
const test = 'object-src https://*.googleapis.com'; | ||
const violations = | ||
checkCsp(test, securityChecks.checkFlashObjectAllowlistBypass); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].severity).toBe(Severity.HIGH); | ||
}); | ||
it('checkFlashObjectAllowlistBypassNoFlashBypass', () => { | ||
const test = 'object-src https://foo.bar'; | ||
const violations = | ||
checkCsp(test, securityChecks.checkFlashObjectAllowlistBypass); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].severity).toBe(Severity.MEDIUM_MAYBE); | ||
}); | ||
it('checkFlashObjectAllowlistBypassSelfAllowed', () => { | ||
const test = 'object-src \'self\''; | ||
const violations = | ||
checkCsp(test, securityChecks.checkFlashObjectAllowlistBypass); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].severity).toBe(Severity.MEDIUM_MAYBE); | ||
expect(violations[0].description) | ||
.toBe('Can you restrict object-src to \'none\' only?'); | ||
}); | ||
/** Tests for csp.securityChecks.checkIpSource */ | ||
it('CheckIpSource', () => { | ||
const test = | ||
'script-src 8.8.8.8; font-src //127.0.0.1 https://[::1] not.an.ip'; | ||
const violations = checkCsp(test, securityChecks.checkIpSource); | ||
expect(violations.length).toBe(3); | ||
expect(violations.every((v) => v.severity === Severity.INFO)).toBeTrue(); | ||
}); | ||
it('LooksLikeIpAddressIPv4', () => { | ||
expect(securityChecks.looksLikeIpAddress('8.8.8.8')).toBeTrue(); | ||
}); | ||
it('LooksLikeIpAddressIPv6', () => { | ||
expect(securityChecks.looksLikeIpAddress('[::1]')).toBeTrue(); | ||
}); | ||
it('CheckDeprecatedDirectiveReportUriWithReportTo', () => { | ||
const test = 'report-uri foo.bar/csp;report-to abc'; | ||
const violations = checkCsp(test, securityChecks.checkDeprecatedDirective); | ||
expect(violations.length).toBe(0); | ||
}); | ||
it('CheckDeprecatedDirectiveWithoutReportUriButWithReportTo', () => { | ||
const test = 'report-to abc'; | ||
const violations = checkCsp(test, securityChecks.checkDeprecatedDirective); | ||
expect(violations.length).toBe(0); | ||
}); | ||
it('CheckDeprecatedDirectiveReflectedXss', () => { | ||
const test = 'reflected-xss block'; | ||
const violations = checkCsp(test, securityChecks.checkDeprecatedDirective); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].severity).toBe(Severity.INFO); | ||
}); | ||
it('CheckDeprecatedDirectiveReferrer', () => { | ||
const test = 'referrer origin'; | ||
const violations = checkCsp(test, securityChecks.checkDeprecatedDirective); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].severity).toBe(Severity.INFO); | ||
}); | ||
/** Tests for csp.securityChecks.checkNonceLength */ | ||
it('CheckNonceLengthWithLongNonce', () => { | ||
const test = 'script-src \'nonce-veryLongRandomNonce\''; | ||
const violations = checkCsp(test, securityChecks.checkNonceLength); | ||
expect(violations.length).toBe(0); | ||
}); | ||
it('CheckNonceLengthWithShortNonce', () => { | ||
const test = 'script-src \'nonce-short\''; | ||
const violations = checkCsp(test, securityChecks.checkNonceLength); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].severity).toBe(Severity.MEDIUM); | ||
}); | ||
it('CheckNonceLengthInvalidCharset', () => { | ||
const test = 'script-src \'nonce-***notBase64***\''; | ||
const violations = checkCsp(test, securityChecks.checkNonceLength); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].severity).toBe(Severity.INFO); | ||
}); | ||
/** Tests for csp.securityChecks.checkSrcHttp */ | ||
it('CheckSrcHttp', () => { | ||
const test = | ||
'script-src http://foo.bar https://test.com; report-uri http://test.com'; | ||
const violations = checkCsp(test, securityChecks.checkSrcHttp); | ||
expect(violations.length).toBe(2); | ||
expect(violations.every((v) => v.severity === Severity.MEDIUM)).toBeTrue(); | ||
}); | ||
/** Tests for csp.securityChecks.checkHasConfiguredReporting */ | ||
it('CheckHasConfiguredReporting_whenNoReporting', () => { | ||
const test = 'script-src \'nonce-aaaaaaaaaa\''; | ||
const violations = | ||
checkCsp(test, securityChecks.checkHasConfiguredReporting); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].severity).toBe(Severity.INFO); | ||
expect(violations[0].directive).toBe('report-uri'); | ||
}); | ||
it('CheckHasConfiguredReporting_whenOnlyReportTo', () => { | ||
const test = 'script-src \'nonce-aaaaaaaaaa\'; report-to name'; | ||
const violations = | ||
checkCsp(test, securityChecks.checkHasConfiguredReporting); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].severity).toBe(Severity.INFO); | ||
expect(violations[0].directive).toBe('report-to'); | ||
}); | ||
it('CheckHasConfiguredReporting_whenOnlyReportUri', () => { | ||
const test = 'script-src \'nonce-aaaaaaaaaa\'; report-uri url'; | ||
const violations = | ||
checkCsp(test, securityChecks.checkHasConfiguredReporting); | ||
expect(violations.length).toBe(0); | ||
}); | ||
it('CheckHasConfiguredReporting_whenReportUriAndReportTo', () => { | ||
const test = | ||
'script-src \'nonce-aaaaaaaaaa\'; report-uri url; report-to name'; | ||
const violations = | ||
checkCsp(test, securityChecks.checkHasConfiguredReporting); | ||
expect(violations.length).toBe(0); | ||
}); | ||
}); | ||
import {Directive, Version} from '../csp'; | ||
import {Finding, Severity} from '../finding'; | ||
import {CspParser} from '../parser'; | ||
import {CheckerFunction} from './checker'; | ||
import * as securityChecks from './security_checks'; | ||
/** | ||
* Helper function for running a check on a CSP string. | ||
* | ||
* @param test CSP string. | ||
* @param checkFunction check. | ||
*/ | ||
function checkCsp(test: string, checkFunction: CheckerFunction): Finding[] { | ||
const parsedCsp = (new CspParser(test)).csp; | ||
return checkFunction(parsedCsp); | ||
} | ||
describe('Test security checks', () => { | ||
/** Tests for csp.securityChecks.checkScriptUnsafeInline */ | ||
it('CheckScriptUnsafeInlineInScriptSrc', () => { | ||
const test = 'default-src https:; script-src \'unsafe-inline\''; | ||
const violations = checkCsp(test, securityChecks.checkScriptUnsafeInline); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].severity).toBe(Severity.HIGH); | ||
}); | ||
it('CheckScriptUnsafeInlineInDefaultSrc', () => { | ||
const test = 'default-src \'unsafe-inline\''; | ||
const violations = checkCsp(test, securityChecks.checkScriptUnsafeInline); | ||
expect(violations.length).toBe(1); | ||
}); | ||
it('CheckScriptUnsafeInlineInDefaultSrcAndNotInScriptSrc', () => { | ||
const test = 'default-src \'unsafe-inline\'; script-src https:'; | ||
const violations = checkCsp(test, securityChecks.checkScriptUnsafeInline); | ||
expect(violations.length).toBe(0); | ||
}); | ||
it('CheckScriptUnsafeInlineWithNonce', () => { | ||
const test = 'script-src \'unsafe-inline\' \'nonce-foobar\''; | ||
const parsedCsp = (new CspParser(test)).csp; | ||
let effectiveCsp = parsedCsp.getEffectiveCsp(Version.CSP1); | ||
let violations = securityChecks.checkScriptUnsafeInline(effectiveCsp); | ||
expect(violations.length).toBe(1); | ||
effectiveCsp = parsedCsp.getEffectiveCsp(Version.CSP3); | ||
violations = securityChecks.checkScriptUnsafeInline(effectiveCsp); | ||
expect(violations.length).toBe(0); | ||
}); | ||
/** Tests for csp.securityChecks.checkScriptUnsafeEval */ | ||
it('CheckScriptUnsafeEvalInScriptSrc', () => { | ||
const test = 'default-src https:; script-src \'unsafe-eval\''; | ||
const violations = checkCsp(test, securityChecks.checkScriptUnsafeEval); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].severity).toBe(Severity.MEDIUM_MAYBE); | ||
}); | ||
it('CheckScriptUnsafeEvalInDefaultSrc', () => { | ||
const test = 'default-src \'unsafe-eval\''; | ||
const violations = checkCsp(test, securityChecks.checkScriptUnsafeEval); | ||
expect(violations.length).toBe(1); | ||
}); | ||
/** Tests for csp.securityChecks.checkPlainUrlSchemes */ | ||
it('CheckPlainUrlSchemesInScriptSrc', () => { | ||
const test = 'script-src data: http: https: sthInvalid:'; | ||
const violations = checkCsp(test, securityChecks.checkPlainUrlSchemes); | ||
expect(violations.length).toBe(3); | ||
expect(violations.every((v) => v.severity === Severity.HIGH)).toBeTrue(); | ||
}); | ||
it('CheckPlainUrlSchemesInObjectSrc', () => { | ||
const test = 'object-src data: http: https: sthInvalid:'; | ||
const violations = checkCsp(test, securityChecks.checkPlainUrlSchemes); | ||
expect(violations.length).toBe(3); | ||
expect(violations.every((v) => v.severity === Severity.HIGH)).toBeTrue(); | ||
}); | ||
it('CheckPlainUrlSchemesInBaseUri', () => { | ||
const test = 'base-uri data: http: https: sthInvalid:'; | ||
const violations = checkCsp(test, securityChecks.checkPlainUrlSchemes); | ||
expect(violations.length).toBe(3); | ||
expect(violations.every((v) => v.severity === Severity.HIGH)).toBeTrue(); | ||
}); | ||
it('CheckPlainUrlSchemesMixed', () => { | ||
const test = 'default-src https:; object-src data: sthInvalid:'; | ||
const violations = checkCsp(test, securityChecks.checkPlainUrlSchemes); | ||
expect(violations.length).toBe(2); | ||
expect(violations.every((v) => v.severity === Severity.HIGH)).toBeTrue(); | ||
expect(violations[0].directive).toBe(Directive.DEFAULT_SRC); | ||
expect(violations[1].directive).toBe(Directive.OBJECT_SRC); | ||
}); | ||
it('CheckPlainUrlSchemesDangerousDirectivesOK', () => { | ||
const test = | ||
'default-src https:; object-src \'none\'; script-src \'none\'; ' + | ||
'base-uri \'none\''; | ||
const violations = checkCsp(test, securityChecks.checkPlainUrlSchemes); | ||
expect(violations.length).toBe(0); | ||
}); | ||
/** Tests for csp.securityChecks.checkWildcards */ | ||
it('CheckWildcardsInScriptSrc', () => { | ||
const test = 'script-src * http://* //*'; | ||
const violations = checkCsp(test, securityChecks.checkWildcards); | ||
expect(violations.length).toBe(3); | ||
expect(violations.every((v) => v.severity === Severity.HIGH)).toBeTrue(); | ||
}); | ||
it('CheckWildcardsInObjectSrc', () => { | ||
const test = 'object-src * http://* //*'; | ||
const violations = checkCsp(test, securityChecks.checkWildcards); | ||
expect(violations.length).toBe(3); | ||
expect(violations.every((v) => v.severity === Severity.HIGH)).toBeTrue(); | ||
}); | ||
it('CheckWildcardsInBaseUri', () => { | ||
const test = 'base-uri * http://* //*'; | ||
const violations = checkCsp(test, securityChecks.checkWildcards); | ||
expect(violations.length).toBe(3); | ||
expect(violations.every((v) => v.severity === Severity.HIGH)).toBeTrue(); | ||
}); | ||
it('CheckWildcardsSchemesMixed', () => { | ||
const test = 'default-src *; object-src * ignore.me.com'; | ||
const violations = checkCsp(test, securityChecks.checkWildcards); | ||
expect(violations.length).toBe(2); | ||
expect(violations.every((v) => v.severity === Severity.HIGH)).toBeTrue(); | ||
expect(violations[0].directive).toBe(Directive.DEFAULT_SRC); | ||
expect(violations[1].directive).toBe(Directive.OBJECT_SRC); | ||
}); | ||
it('CheckWildcardsDangerousDirectivesOK', () => { | ||
const test = 'default-src *; object-src *.foo.bar; script-src \'none\'; ' + | ||
'base-uri \'none\''; | ||
const violations = checkCsp(test, securityChecks.checkWildcards); | ||
expect(violations.length).toBe(0); | ||
}); | ||
/** Tests for csp.securityChecks.checkMissingDirectives */ | ||
it('CheckMissingDirectivesMissingObjectSrc', () => { | ||
const test = 'script-src \'none\''; | ||
const violations = checkCsp(test, securityChecks.checkMissingDirectives); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].severity).toBe(Severity.HIGH); | ||
}); | ||
it('CheckMissingDirectivesMissingScriptSrc', () => { | ||
const test = 'object-src \'none\''; | ||
const violations = checkCsp(test, securityChecks.checkMissingDirectives); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].severity).toBe(Severity.HIGH); | ||
}); | ||
it('CheckMissingDirectivesObjectSrcSelf', () => { | ||
const test = 'object-src \'self\''; | ||
const violations = checkCsp(test, securityChecks.checkMissingDirectives); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].severity).toBe(Severity.HIGH); | ||
}); | ||
it('CheckMissingDirectivesMissingBaseUriInNonceCsp', () => { | ||
const test = 'script-src \'nonce-123\'; object-src \'none\''; | ||
const violations = checkCsp(test, securityChecks.checkMissingDirectives); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].severity).toBe(Severity.HIGH); | ||
}); | ||
it('CheckMissingDirectivesMissingBaseUriInHashWStrictDynamicCsp', () => { | ||
const test = | ||
'script-src \'sha256-123456\' \'strict-dynamic\'; object-src \'none\''; | ||
const violations = checkCsp(test, securityChecks.checkMissingDirectives); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].severity).toBe(Severity.HIGH); | ||
}); | ||
it('CheckMissingDirectivesMissingBaseUriInHashCsp', () => { | ||
const test = 'script-src \'sha256-123456\'; object-src \'none\''; | ||
const violations = checkCsp(test, securityChecks.checkMissingDirectives); | ||
expect(violations.length).toBe(0); | ||
}); | ||
it('CheckMissingDirectivesScriptAndObjectSrcSet', () => { | ||
const test = 'script-src \'none\'; object-src \'none\''; | ||
const violations = checkCsp(test, securityChecks.checkMissingDirectives); | ||
expect(violations.length).toBe(0); | ||
}); | ||
it('CheckMissingDirectivesDefaultSrcSet', () => { | ||
const test = 'default-src https:;'; | ||
const violations = checkCsp(test, securityChecks.checkMissingDirectives); | ||
expect(violations.length).toBe(0); | ||
}); | ||
it('CheckMissingDirectivesDefaultSrcSetToNone', () => { | ||
const test = 'default-src \'none\';'; | ||
const violations = checkCsp(test, securityChecks.checkMissingDirectives); | ||
expect(violations.length).toBe(0); | ||
}); | ||
/** Tests for csp.securityChecks.checkScriptAllowlistBypass */ | ||
it('checkScriptAllowlistBypassJSONPBypass', () => { | ||
const test = 'script-src *.google.com'; | ||
const violations = | ||
checkCsp(test, securityChecks.checkScriptAllowlistBypass); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].severity).toBe(Severity.HIGH); | ||
expect(violations[0].description.includes( | ||
'www.google.com is known to host JSONP endpoints which')) | ||
.toBeTrue(); | ||
}); | ||
it('checkScriptAllowlistBypassWithNoneAndJSONPBypass', () => { | ||
const test = 'script-src *.google.com \'none\''; | ||
const violations = | ||
checkCsp(test, securityChecks.checkScriptAllowlistBypass); | ||
expect(violations.length).toBe(0); | ||
}); | ||
it('checkScriptAllowlistBypassJSONPBypassEvalRequired', () => { | ||
const test = 'script-src https://googletagmanager.com \'unsafe-eval\''; | ||
const violations = | ||
checkCsp(test, securityChecks.checkScriptAllowlistBypass); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].severity).toBe(Severity.HIGH); | ||
}); | ||
it('checkScriptAllowlistBypassJSONPBypassEvalRequiredNotPresent', () => { | ||
const test = 'script-src https://googletagmanager.com'; | ||
const violations = | ||
checkCsp(test, securityChecks.checkScriptAllowlistBypass); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].severity).toBe(Severity.MEDIUM_MAYBE); | ||
}); | ||
it('checkScriptAllowlistBypassAngularBypass', () => { | ||
const test = 'script-src gstatic.com'; | ||
const violations = | ||
checkCsp(test, securityChecks.checkScriptAllowlistBypass); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].severity).toBe(Severity.HIGH); | ||
expect(violations[0].description.includes( | ||
'gstatic.com is known to host Angular libraries which')) | ||
.toBeTrue(); | ||
}); | ||
it('checkScriptAllowlistBypassNoBypassWarningOnly', () => { | ||
const test = 'script-src foo.bar'; | ||
const violations = | ||
checkCsp(test, securityChecks.checkScriptAllowlistBypass); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].severity).toBe(Severity.MEDIUM_MAYBE); | ||
}); | ||
it('checkScriptAllowlistBypassNoBypassSelfWarningOnly', () => { | ||
const test = 'script-src \'self\''; | ||
const violations = | ||
checkCsp(test, securityChecks.checkScriptAllowlistBypass); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].severity).toBe(Severity.MEDIUM_MAYBE); | ||
}); | ||
/** Tests for csp.securityChecks.checkFlashObjectAllowlistBypass */ | ||
it('checkFlashObjectAllowlistBypassFlashBypass', () => { | ||
const test = 'object-src https://*.googleapis.com'; | ||
const violations = | ||
checkCsp(test, securityChecks.checkFlashObjectAllowlistBypass); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].severity).toBe(Severity.HIGH); | ||
}); | ||
it('checkFlashObjectAllowlistBypassNoFlashBypass', () => { | ||
const test = 'object-src https://foo.bar'; | ||
const violations = | ||
checkCsp(test, securityChecks.checkFlashObjectAllowlistBypass); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].severity).toBe(Severity.MEDIUM_MAYBE); | ||
}); | ||
it('checkFlashObjectAllowlistBypassSelfAllowed', () => { | ||
const test = 'object-src \'self\''; | ||
const violations = | ||
checkCsp(test, securityChecks.checkFlashObjectAllowlistBypass); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].severity).toBe(Severity.MEDIUM_MAYBE); | ||
expect(violations[0].description) | ||
.toBe('Can you restrict object-src to \'none\' only?'); | ||
}); | ||
/** Tests for csp.securityChecks.checkIpSource */ | ||
it('CheckIpSource', () => { | ||
const test = | ||
'script-src 8.8.8.8; font-src //127.0.0.1 https://[::1] not.an.ip'; | ||
const violations = checkCsp(test, securityChecks.checkIpSource); | ||
expect(violations.length).toBe(3); | ||
expect(violations.every((v) => v.severity === Severity.INFO)).toBeTrue(); | ||
}); | ||
it('LooksLikeIpAddressIPv4', () => { | ||
expect(securityChecks.looksLikeIpAddress('8.8.8.8')).toBeTrue(); | ||
}); | ||
it('LooksLikeIpAddressIPv6', () => { | ||
expect(securityChecks.looksLikeIpAddress('[::1]')).toBeTrue(); | ||
}); | ||
it('CheckDeprecatedDirectiveReportUriWithReportTo', () => { | ||
const test = 'report-uri foo.bar/csp;report-to abc'; | ||
const violations = checkCsp(test, securityChecks.checkDeprecatedDirective); | ||
expect(violations.length).toBe(0); | ||
}); | ||
it('CheckDeprecatedDirectiveWithoutReportUriButWithReportTo', () => { | ||
const test = 'report-to abc'; | ||
const violations = checkCsp(test, securityChecks.checkDeprecatedDirective); | ||
expect(violations.length).toBe(0); | ||
}); | ||
it('CheckDeprecatedDirectiveReflectedXss', () => { | ||
const test = 'reflected-xss block'; | ||
const violations = checkCsp(test, securityChecks.checkDeprecatedDirective); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].severity).toBe(Severity.INFO); | ||
}); | ||
it('CheckDeprecatedDirectiveReferrer', () => { | ||
const test = 'referrer origin'; | ||
const violations = checkCsp(test, securityChecks.checkDeprecatedDirective); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].severity).toBe(Severity.INFO); | ||
}); | ||
/** Tests for csp.securityChecks.checkNonceLength */ | ||
it('CheckNonceLengthWithLongNonce', () => { | ||
const test = 'script-src \'nonce-veryLongRandomNonce\''; | ||
const violations = checkCsp(test, securityChecks.checkNonceLength); | ||
expect(violations.length).toBe(0); | ||
}); | ||
it('CheckNonceLengthWithShortNonce', () => { | ||
const test = 'script-src \'nonce-short\''; | ||
const violations = checkCsp(test, securityChecks.checkNonceLength); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].severity).toBe(Severity.MEDIUM); | ||
}); | ||
it('CheckNonceLengthInvalidCharset', () => { | ||
const test = 'script-src \'nonce-***notBase64***\''; | ||
const violations = checkCsp(test, securityChecks.checkNonceLength); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].severity).toBe(Severity.INFO); | ||
}); | ||
/** Tests for csp.securityChecks.checkSrcHttp */ | ||
it('CheckSrcHttp', () => { | ||
const test = | ||
'script-src http://foo.bar https://test.com; report-uri http://test.com'; | ||
const violations = checkCsp(test, securityChecks.checkSrcHttp); | ||
expect(violations.length).toBe(2); | ||
expect(violations.every((v) => v.severity === Severity.MEDIUM)).toBeTrue(); | ||
}); | ||
/** Tests for csp.securityChecks.checkHasConfiguredReporting */ | ||
it('CheckHasConfiguredReporting_whenNoReporting', () => { | ||
const test = 'script-src \'nonce-aaaaaaaaaa\''; | ||
const violations = | ||
checkCsp(test, securityChecks.checkHasConfiguredReporting); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].severity).toBe(Severity.INFO); | ||
expect(violations[0].directive).toBe('report-uri'); | ||
}); | ||
it('CheckHasConfiguredReporting_whenOnlyReportTo', () => { | ||
const test = 'script-src \'nonce-aaaaaaaaaa\'; report-to name'; | ||
const violations = | ||
checkCsp(test, securityChecks.checkHasConfiguredReporting); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].severity).toBe(Severity.INFO); | ||
expect(violations[0].directive).toBe('report-to'); | ||
}); | ||
it('CheckHasConfiguredReporting_whenOnlyReportUri', () => { | ||
const test = 'script-src \'nonce-aaaaaaaaaa\'; report-uri url'; | ||
const violations = | ||
checkCsp(test, securityChecks.checkHasConfiguredReporting); | ||
expect(violations.length).toBe(0); | ||
}); | ||
it('CheckHasConfiguredReporting_whenReportUriAndReportTo', () => { | ||
const test = | ||
'script-src \'nonce-aaaaaaaaaa\'; report-uri url; report-to name'; | ||
const violations = | ||
checkCsp(test, securityChecks.checkHasConfiguredReporting); | ||
expect(violations.length).toBe(0); | ||
}); | ||
}); |
@@ -172,4 +172,3 @@ /** | ||
} | ||
if (objectRestrictions !== undefined && objectRestrictions.length === 1 && | ||
objectRestrictions[0] === Keyword.NONE) { | ||
if (objectRestrictions !== undefined && objectRestrictions.length >= 1) { | ||
return []; | ||
@@ -176,0 +175,0 @@ } |
@@ -144,2 +144,8 @@ "use strict"; | ||
}); | ||
it('CheckMissingDirectivesObjectSrcSelf', () => { | ||
const test = 'object-src \'self\''; | ||
const violations = checkCsp(test, securityChecks.checkMissingDirectives); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].severity).toBe(finding_1.Severity.HIGH); | ||
}); | ||
it('CheckMissingDirectivesMissingBaseUriInNonceCsp', () => { | ||
@@ -170,4 +176,3 @@ const test = 'script-src \'nonce-123\'; object-src \'none\''; | ||
const violations = checkCsp(test, securityChecks.checkMissingDirectives); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].severity).toBe(finding_1.Severity.HIGH); | ||
expect(violations.length).toBe(0); | ||
}); | ||
@@ -174,0 +179,0 @@ it('CheckMissingDirectivesDefaultSrcSetToNone', () => { |
@@ -91,4 +91,3 @@ "use strict"; | ||
} | ||
if (objectRestrictions !== undefined && objectRestrictions.length === 1 && | ||
objectRestrictions[0] === csp_1.Keyword.NONE) { | ||
if (objectRestrictions !== undefined && objectRestrictions.length >= 1) { | ||
return []; | ||
@@ -95,0 +94,0 @@ } |
@@ -112,3 +112,3 @@ "use strict"; | ||
it('robust check only CSPs with script-src', () => { | ||
const policies = ['script-src *', 'object-src \'none\'']; | ||
const policies = ['script-src https://example.com', 'object-src \'none\'']; | ||
const violations = lighthouseChecks.evaluateForFailure(parsePolicies(policies)); | ||
@@ -173,2 +173,27 @@ expect(violations.length).toBe(1); | ||
}); | ||
it('check wildcards', () => { | ||
const policies = ['script-src \'none\'; object-src *']; | ||
const violations = lighthouseChecks.evaluateForFailure(parsePolicies(policies)); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].severity).toBe(finding_1.Severity.HIGH); | ||
expect(violations[0].directive).toBe('object-src'); | ||
expect(violations[0].description) | ||
.toBe(`object-src should not allow '*' as source`); | ||
}); | ||
it('check wildcards on multiple', () => { | ||
const policies = ['script-src \'none\'; object-src *', 'object-src \'none\'']; | ||
const violations = lighthouseChecks.evaluateForFailure(parsePolicies(policies)); | ||
expect(violations.length).toBe(0); | ||
}); | ||
it('check plain url schemes', () => { | ||
const policies = [ | ||
`script-src 'strict-dynamic' 'nonce-random123' 'unsafe-inline' https:; base-uri 'none'; object-src https:` | ||
]; | ||
const violations = lighthouseChecks.evaluateForFailure(parsePolicies(policies)); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].severity).toBe(finding_1.Severity.HIGH); | ||
expect(violations[0].directive).toBe('object-src'); | ||
expect(violations[0].description) | ||
.toBe(`https: URI in object-src allows the execution of unsafe scripts.`); | ||
}); | ||
}); | ||
@@ -216,10 +241,6 @@ describe('Test evaluateForWarnings', () => { | ||
const violations = lighthouseChecks.evaluateForWarnings(parsePolicies([test])); | ||
expect(violations.length).toBe(2); | ||
expect(violations[0].severity).toBe(finding_1.Severity.INFO); | ||
expect(violations[0].directive).toBe('report-uri'); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].severity).toBe(finding_1.Severity.STRICT_CSP); | ||
expect(violations[0].directive).toBe('script-src'); | ||
expect(violations[0].description) | ||
.toBe(`This CSP policy does not configure a reporting destination. This makes it difficult to maintain the CSP policy over time and monitor for any breakages.`); | ||
expect(violations[1].severity).toBe(finding_1.Severity.STRICT_CSP); | ||
expect(violations[1].directive).toBe('script-src'); | ||
expect(violations[1].description) | ||
.toBe('Consider adding \'unsafe-inline\' (ignored by browsers supporting nonces/hashes) to be backward compatible with older browsers.'); | ||
@@ -226,0 +247,0 @@ }); |
@@ -55,3 +55,4 @@ "use strict"; | ||
]; | ||
const effectiveCsps = parsedCsps.map(csp => csp.getEffectiveCsp(csp_1.Version.CSP3)).filter(csp => { | ||
const effectiveCsps = parsedCsps.map(csp => csp.getEffectiveCsp(csp_1.Version.CSP3)); | ||
const effectiveCspsWithScript = effectiveCsps.filter(csp => { | ||
const directiveName = csp.getEffectiveDirective(csp_1.Directive.SCRIPT_SRC); | ||
@@ -61,4 +62,6 @@ return csp.directives[directiveName]; | ||
const robust = [ | ||
...atLeastOnePasses(effectiveCsps, strictcsp_checks_1.checkStrictDynamic), | ||
...atLeastOnePasses(effectiveCsps, security_checks_1.checkScriptUnsafeInline), | ||
...atLeastOnePasses(effectiveCspsWithScript, strictcsp_checks_1.checkStrictDynamic), | ||
...atLeastOnePasses(effectiveCspsWithScript, security_checks_1.checkScriptUnsafeInline), | ||
...atLeastOnePasses(effectiveCsps, security_checks_1.checkWildcards), | ||
...atLeastOnePasses(effectiveCsps, security_checks_1.checkPlainUrlSchemes), | ||
]; | ||
@@ -69,10 +72,6 @@ return [...targetsXssFindings, ...robust]; | ||
function evaluateForWarnings(parsedCsps) { | ||
const hasReportingFindings = atLeastOnePasses(parsedCsps, security_checks_1.checkHasConfiguredReporting); | ||
const compatibleWithNonCompliantBrowsersFindings = [ | ||
return [ | ||
...atLeastOneFails(parsedCsps, strictcsp_checks_1.checkUnsafeInlineFallback), | ||
...atLeastOneFails(parsedCsps, strictcsp_checks_1.checkAllowlistFallback) | ||
]; | ||
return [ | ||
...hasReportingFindings, ...compatibleWithNonCompliantBrowsersFindings | ||
]; | ||
} | ||
@@ -79,0 +78,0 @@ exports.evaluateForWarnings = evaluateForWarnings; |
@@ -5,504 +5,535 @@ /** | ||
import 'jasmine'; | ||
import 'jasmine'; | ||
import {Csp,} from '../csp'; | ||
import {Severity} from '../finding'; | ||
import {CspParser} from '../parser'; | ||
import * as lighthouseChecks from './lighthouse_checks'; | ||
function parsePolicies(policies: string[]): Csp[] { | ||
return policies.map(p => (new CspParser(p)).csp); | ||
} | ||
describe('Test evaluateForFailure', () => { | ||
it('robust nonce-based policy', () => { | ||
const test = | ||
'script-src \'nonce-aaaaaaaaaa\'; object-src \'none\'; base-uri \'none\''; | ||
const violations = | ||
lighthouseChecks.evaluateForFailure(parsePolicies([test])); | ||
expect(violations.length).toBe(0); | ||
}); | ||
it('robust hash-based policy', () => { | ||
const test = 'script-src \'sha256-aaaaaaaaaa\'; object-src \'none\''; | ||
const violations = | ||
lighthouseChecks.evaluateForFailure(parsePolicies([test])); | ||
expect(violations.length).toBe(0); | ||
}); | ||
it('policy not attempt', () => { | ||
const test = 'block-all-mixed-content'; | ||
const violations = | ||
lighthouseChecks.evaluateForFailure(parsePolicies([test])); | ||
expect(violations.length).toBe(2); | ||
expect(violations[0].severity).toBe(Severity.HIGH); | ||
expect(violations[0].directive).toBe('script-src'); | ||
expect(violations[0].description).toBe('script-src directive is missing.'); | ||
expect(violations[1].severity).toBe(Severity.HIGH); | ||
expect(violations[1].directive).toBe('object-src'); | ||
expect(violations[1].description) | ||
.toBe( | ||
`Missing object-src allows the injection of plugins which can execute JavaScript. Can you set it to 'none'?`); | ||
}); | ||
it('policy not robust', () => { | ||
const test = 'script-src *.google.com; object-src \'none\''; | ||
const violations = | ||
lighthouseChecks.evaluateForFailure(parsePolicies([test])); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].severity).toBe(Severity.STRICT_CSP); | ||
expect(violations[0].directive).toBe('script-src'); | ||
expect(violations[0].description) | ||
.toBe( | ||
`Host allowlists can frequently be bypassed. Consider using 'strict-dynamic' in combination with CSP nonces or hashes.`); | ||
}); | ||
it('robust policy and not robust policy', () => { | ||
const policies = [ | ||
'script-src *.google.com; object-src \'none\'', | ||
'script-src \'nonce-aaaaaaaaaa\'; base-uri \'none\'' | ||
]; | ||
const violations = | ||
lighthouseChecks.evaluateForFailure(parsePolicies(policies)); | ||
expect(violations.length).toBe(0); | ||
}); | ||
it('split across many policies', () => { | ||
const policies = [ | ||
'object-src \'none\'', 'script-src \'nonce-aaaaaaaaaa\'', | ||
'base-uri \'none\'' | ||
]; | ||
const violations = | ||
lighthouseChecks.evaluateForFailure(parsePolicies(policies)); | ||
expect(violations.length).toBe(0); | ||
}); | ||
it('split across many policies with default-src', () => { | ||
const policies = ['default-src \'none\'', 'base-uri \'none\'']; | ||
const violations = | ||
lighthouseChecks.evaluateForFailure(parsePolicies(policies)); | ||
expect(violations.length).toBe(0); | ||
}); | ||
it('split across many policies some mixed useless policies', () => { | ||
const policies = [ | ||
'object-src \'none\'', 'script-src \'nonce-aaaaaaaaaa\'', | ||
'base-uri \'none\'', 'block-all-mixed-content' | ||
]; | ||
const violations = | ||
lighthouseChecks.evaluateForFailure(parsePolicies(policies)); | ||
expect(violations.length).toBe(0); | ||
}); | ||
it('split across many policies with allowlist', () => { | ||
const policies = [ | ||
'object-src \'none\'', 'script-src \'nonce-aaaaaaaaaa\'', | ||
'base-uri \'none\'', 'script-src *' | ||
]; | ||
const violations = | ||
lighthouseChecks.evaluateForFailure(parsePolicies(policies)); | ||
expect(violations.length).toBe(0); | ||
}); | ||
it('not robust and not attempt', () => { | ||
const policies = ['block-all-mixed-content', 'script-src *.google.com']; | ||
const violations = | ||
lighthouseChecks.evaluateForFailure(parsePolicies(policies)); | ||
expect(violations.length).toBe(2); | ||
expect(violations[0].severity).toBe(Severity.HIGH); | ||
expect(violations[0].directive).toBe('object-src'); | ||
expect(violations[0].description) | ||
.toBe( | ||
`Missing object-src allows the injection of plugins which can execute JavaScript. Can you set it to 'none'?`); | ||
expect(violations[1].severity).toBe(Severity.STRICT_CSP); | ||
expect(violations[1].directive).toBe('script-src'); | ||
expect(violations[1].description) | ||
.toBe( | ||
`Host allowlists can frequently be bypassed. Consider using \'strict-dynamic\' in combination with CSP nonces or hashes.`); | ||
}); | ||
it('robust check only CSPs with script-src', () => { | ||
const policies = ['script-src *', 'object-src \'none\'']; | ||
const violations = | ||
lighthouseChecks.evaluateForFailure(parsePolicies(policies)); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].severity).toBe(Severity.STRICT_CSP); | ||
expect(violations[0].directive).toBe('script-src'); | ||
expect(violations[0].description) | ||
.toBe( | ||
`Host allowlists can frequently be bypassed. Consider using \'strict-dynamic\' in combination with CSP nonces or hashes.`); | ||
}); | ||
it('two not attempt', () => { | ||
const policies = ['block-all-mixed-content', 'block-all-mixed-content']; | ||
const violations = | ||
lighthouseChecks.evaluateForFailure(parsePolicies(policies)); | ||
expect(violations.length).toBe(2); | ||
expect(violations[0].severity).toBe(Severity.HIGH); | ||
expect(violations[0].directive).toBe('script-src'); | ||
expect(violations[0].description).toBe('script-src directive is missing.'); | ||
expect(violations[1].severity).toBe(Severity.HIGH); | ||
expect(violations[1].directive).toBe('object-src'); | ||
expect(violations[1].description) | ||
.toBe( | ||
`Missing object-src allows the injection of plugins which can execute JavaScript. Can you set it to 'none'?`); | ||
}); | ||
it('two not attempt somewhat', () => { | ||
const policies = [ | ||
'block-all-mixed-content; object-src \'none\'', | ||
'block-all-mixed-content', | ||
]; | ||
const violations = | ||
lighthouseChecks.evaluateForFailure(parsePolicies(policies)); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].severity).toBe(Severity.HIGH); | ||
expect(violations[0].directive).toBe('script-src'); | ||
expect(violations[0].description).toBe('script-src directive is missing.'); | ||
}); | ||
it('base-uri split across many policies', () => { | ||
const policies = [ | ||
'script-src \'nonce-aaaaaaaaaaa\'; object-src \'none\'', | ||
'base-uri \'none\'', | ||
]; | ||
const violations = | ||
lighthouseChecks.evaluateForFailure(parsePolicies(policies)); | ||
expect(violations.length).toBe(0); | ||
}); | ||
it('base-uri not set', () => { | ||
const policies = [ | ||
'script-src \'nonce-aaaaaaaaaaa\'; object-src \'none\'', | ||
]; | ||
const violations = | ||
lighthouseChecks.evaluateForFailure(parsePolicies(policies)); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].severity).toBe(Severity.HIGH); | ||
expect(violations[0].directive).toBe('base-uri'); | ||
expect(violations[0].description) | ||
.toBe( | ||
`Missing base-uri allows the injection of base tags. They can be used to set the base URL for all relative (script) URLs to an attacker controlled domain. Can you set it to 'none' or 'self'?`); | ||
}); | ||
it('base-uri not set in either policy', () => { | ||
const policies = [ | ||
'script-src \'nonce-aaaaaaaaaaa\'; object-src \'none\'', | ||
'block-all-mixed-content' | ||
]; | ||
const violations = | ||
lighthouseChecks.evaluateForFailure(parsePolicies(policies)); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].severity).toBe(Severity.HIGH); | ||
expect(violations[0].directive).toBe('base-uri'); | ||
}); | ||
}); | ||
describe('Test evaluateForWarnings', () => { | ||
it('perfect', () => { | ||
const test = | ||
'script-src \'nonce-aaaaaaaaaa\' \'unsafe-inline\' http: https:; report-uri url'; | ||
const violations = | ||
lighthouseChecks.evaluateForWarnings(parsePolicies([test])); | ||
expect(violations.length).toBe(0); | ||
}); | ||
it('perfect except some failures', () => { | ||
const policies = [ | ||
'script-src \'nonce-aaaaaaaaaa\' \'unsafe-inline\' http: https:; report-uri url; object-src \'none\'', | ||
'block-all-mixed-content' | ||
]; | ||
const violations = | ||
lighthouseChecks.evaluateForWarnings(parsePolicies(policies)); | ||
expect(violations.length).toBe(0); | ||
}); | ||
it('a perfect policy and a policy that does not target', () => { | ||
const policies = [ | ||
'script-src \'nonce-aaaaaaaaaa\' \'unsafe-inline\' http: https:; report-uri url; base-uri \'none\'; object-src \'none\'', | ||
'block-all-mixed-content' | ||
]; | ||
const violations = | ||
lighthouseChecks.evaluateForWarnings(parsePolicies(policies)); | ||
expect(violations.length).toBe(0); | ||
}); | ||
it('perfect policy split into two', () => { | ||
const policies = [ | ||
'script-src \'nonce-aaaaaaaaaa\' \'unsafe-inline\' http: https:; report-uri url; base-uri \'none\'; ', | ||
'block-all-mixed-content; object-src \'none\'' | ||
]; | ||
const violations = | ||
lighthouseChecks.evaluateForWarnings(parsePolicies(policies)); | ||
expect(violations.length).toBe(0); | ||
}); | ||
it('perfect policy split into three', () => { | ||
const policies = [ | ||
'script-src \'nonce-aaaaaaaaaa\' \'unsafe-inline\' http: https:; report-uri url; base-uri \'none\'; ', | ||
'block-all-mixed-content', 'object-src \'none\'' | ||
]; | ||
const violations = | ||
lighthouseChecks.evaluateForWarnings(parsePolicies(policies)); | ||
expect(violations.length).toBe(0); | ||
}); | ||
it('no reporting and malformed', () => { | ||
const test = 'script-src \'nonce-aaaaaaaaaa\'; unknown-directive'; | ||
const violations = | ||
lighthouseChecks.evaluateForWarnings(parsePolicies([test])); | ||
expect(violations.length).toBe(2); | ||
expect(violations[0].severity).toBe(Severity.INFO); | ||
expect(violations[0].directive).toBe('report-uri'); | ||
expect(violations[0].description) | ||
.toBe( | ||
`This CSP policy does not configure a reporting destination. This makes it difficult to maintain the CSP policy over time and monitor for any breakages.`); | ||
expect(violations[1].severity).toBe(Severity.STRICT_CSP); | ||
expect(violations[1].directive).toBe('script-src'); | ||
expect(violations[1].description) | ||
.toBe( | ||
'Consider adding \'unsafe-inline\' (ignored by browsers supporting nonces/hashes) to be backward compatible with older browsers.'); | ||
}); | ||
it('missing unsafe-inline fallback', () => { | ||
const test = 'script-src \'nonce-aaaaaaaaaa\'; report-uri url'; | ||
const violations = | ||
lighthouseChecks.evaluateForWarnings(parsePolicies([test])); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].severity).toBe(Severity.STRICT_CSP); | ||
expect(violations[0].directive).toBe('script-src'); | ||
expect(violations[0].description) | ||
.toBe( | ||
'Consider adding \'unsafe-inline\' (ignored by browsers supporting nonces/hashes) to be backward compatible with older browsers.'); | ||
}); | ||
it('missing allowlist fallback', () => { | ||
const test = | ||
'script-src \'nonce-aaaaaaaaaa\' \'strict-dynamic\' \'unsafe-inline\'; report-uri url'; | ||
const violations = | ||
lighthouseChecks.evaluateForWarnings(parsePolicies([test])); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].severity).toBe(Severity.STRICT_CSP); | ||
expect(violations[0].directive).toBe('script-src'); | ||
expect(violations[0].description) | ||
.toBe( | ||
'Consider adding https: and http: url schemes (ignored by browsers supporting \'strict-dynamic\') to be backward compatible with older browsers.'); | ||
}); | ||
it('missing semicolon', () => { | ||
const test = | ||
'script-src \'nonce-aaaaaaaaa\' \'unsafe-inline\'; report-uri url object-src \'self\''; | ||
const violations = | ||
lighthouseChecks.evaluateForWarnings(parsePolicies([test])); | ||
expect(violations.length).toBe(0); | ||
}); | ||
it('invalid keyword', () => { | ||
const test = | ||
'script-src \'nonce-aaaaaaaaa\' \'invalid\' \'unsafe-inline\'; report-uri url'; | ||
const violations = | ||
lighthouseChecks.evaluateForWarnings(parsePolicies([test])); | ||
expect(violations.length).toBe(0); | ||
}); | ||
it('perfect policy and invalid policy', () => { | ||
const policies = [ | ||
'script-src \'nonce-aaaaaaaaaa\' \'unsafe-inline\' http: https:; report-uri url; base-uri \'none\'; object-src \'none\'', | ||
'unknown' | ||
]; | ||
const violations = | ||
lighthouseChecks.evaluateForWarnings(parsePolicies(policies)); | ||
expect(violations.length).toBe(0); | ||
}); | ||
it('reporting on the wrong policy', () => { | ||
const policies = [ | ||
'script-src \'nonce-aaaaaaaaaa\' \'unsafe-inline\' http: https:', | ||
'block-all-mixed-content; report-uri url' | ||
]; | ||
const violations = | ||
lighthouseChecks.evaluateForWarnings(parsePolicies(policies)); | ||
expect(violations.length).toBe(0); | ||
}); | ||
it('missing unsafe-inline fallback split over two policies', () => { | ||
const policies = [ | ||
'script-src \'nonce-aaaaaaaaaa\'', | ||
'block-all-mixed-content; report-uri url' | ||
]; | ||
const violations = | ||
lighthouseChecks.evaluateForWarnings(parsePolicies(policies)); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].severity).toBe(Severity.STRICT_CSP); | ||
expect(violations[0].directive).toBe('script-src'); | ||
expect(violations[0].description) | ||
.toBe( | ||
'Consider adding \'unsafe-inline\' (ignored by browsers supporting nonces/hashes) to be backward compatible with older browsers.'); | ||
}); | ||
it('strict-dynamic with no fallback in any policy', () => { | ||
const policies = [ | ||
'script-src \'nonce-aaaaaaaaaa\' \'strict-dynamic\'', | ||
'block-all-mixed-content; report-uri url' | ||
]; | ||
const violations = | ||
lighthouseChecks.evaluateForWarnings(parsePolicies(policies)); | ||
expect(violations.length).toBe(2); | ||
expect(violations[0].severity).toBe(Severity.STRICT_CSP); | ||
expect(violations[0].directive).toBe('script-src'); | ||
expect(violations[0].description) | ||
.toBe( | ||
'Consider adding \'unsafe-inline\' (ignored by browsers supporting nonces/hashes) to be backward compatible with older browsers.'); | ||
expect(violations[1].severity).toBe(Severity.STRICT_CSP); | ||
expect(violations[1].directive).toBe('script-src'); | ||
expect(violations[1].description) | ||
.toBe( | ||
'Consider adding https: and http: url schemes (ignored by browsers supporting \'strict-dynamic\') to be backward compatible with older browsers.'); | ||
}); | ||
}); | ||
describe('Test evaluateForSyntaxErrors', () => { | ||
it('whenPerfectPolicies', () => { | ||
const policies = [ | ||
'script-src \'nonce-aaaaaaaaaa\' \'unsafe-inline\' http: https:', | ||
'block-all-mixed-content; report-uri url' | ||
]; | ||
const violations = | ||
lighthouseChecks.evaluateForSyntaxErrors(parsePolicies(policies)); | ||
expect(violations.length).toBe(2); | ||
expect(violations[0].length).toBe(0); | ||
expect(violations[1].length).toBe(0); | ||
}); | ||
it('whenShortNonce', () => { | ||
const test = 'script-src \'nonce-a\' \'unsafe-inline\'; report-uri url'; | ||
const violations = | ||
lighthouseChecks.evaluateForSyntaxErrors(parsePolicies([test])); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].length).toBe(1); | ||
expect(violations[0][0].severity).toBe(Severity.MEDIUM); | ||
expect(violations[0][0].directive).toBe('script-src'); | ||
expect(violations[0][0].description) | ||
.toBe('Nonces should be at least 8 characters long.'); | ||
}); | ||
it('whenUnknownDirective', () => { | ||
const test = | ||
'script-src \'nonce-aaaaaaaaa\' \'unsafe-inline\'; report-uri url; unknown'; | ||
const violations = | ||
lighthouseChecks.evaluateForSyntaxErrors(parsePolicies([test])); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].length).toBe(1); | ||
expect(violations[0][0].severity).toBe(Severity.SYNTAX); | ||
expect(violations[0][0].directive).toBe('unknown'); | ||
expect(violations[0][0].description) | ||
.toBe('Directive "unknown" is not a known CSP directive.'); | ||
}); | ||
it('whenDeprecatedDirective', () => { | ||
const test = | ||
'script-src \'nonce-aaaaaaaaa\' \'unsafe-inline\'; report-uri url; reflected-xss foo'; | ||
const violations = | ||
lighthouseChecks.evaluateForSyntaxErrors(parsePolicies([test])); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].length).toBe(1); | ||
expect(violations[0][0].severity).toBe(Severity.INFO); | ||
expect(violations[0][0].directive).toBe('reflected-xss'); | ||
expect(violations[0][0].description) | ||
.toBe( | ||
'reflected-xss is deprecated since CSP2. Please, use the X-XSS-Protection header instead.'); | ||
}); | ||
it('whenMissingSemicolon', () => { | ||
const test = | ||
'script-src \'nonce-aaaaaaaaa\' \'unsafe-inline\'; report-uri url object-src \'none\''; | ||
const violations = | ||
lighthouseChecks.evaluateForSyntaxErrors(parsePolicies([test])); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].length).toBe(1); | ||
expect(violations[0][0].severity).toBe(Severity.SYNTAX); | ||
expect(violations[0][0].directive).toBe('report-uri'); | ||
expect(violations[0][0].description) | ||
.toBe( | ||
'Did you forget the semicolon? "object-src" seems to be a directive, not a value.'); | ||
}); | ||
it('whenInvalidKeyword', () => { | ||
const test = | ||
'script-src \'nonce-aaaaaaaaa\' \'unsafe-inline\'; object-src \'invalid\''; | ||
const violations = | ||
lighthouseChecks.evaluateForSyntaxErrors(parsePolicies([test])); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].length).toBe(1); | ||
expect(violations[0][0].severity).toBe(Severity.SYNTAX); | ||
expect(violations[0][0].directive).toBe('object-src'); | ||
expect(violations[0][0].description) | ||
.toBe('\'invalid\' seems to be an invalid CSP keyword.'); | ||
}); | ||
it('manyPolicies', () => { | ||
const policies = [ | ||
'object-src \'invalid\'', 'script-src \'none\'', | ||
'script-src \'nonce-short\' default-src \'none\'' | ||
]; | ||
const violations = | ||
lighthouseChecks.evaluateForSyntaxErrors(parsePolicies(policies)); | ||
expect(violations.length).toBe(3); | ||
expect(violations[0].length).toBe(1); | ||
expect(violations[0][0].severity).toBe(Severity.SYNTAX); | ||
expect(violations[0][0].directive).toBe('object-src'); | ||
expect(violations[0][0].description) | ||
.toBe('\'invalid\' seems to be an invalid CSP keyword.'); | ||
expect(violations[1].length).toBe(0); | ||
expect(violations[2].length).toBe(2); | ||
expect(violations[2][0].severity).toBe(Severity.MEDIUM); | ||
expect(violations[2][0].directive).toBe('script-src'); | ||
expect(violations[2][0].description) | ||
.toBe('Nonces should be at least 8 characters long.'); | ||
expect(violations[2][1].severity).toBe(Severity.SYNTAX); | ||
expect(violations[2][1].directive).toBe('script-src'); | ||
expect(violations[2][1].description) | ||
.toBe( | ||
'Did you forget the semicolon? "default-src" seems to be a directive, not a value.'); | ||
}); | ||
}); | ||
import {Csp,} from '../csp'; | ||
import {Severity} from '../finding'; | ||
import {CspParser} from '../parser'; | ||
import * as lighthouseChecks from './lighthouse_checks'; | ||
function parsePolicies(policies: string[]): Csp[] { | ||
return policies.map(p => (new CspParser(p)).csp); | ||
} | ||
describe('Test evaluateForFailure', () => { | ||
it('robust nonce-based policy', () => { | ||
const test = | ||
'script-src \'nonce-aaaaaaaaaa\'; object-src \'none\'; base-uri \'none\''; | ||
const violations = | ||
lighthouseChecks.evaluateForFailure(parsePolicies([test])); | ||
expect(violations.length).toBe(0); | ||
}); | ||
it('robust hash-based policy', () => { | ||
const test = 'script-src \'sha256-aaaaaaaaaa\'; object-src \'none\''; | ||
const violations = | ||
lighthouseChecks.evaluateForFailure(parsePolicies([test])); | ||
expect(violations.length).toBe(0); | ||
}); | ||
it('policy not attempt', () => { | ||
const test = 'block-all-mixed-content'; | ||
const violations = | ||
lighthouseChecks.evaluateForFailure(parsePolicies([test])); | ||
expect(violations.length).toBe(2); | ||
expect(violations[0].severity).toBe(Severity.HIGH); | ||
expect(violations[0].directive).toBe('script-src'); | ||
expect(violations[0].description).toBe('script-src directive is missing.'); | ||
expect(violations[1].severity).toBe(Severity.HIGH); | ||
expect(violations[1].directive).toBe('object-src'); | ||
expect(violations[1].description) | ||
.toBe( | ||
`Missing object-src allows the injection of plugins which can execute JavaScript. Can you set it to 'none'?`); | ||
}); | ||
it('policy not robust', () => { | ||
const test = 'script-src *.google.com; object-src \'none\''; | ||
const violations = | ||
lighthouseChecks.evaluateForFailure(parsePolicies([test])); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].severity).toBe(Severity.STRICT_CSP); | ||
expect(violations[0].directive).toBe('script-src'); | ||
expect(violations[0].description) | ||
.toBe( | ||
`Host allowlists can frequently be bypassed. Consider using 'strict-dynamic' in combination with CSP nonces or hashes.`); | ||
}); | ||
it('robust policy and not robust policy', () => { | ||
const policies = [ | ||
'script-src *.google.com; object-src \'none\'', | ||
'script-src \'nonce-aaaaaaaaaa\'; base-uri \'none\'' | ||
]; | ||
const violations = | ||
lighthouseChecks.evaluateForFailure(parsePolicies(policies)); | ||
expect(violations.length).toBe(0); | ||
}); | ||
it('split across many policies', () => { | ||
const policies = [ | ||
'object-src \'none\'', 'script-src \'nonce-aaaaaaaaaa\'', | ||
'base-uri \'none\'' | ||
]; | ||
const violations = | ||
lighthouseChecks.evaluateForFailure(parsePolicies(policies)); | ||
expect(violations.length).toBe(0); | ||
}); | ||
it('split across many policies with default-src', () => { | ||
const policies = ['default-src \'none\'', 'base-uri \'none\'']; | ||
const violations = | ||
lighthouseChecks.evaluateForFailure(parsePolicies(policies)); | ||
expect(violations.length).toBe(0); | ||
}); | ||
it('split across many policies some mixed useless policies', () => { | ||
const policies = [ | ||
'object-src \'none\'', 'script-src \'nonce-aaaaaaaaaa\'', | ||
'base-uri \'none\'', 'block-all-mixed-content' | ||
]; | ||
const violations = | ||
lighthouseChecks.evaluateForFailure(parsePolicies(policies)); | ||
expect(violations.length).toBe(0); | ||
}); | ||
it('split across many policies with allowlist', () => { | ||
const policies = [ | ||
'object-src \'none\'', 'script-src \'nonce-aaaaaaaaaa\'', | ||
'base-uri \'none\'', 'script-src *' | ||
]; | ||
const violations = | ||
lighthouseChecks.evaluateForFailure(parsePolicies(policies)); | ||
expect(violations.length).toBe(0); | ||
}); | ||
it('not robust and not attempt', () => { | ||
const policies = ['block-all-mixed-content', 'script-src *.google.com']; | ||
const violations = | ||
lighthouseChecks.evaluateForFailure(parsePolicies(policies)); | ||
expect(violations.length).toBe(2); | ||
expect(violations[0].severity).toBe(Severity.HIGH); | ||
expect(violations[0].directive).toBe('object-src'); | ||
expect(violations[0].description) | ||
.toBe( | ||
`Missing object-src allows the injection of plugins which can execute JavaScript. Can you set it to 'none'?`); | ||
expect(violations[1].severity).toBe(Severity.STRICT_CSP); | ||
expect(violations[1].directive).toBe('script-src'); | ||
expect(violations[1].description) | ||
.toBe( | ||
`Host allowlists can frequently be bypassed. Consider using \'strict-dynamic\' in combination with CSP nonces or hashes.`); | ||
}); | ||
it('robust check only CSPs with script-src', () => { | ||
const policies = ['script-src https://example.com', 'object-src \'none\'']; | ||
const violations = | ||
lighthouseChecks.evaluateForFailure(parsePolicies(policies)); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].severity).toBe(Severity.STRICT_CSP); | ||
expect(violations[0].directive).toBe('script-src'); | ||
expect(violations[0].description) | ||
.toBe( | ||
`Host allowlists can frequently be bypassed. Consider using \'strict-dynamic\' in combination with CSP nonces or hashes.`); | ||
}); | ||
it('two not attempt', () => { | ||
const policies = ['block-all-mixed-content', 'block-all-mixed-content']; | ||
const violations = | ||
lighthouseChecks.evaluateForFailure(parsePolicies(policies)); | ||
expect(violations.length).toBe(2); | ||
expect(violations[0].severity).toBe(Severity.HIGH); | ||
expect(violations[0].directive).toBe('script-src'); | ||
expect(violations[0].description).toBe('script-src directive is missing.'); | ||
expect(violations[1].severity).toBe(Severity.HIGH); | ||
expect(violations[1].directive).toBe('object-src'); | ||
expect(violations[1].description) | ||
.toBe( | ||
`Missing object-src allows the injection of plugins which can execute JavaScript. Can you set it to 'none'?`); | ||
}); | ||
it('two not attempt somewhat', () => { | ||
const policies = [ | ||
'block-all-mixed-content; object-src \'none\'', | ||
'block-all-mixed-content', | ||
]; | ||
const violations = | ||
lighthouseChecks.evaluateForFailure(parsePolicies(policies)); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].severity).toBe(Severity.HIGH); | ||
expect(violations[0].directive).toBe('script-src'); | ||
expect(violations[0].description).toBe('script-src directive is missing.'); | ||
}); | ||
it('base-uri split across many policies', () => { | ||
const policies = [ | ||
'script-src \'nonce-aaaaaaaaaaa\'; object-src \'none\'', | ||
'base-uri \'none\'', | ||
]; | ||
const violations = | ||
lighthouseChecks.evaluateForFailure(parsePolicies(policies)); | ||
expect(violations.length).toBe(0); | ||
}); | ||
it('base-uri not set', () => { | ||
const policies = [ | ||
'script-src \'nonce-aaaaaaaaaaa\'; object-src \'none\'', | ||
]; | ||
const violations = | ||
lighthouseChecks.evaluateForFailure(parsePolicies(policies)); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].severity).toBe(Severity.HIGH); | ||
expect(violations[0].directive).toBe('base-uri'); | ||
expect(violations[0].description) | ||
.toBe( | ||
`Missing base-uri allows the injection of base tags. They can be used to set the base URL for all relative (script) URLs to an attacker controlled domain. Can you set it to 'none' or 'self'?`); | ||
}); | ||
it('base-uri not set in either policy', () => { | ||
const policies = [ | ||
'script-src \'nonce-aaaaaaaaaaa\'; object-src \'none\'', | ||
'block-all-mixed-content' | ||
]; | ||
const violations = | ||
lighthouseChecks.evaluateForFailure(parsePolicies(policies)); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].severity).toBe(Severity.HIGH); | ||
expect(violations[0].directive).toBe('base-uri'); | ||
}); | ||
it('check wildcards', () => { | ||
const policies = ['script-src \'none\'; object-src *']; | ||
const violations = | ||
lighthouseChecks.evaluateForFailure(parsePolicies(policies)); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].severity).toBe(Severity.HIGH); | ||
expect(violations[0].directive).toBe('object-src'); | ||
expect(violations[0].description) | ||
.toBe(`object-src should not allow '*' as source`); | ||
}); | ||
it('check wildcards on multiple', () => { | ||
const policies = | ||
['script-src \'none\'; object-src *', 'object-src \'none\'']; | ||
const violations = | ||
lighthouseChecks.evaluateForFailure(parsePolicies(policies)); | ||
expect(violations.length).toBe(0); | ||
}); | ||
it('check plain url schemes', () => { | ||
const policies = [ | ||
`script-src 'strict-dynamic' 'nonce-random123' 'unsafe-inline' https:; base-uri 'none'; object-src https:` | ||
]; | ||
const violations = | ||
lighthouseChecks.evaluateForFailure(parsePolicies(policies)); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].severity).toBe(Severity.HIGH); | ||
expect(violations[0].directive).toBe('object-src'); | ||
expect(violations[0].description) | ||
.toBe( | ||
`https: URI in object-src allows the execution of unsafe scripts.`); | ||
}); | ||
}); | ||
describe('Test evaluateForWarnings', () => { | ||
it('perfect', () => { | ||
const test = | ||
'script-src \'nonce-aaaaaaaaaa\' \'unsafe-inline\' http: https:; report-uri url'; | ||
const violations = | ||
lighthouseChecks.evaluateForWarnings(parsePolicies([test])); | ||
expect(violations.length).toBe(0); | ||
}); | ||
it('perfect except some failures', () => { | ||
const policies = [ | ||
'script-src \'nonce-aaaaaaaaaa\' \'unsafe-inline\' http: https:; report-uri url; object-src \'none\'', | ||
'block-all-mixed-content' | ||
]; | ||
const violations = | ||
lighthouseChecks.evaluateForWarnings(parsePolicies(policies)); | ||
expect(violations.length).toBe(0); | ||
}); | ||
it('a perfect policy and a policy that does not target', () => { | ||
const policies = [ | ||
'script-src \'nonce-aaaaaaaaaa\' \'unsafe-inline\' http: https:; report-uri url; base-uri \'none\'; object-src \'none\'', | ||
'block-all-mixed-content' | ||
]; | ||
const violations = | ||
lighthouseChecks.evaluateForWarnings(parsePolicies(policies)); | ||
expect(violations.length).toBe(0); | ||
}); | ||
it('perfect policy split into two', () => { | ||
const policies = [ | ||
'script-src \'nonce-aaaaaaaaaa\' \'unsafe-inline\' http: https:; report-uri url; base-uri \'none\'; ', | ||
'block-all-mixed-content; object-src \'none\'' | ||
]; | ||
const violations = | ||
lighthouseChecks.evaluateForWarnings(parsePolicies(policies)); | ||
expect(violations.length).toBe(0); | ||
}); | ||
it('perfect policy split into three', () => { | ||
const policies = [ | ||
'script-src \'nonce-aaaaaaaaaa\' \'unsafe-inline\' http: https:; report-uri url; base-uri \'none\'; ', | ||
'block-all-mixed-content', 'object-src \'none\'' | ||
]; | ||
const violations = | ||
lighthouseChecks.evaluateForWarnings(parsePolicies(policies)); | ||
expect(violations.length).toBe(0); | ||
}); | ||
it('no reporting and malformed', () => { | ||
const test = 'script-src \'nonce-aaaaaaaaaa\'; unknown-directive'; | ||
const violations = | ||
lighthouseChecks.evaluateForWarnings(parsePolicies([test])); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].severity).toBe(Severity.STRICT_CSP); | ||
expect(violations[0].directive).toBe('script-src'); | ||
expect(violations[0].description) | ||
.toBe( | ||
'Consider adding \'unsafe-inline\' (ignored by browsers supporting nonces/hashes) to be backward compatible with older browsers.'); | ||
}); | ||
it('missing unsafe-inline fallback', () => { | ||
const test = 'script-src \'nonce-aaaaaaaaaa\'; report-uri url'; | ||
const violations = | ||
lighthouseChecks.evaluateForWarnings(parsePolicies([test])); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].severity).toBe(Severity.STRICT_CSP); | ||
expect(violations[0].directive).toBe('script-src'); | ||
expect(violations[0].description) | ||
.toBe( | ||
'Consider adding \'unsafe-inline\' (ignored by browsers supporting nonces/hashes) to be backward compatible with older browsers.'); | ||
}); | ||
it('missing allowlist fallback', () => { | ||
const test = | ||
'script-src \'nonce-aaaaaaaaaa\' \'strict-dynamic\' \'unsafe-inline\'; report-uri url'; | ||
const violations = | ||
lighthouseChecks.evaluateForWarnings(parsePolicies([test])); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].severity).toBe(Severity.STRICT_CSP); | ||
expect(violations[0].directive).toBe('script-src'); | ||
expect(violations[0].description) | ||
.toBe( | ||
'Consider adding https: and http: url schemes (ignored by browsers supporting \'strict-dynamic\') to be backward compatible with older browsers.'); | ||
}); | ||
it('missing semicolon', () => { | ||
const test = | ||
'script-src \'nonce-aaaaaaaaa\' \'unsafe-inline\'; report-uri url object-src \'self\''; | ||
const violations = | ||
lighthouseChecks.evaluateForWarnings(parsePolicies([test])); | ||
expect(violations.length).toBe(0); | ||
}); | ||
it('invalid keyword', () => { | ||
const test = | ||
'script-src \'nonce-aaaaaaaaa\' \'invalid\' \'unsafe-inline\'; report-uri url'; | ||
const violations = | ||
lighthouseChecks.evaluateForWarnings(parsePolicies([test])); | ||
expect(violations.length).toBe(0); | ||
}); | ||
it('perfect policy and invalid policy', () => { | ||
const policies = [ | ||
'script-src \'nonce-aaaaaaaaaa\' \'unsafe-inline\' http: https:; report-uri url; base-uri \'none\'; object-src \'none\'', | ||
'unknown' | ||
]; | ||
const violations = | ||
lighthouseChecks.evaluateForWarnings(parsePolicies(policies)); | ||
expect(violations.length).toBe(0); | ||
}); | ||
it('reporting on the wrong policy', () => { | ||
const policies = [ | ||
'script-src \'nonce-aaaaaaaaaa\' \'unsafe-inline\' http: https:', | ||
'block-all-mixed-content; report-uri url' | ||
]; | ||
const violations = | ||
lighthouseChecks.evaluateForWarnings(parsePolicies(policies)); | ||
expect(violations.length).toBe(0); | ||
}); | ||
it('missing unsafe-inline fallback split over two policies', () => { | ||
const policies = [ | ||
'script-src \'nonce-aaaaaaaaaa\'', | ||
'block-all-mixed-content; report-uri url' | ||
]; | ||
const violations = | ||
lighthouseChecks.evaluateForWarnings(parsePolicies(policies)); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].severity).toBe(Severity.STRICT_CSP); | ||
expect(violations[0].directive).toBe('script-src'); | ||
expect(violations[0].description) | ||
.toBe( | ||
'Consider adding \'unsafe-inline\' (ignored by browsers supporting nonces/hashes) to be backward compatible with older browsers.'); | ||
}); | ||
it('strict-dynamic with no fallback in any policy', () => { | ||
const policies = [ | ||
'script-src \'nonce-aaaaaaaaaa\' \'strict-dynamic\'', | ||
'block-all-mixed-content; report-uri url' | ||
]; | ||
const violations = | ||
lighthouseChecks.evaluateForWarnings(parsePolicies(policies)); | ||
expect(violations.length).toBe(2); | ||
expect(violations[0].severity).toBe(Severity.STRICT_CSP); | ||
expect(violations[0].directive).toBe('script-src'); | ||
expect(violations[0].description) | ||
.toBe( | ||
'Consider adding \'unsafe-inline\' (ignored by browsers supporting nonces/hashes) to be backward compatible with older browsers.'); | ||
expect(violations[1].severity).toBe(Severity.STRICT_CSP); | ||
expect(violations[1].directive).toBe('script-src'); | ||
expect(violations[1].description) | ||
.toBe( | ||
'Consider adding https: and http: url schemes (ignored by browsers supporting \'strict-dynamic\') to be backward compatible with older browsers.'); | ||
}); | ||
}); | ||
describe('Test evaluateForSyntaxErrors', () => { | ||
it('whenPerfectPolicies', () => { | ||
const policies = [ | ||
'script-src \'nonce-aaaaaaaaaa\' \'unsafe-inline\' http: https:', | ||
'block-all-mixed-content; report-uri url' | ||
]; | ||
const violations = | ||
lighthouseChecks.evaluateForSyntaxErrors(parsePolicies(policies)); | ||
expect(violations.length).toBe(2); | ||
expect(violations[0].length).toBe(0); | ||
expect(violations[1].length).toBe(0); | ||
}); | ||
it('whenShortNonce', () => { | ||
const test = 'script-src \'nonce-a\' \'unsafe-inline\'; report-uri url'; | ||
const violations = | ||
lighthouseChecks.evaluateForSyntaxErrors(parsePolicies([test])); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].length).toBe(1); | ||
expect(violations[0][0].severity).toBe(Severity.MEDIUM); | ||
expect(violations[0][0].directive).toBe('script-src'); | ||
expect(violations[0][0].description) | ||
.toBe('Nonces should be at least 8 characters long.'); | ||
}); | ||
it('whenUnknownDirective', () => { | ||
const test = | ||
'script-src \'nonce-aaaaaaaaa\' \'unsafe-inline\'; report-uri url; unknown'; | ||
const violations = | ||
lighthouseChecks.evaluateForSyntaxErrors(parsePolicies([test])); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].length).toBe(1); | ||
expect(violations[0][0].severity).toBe(Severity.SYNTAX); | ||
expect(violations[0][0].directive).toBe('unknown'); | ||
expect(violations[0][0].description) | ||
.toBe('Directive "unknown" is not a known CSP directive.'); | ||
}); | ||
it('whenDeprecatedDirective', () => { | ||
const test = | ||
'script-src \'nonce-aaaaaaaaa\' \'unsafe-inline\'; report-uri url; reflected-xss foo'; | ||
const violations = | ||
lighthouseChecks.evaluateForSyntaxErrors(parsePolicies([test])); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].length).toBe(1); | ||
expect(violations[0][0].severity).toBe(Severity.INFO); | ||
expect(violations[0][0].directive).toBe('reflected-xss'); | ||
expect(violations[0][0].description) | ||
.toBe( | ||
'reflected-xss is deprecated since CSP2. Please, use the X-XSS-Protection header instead.'); | ||
}); | ||
it('whenMissingSemicolon', () => { | ||
const test = | ||
'script-src \'nonce-aaaaaaaaa\' \'unsafe-inline\'; report-uri url object-src \'none\''; | ||
const violations = | ||
lighthouseChecks.evaluateForSyntaxErrors(parsePolicies([test])); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].length).toBe(1); | ||
expect(violations[0][0].severity).toBe(Severity.SYNTAX); | ||
expect(violations[0][0].directive).toBe('report-uri'); | ||
expect(violations[0][0].description) | ||
.toBe( | ||
'Did you forget the semicolon? "object-src" seems to be a directive, not a value.'); | ||
}); | ||
it('whenInvalidKeyword', () => { | ||
const test = | ||
'script-src \'nonce-aaaaaaaaa\' \'unsafe-inline\'; object-src \'invalid\''; | ||
const violations = | ||
lighthouseChecks.evaluateForSyntaxErrors(parsePolicies([test])); | ||
expect(violations.length).toBe(1); | ||
expect(violations[0].length).toBe(1); | ||
expect(violations[0][0].severity).toBe(Severity.SYNTAX); | ||
expect(violations[0][0].directive).toBe('object-src'); | ||
expect(violations[0][0].description) | ||
.toBe('\'invalid\' seems to be an invalid CSP keyword.'); | ||
}); | ||
it('manyPolicies', () => { | ||
const policies = [ | ||
'object-src \'invalid\'', 'script-src \'none\'', | ||
'script-src \'nonce-short\' default-src \'none\'' | ||
]; | ||
const violations = | ||
lighthouseChecks.evaluateForSyntaxErrors(parsePolicies(policies)); | ||
expect(violations.length).toBe(3); | ||
expect(violations[0].length).toBe(1); | ||
expect(violations[0][0].severity).toBe(Severity.SYNTAX); | ||
expect(violations[0][0].directive).toBe('object-src'); | ||
expect(violations[0][0].description) | ||
.toBe('\'invalid\' seems to be an invalid CSP keyword.'); | ||
expect(violations[1].length).toBe(0); | ||
expect(violations[2].length).toBe(2); | ||
expect(violations[2][0].severity).toBe(Severity.MEDIUM); | ||
expect(violations[2][0].directive).toBe('script-src'); | ||
expect(violations[2][0].description) | ||
.toBe('Nonces should be at least 8 characters long.'); | ||
expect(violations[2][1].severity).toBe(Severity.SYNTAX); | ||
expect(violations[2][1].directive).toBe('script-src'); | ||
expect(violations[2][1].description) | ||
.toBe( | ||
'Did you forget the semicolon? "default-src" seems to be a directive, not a value.'); | ||
}); | ||
}); |
@@ -8,3 +8,3 @@ /** | ||
import {checkInvalidKeyword, checkMissingSemicolon, checkUnknownDirective} from '../checks/parser_checks'; | ||
import {checkDeprecatedDirective, checkHasConfiguredReporting, checkMissingObjectSrcDirective, checkMissingScriptSrcDirective, checkMultipleMissingBaseUriDirective, checkNonceLength, checkScriptUnsafeInline} from '../checks/security_checks'; | ||
import {checkDeprecatedDirective, checkMissingObjectSrcDirective, checkMissingScriptSrcDirective, checkMultipleMissingBaseUriDirective, checkNonceLength, checkPlainUrlSchemes, checkScriptUnsafeInline, checkWildcards} from '../checks/security_checks'; | ||
import {checkAllowlistFallback, checkStrictDynamic, checkUnsafeInlineFallback} from '../checks/strictcsp_checks'; | ||
@@ -97,9 +97,12 @@ import {Csp, Directive, Version} from '../csp'; | ||
const effectiveCsps = | ||
parsedCsps.map(csp => csp.getEffectiveCsp(Version.CSP3)).filter(csp => { | ||
const directiveName = csp.getEffectiveDirective(Directive.SCRIPT_SRC); | ||
return csp.directives[directiveName]; | ||
}); | ||
parsedCsps.map(csp => csp.getEffectiveCsp(Version.CSP3)); | ||
const effectiveCspsWithScript = effectiveCsps.filter(csp => { | ||
const directiveName = csp.getEffectiveDirective(Directive.SCRIPT_SRC); | ||
return csp.directives[directiveName]; | ||
}); | ||
const robust = [ | ||
...atLeastOnePasses(effectiveCsps, checkStrictDynamic), | ||
...atLeastOnePasses(effectiveCsps, checkScriptUnsafeInline), | ||
...atLeastOnePasses(effectiveCspsWithScript, checkStrictDynamic), | ||
...atLeastOnePasses(effectiveCspsWithScript, checkScriptUnsafeInline), | ||
...atLeastOnePasses(effectiveCsps, checkWildcards), | ||
...atLeastOnePasses(effectiveCsps, checkPlainUrlSchemes), | ||
]; | ||
@@ -116,14 +119,9 @@ return [...targetsXssFindings, ...robust]; | ||
// Check #1 is implemented by Lighthouse directly | ||
// Check #2 | ||
const hasReportingFindings = | ||
atLeastOnePasses(parsedCsps, checkHasConfiguredReporting); | ||
// Check #2 is no longer used in Lighthouse. | ||
// Check #3 | ||
const compatibleWithNonCompliantBrowsersFindings = [ | ||
return [ | ||
...atLeastOneFails(parsedCsps, checkUnsafeInlineFallback), | ||
...atLeastOneFails(parsedCsps, checkAllowlistFallback) | ||
]; | ||
return [ | ||
...hasReportingFindings, ...compatibleWithNonCompliantBrowsersFindings | ||
]; | ||
} | ||
@@ -130,0 +128,0 @@ |
{ | ||
"name": "csp_evaluator", | ||
"version": "1.0.7", | ||
"version": "1.1.0", | ||
"description": "Evaluate Content Security Policies for a wide range of bypasses and weaknesses", | ||
@@ -5,0 +5,0 @@ "main": "dist/evaluator.js", |
@@ -52,10 +52,2 @@ # CSP Evaluator Core Library | ||
## Testing | ||
To run unit tests, run: | ||
```bash | ||
npm install && npm test | ||
``` | ||
## Example Usage | ||
@@ -62,0 +54,0 @@ |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
403165
6401
62