fix: prevent asset conflicts between React and Grid.js versions

Add coexistence checks to all enqueue methods to prevent loading
both React and Grid.js assets simultaneously.

Changes:
- ReactAdmin.php: Only enqueue React assets when ?react=1
- Init.php: Skip Grid.js when React active on admin pages
- Form.php, Coupon.php, Access.php: Restore classic assets when ?react=0
- Customer.php, Product.php, License.php: Add coexistence checks

Now the toggle between Classic and React versions works correctly.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
dwindown
2026-04-18 17:02:14 +07:00
parent bd9cdac02e
commit e8fbfb14c1
74973 changed files with 6658406 additions and 71 deletions

12
node_modules/csp_evaluator/checks/checker.ts generated vendored Normal file
View File

@@ -0,0 +1,12 @@
/**
* @fileoverview Shared interfaces for functions that check CSP policies.
*/
import {Csp} from '../csp';
import {Finding} from '../finding';
/**
* A function that checks a given Csp for problems and returns an unordered
* list of Findings.
*/
export type CheckerFunction = (csp: Csp) => Finding[];

156
node_modules/csp_evaluator/checks/parser_checks.ts generated vendored Normal file
View File

@@ -0,0 +1,156 @@
/**
* @fileoverview Collection of CSP parser checks which can be used to find
* common syntax mistakes like missing semicolons, invalid directives or
* invalid keywords.
* @author lwe@google.com (Lukas Weichselbaum)
*
* @license
* Copyright 2016 Google Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as csp from '../csp';
import {Csp, Keyword} from '../csp';
import {Finding, Severity, Type} from '../finding';
/**
* Checks if the csp contains invalid directives.
*
* Example policy where this check would trigger:
* foobar-src foo.bar
*
* @param parsedCsp A parsed csp.
*/
export function checkUnknownDirective(parsedCsp: Csp): Finding[] {
const findings: Finding[] = [];
for (const directive of Object.keys(parsedCsp.directives)) {
if (csp.isDirective(directive)) {
// Directive is known.
continue;
}
if (directive.endsWith(':')) {
findings.push(new Finding(
Type.UNKNOWN_DIRECTIVE, 'CSP directives don\'t end with a colon.',
Severity.SYNTAX, directive));
} else {
findings.push(new Finding(
Type.UNKNOWN_DIRECTIVE,
'Directive "' + directive + '" is not a known CSP directive.',
Severity.SYNTAX, directive));
}
}
return findings;
}
/**
* Checks if semicolons are missing in the csp.
*
* Example policy where this check would trigger (missing semicolon before
* start of object-src):
* script-src foo.bar object-src 'none'
*
* @param parsedCsp A parsed csp.
*/
export function checkMissingSemicolon(parsedCsp: Csp): Finding[] {
const findings: Finding[] = [];
for (const [directive, directiveValues] of Object.entries(
parsedCsp.directives)) {
if (directiveValues === undefined) {
continue;
}
for (const value of directiveValues) {
// If we find a known directive inside a directive value, it is very
// likely that a semicolon was forgoten.
if (csp.isDirective(value)) {
findings.push(new Finding(
Type.MISSING_SEMICOLON,
'Did you forget the semicolon? ' +
'"' + value + '" seems to be a directive, not a value.',
Severity.SYNTAX, directive, value));
}
}
}
return findings;
}
/**
* Checks if csp contains invalid keywords.
*
* Example policy where this check would trigger:
* script-src 'notAkeyword'
*
* @param parsedCsp A parsed csp.
*/
export function checkInvalidKeyword(parsedCsp: Csp): Finding[] {
const findings: Finding[] = [];
const keywordsNoTicks =
Object.values(Keyword).map((k) => k.replace(/'/g, ''));
for (const [directive, directiveValues] of Object.entries(
parsedCsp.directives)) {
if (directiveValues === undefined) {
continue;
}
for (const value of directiveValues) {
// Check if single ticks have been forgotten.
if (keywordsNoTicks.some((k) => k === value) ||
value.startsWith('nonce-') ||
value.match(/^(sha256|sha384|sha512)-/)) {
findings.push(new Finding(
Type.INVALID_KEYWORD,
'Did you forget to surround "' + value + '" with single-ticks?',
Severity.SYNTAX, directive, value));
continue;
}
// Continue, if the value doesn't start with single tick.
// All CSP keywords start with a single tick.
if (!value.startsWith('\'')) {
continue;
}
if (directive === csp.Directive.REQUIRE_TRUSTED_TYPES_FOR) {
// Continue, if it's an allowed Trusted Types sink.
if (value === csp.TrustedTypesSink.SCRIPT) {
continue;
}
} else if (directive === csp.Directive.TRUSTED_TYPES) {
// Continue, if it's an allowed Trusted Types keyword.
if (value === '\'allow-duplicates\'' || value === '\'none\'') {
continue;
}
} else {
// Continue, if it's a valid keyword.
if (csp.isKeyword(value) || csp.isHash(value) || csp.isNonce(value)) {
continue;
}
}
findings.push(new Finding(
Type.INVALID_KEYWORD, value + ' seems to be an invalid CSP keyword.',
Severity.SYNTAX, directive, value));
}
}
return findings;
}

View File

@@ -0,0 +1,94 @@
/**
* @license
* Copyright 2016 Google Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @fileoverview Tests for CSP Parser checks.
* @author lwe@google.com (Lukas Weichselbaum)
*/
import 'jasmine';
import {Finding, Severity} from '../finding';
import {CspParser} from '../parser';
import {CheckerFunction} from './checker';
import * as parserChecks from './parser_checks';
/**
* Runs 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 parser checks', () => {
/** Tests for csp.parserChecks.checkUnknownDirective */
it('CheckUnknownDirective', () => {
const test = 'foobar-src http:';
const violations = checkCsp(test, parserChecks.checkUnknownDirective);
expect(violations.length).toBe(1);
expect(violations[0].severity).toBe(Severity.SYNTAX);
expect(violations[0].directive).toBe('foobar-src');
});
/** Tests for csp.parserChecks.checkMissingSemicolon */
it('CheckMissingSemicolon', () => {
const test = 'default-src foo.bar script-src \'none\'';
const violations = checkCsp(test, parserChecks.checkMissingSemicolon);
expect(violations.length).toBe(1);
expect(violations[0].severity).toBe(Severity.SYNTAX);
expect(violations[0].value).toBe('script-src');
});
/** Tests for csp.parserChecks.checkInvalidKeyword */
it('CheckInvalidKeywordForgottenSingleTicks', () => {
const test = 'script-src strict-dynamic nonce-test sha256-asdf';
const violations = checkCsp(test, parserChecks.checkInvalidKeyword);
expect(violations.length).toBe(3);
expect(violations.every((v) => v.severity === Severity.SYNTAX)).toBeTrue();
expect(violations.every((v) => v.description.includes('single-ticks')))
.toBeTrue();
});
it('CheckInvalidKeywordUnknownKeyword', () => {
const test = 'script-src \'foo-bar\'';
const violations = checkCsp(test, parserChecks.checkInvalidKeyword);
expect(violations.length).toBe(1);
expect(violations[0].severity).toBe(Severity.SYNTAX);
expect(violations[0].value).toBe('\'foo-bar\'');
});
it('CheckInvalidKeywordAllowsRequireTrustedTypesForScript', () => {
const test = 'require-trusted-types-for \'script\'';
const violations = checkCsp(test, parserChecks.checkInvalidKeyword);
expect(violations.length).toBe(0);
});
it('CheckInvalidKeywordAllowsTrustedTypesAllowDuplicateKeyword', () => {
const test = 'trusted-types \'allow-duplicates\' policy1';
const violations = checkCsp(test, parserChecks.checkInvalidKeyword);
expect(violations.length).toBe(0);
});
});

578
node_modules/csp_evaluator/checks/security_checks.ts generated vendored Normal file
View File

@@ -0,0 +1,578 @@
/**
* @fileoverview Collection of CSP evaluation checks.
* @author lwe@google.com (Lukas Weichselbaum)
*
* @license
* Copyright 2016 Google Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as angular from '../allowlist_bypasses/angular';
import * as flash from '../allowlist_bypasses/flash';
import * as jsonp from '../allowlist_bypasses/jsonp';
import * as csp from '../csp';
import {Csp, Directive, Keyword} from '../csp';
import {Finding, Severity, Type} from '../finding';
import * as utils from '../utils';
/**
* A list of CSP directives that can allow XSS vulnerabilities if they fail
* validation.
*/
export const DIRECTIVES_CAUSING_XSS: Directive[] =
[Directive.SCRIPT_SRC, Directive.OBJECT_SRC, Directive.BASE_URI];
/**
* A list of URL schemes that can allow XSS vulnerabilities when requests to
* them are made.
*/
export const URL_SCHEMES_CAUSING_XSS: string[] = ['data:', 'http:', 'https:'];
/**
* Checks if passed csp allows inline scripts.
* Findings of this check are critical and FP free.
* unsafe-inline is ignored in the presence of a nonce or a hash. This check
* does not account for this and therefore the effectiveCsp needs to be passed.
*
* Example policy where this check would trigger:
* script-src 'unsafe-inline'
*
* @param effectiveCsp A parsed csp that only contains values which
* are active in a certain version of CSP (e.g. no unsafe-inline if a nonce
* is present).
*/
export function checkScriptUnsafeInline(effectiveCsp: Csp): Finding[] {
const directiveName =
effectiveCsp.getEffectiveDirective(Directive.SCRIPT_SRC);
const values: string[] = effectiveCsp.directives[directiveName] || [];
// Check if unsafe-inline is present.
if (values.includes(Keyword.UNSAFE_INLINE)) {
return [new Finding(
Type.SCRIPT_UNSAFE_INLINE,
`'unsafe-inline' allows the execution of unsafe in-page scripts ` +
'and event handlers.',
Severity.HIGH, directiveName, Keyword.UNSAFE_INLINE)];
}
return [];
}
/**
* Checks if passed csp allows eval in scripts.
* Findings of this check have a medium severity and are FP free.
*
* Example policy where this check would trigger:
* script-src 'unsafe-eval'
*
* @param parsedCsp Parsed CSP.
*/
export function checkScriptUnsafeEval(parsedCsp: Csp): Finding[] {
const directiveName = parsedCsp.getEffectiveDirective(Directive.SCRIPT_SRC);
const values: string[] = parsedCsp.directives[directiveName] || [];
// Check if unsafe-eval is present.
if (values.includes(Keyword.UNSAFE_EVAL)) {
return [new Finding(
Type.SCRIPT_UNSAFE_EVAL,
`'unsafe-eval' allows the execution of code injected into DOM APIs ` +
'such as eval().',
Severity.MEDIUM_MAYBE, directiveName, Keyword.UNSAFE_EVAL)];
}
return [];
}
/**
* Checks if plain URL schemes (e.g. http:) are allowed in sensitive directives.
* Findings of this check have a high severity and are FP free.
*
* Example policy where this check would trigger:
* script-src https: http: data:
*
* @param parsedCsp Parsed CSP.
*/
export function checkPlainUrlSchemes(parsedCsp: Csp): Finding[] {
const violations: Finding[] = [];
const directivesToCheck =
parsedCsp.getEffectiveDirectives(DIRECTIVES_CAUSING_XSS);
for (const directive of directivesToCheck) {
const values = parsedCsp.directives[directive] || [];
for (const value of values) {
if (URL_SCHEMES_CAUSING_XSS.includes(value)) {
violations.push(new Finding(
Type.PLAIN_URL_SCHEMES,
value + ' URI in ' + directive + ' allows the execution of ' +
'unsafe scripts.',
Severity.HIGH, directive, value));
}
}
}
return violations;
}
/**
* Checks if csp contains wildcards in sensitive directives.
* Findings of this check have a high severity and are FP free.
*
* Example policy where this check would trigger:
* script-src *
*
* @param parsedCsp Parsed CSP.
*/
export function checkWildcards(parsedCsp: Csp): Finding[] {
const violations: Finding[] = [];
const directivesToCheck =
parsedCsp.getEffectiveDirectives(DIRECTIVES_CAUSING_XSS);
for (const directive of directivesToCheck) {
const values = parsedCsp.directives[directive] || [];
for (const value of values) {
const url = utils.getSchemeFreeUrl(value);
if (url === '*') {
violations.push(new Finding(
Type.PLAIN_WILDCARD, directive + ` should not allow '*' as source`,
Severity.HIGH, directive, value));
continue;
}
}
}
return violations;
}
/**
* Checks if object-src is restricted to none either directly or via a
* default-src.
*/
export function checkMissingObjectSrcDirective(parsedCsp: Csp): Finding[] {
let objectRestrictions: string[]|undefined = [];
if (Directive.OBJECT_SRC in parsedCsp.directives) {
objectRestrictions = parsedCsp.directives[Directive.OBJECT_SRC];
} else if (Directive.DEFAULT_SRC in parsedCsp.directives) {
objectRestrictions = parsedCsp.directives[Directive.DEFAULT_SRC];
}
if (objectRestrictions !== undefined && objectRestrictions.length >= 1) {
return [];
}
return [new Finding(
Type.MISSING_DIRECTIVES,
`Missing object-src allows the injection of plugins which can execute JavaScript. Can you set it to 'none'?`,
Severity.HIGH, Directive.OBJECT_SRC)];
}
/**
* Checks if script-src is restricted either directly or via a default-src.
*/
export function checkMissingScriptSrcDirective(parsedCsp: Csp): Finding[] {
if (Directive.SCRIPT_SRC in parsedCsp.directives ||
Directive.DEFAULT_SRC in parsedCsp.directives) {
return [];
}
return [new Finding(
Type.MISSING_DIRECTIVES, 'script-src directive is missing.',
Severity.HIGH, Directive.SCRIPT_SRC)];
}
/**
* Checks if the base-uri needs to be restricted and if so, whether it has been
* restricted.
*/
export function checkMissingBaseUriDirective(parsedCsp: Csp): Finding[] {
return checkMultipleMissingBaseUriDirective([parsedCsp]);
}
/**
* Checks if the base-uri needs to be restricted and if so, whether it has been
* restricted.
*/
export function checkMultipleMissingBaseUriDirective(parsedCsps: Csp[]):
Finding[] {
// base-uri can be used to bypass nonce based CSPs and hash based CSPs that
// use strict dynamic
const needsBaseUri = (csp: Csp) =>
(csp.policyHasScriptNonces() ||
(csp.policyHasScriptHashes() && csp.policyHasStrictDynamic()));
const hasBaseUri = (csp: Csp) => Directive.BASE_URI in csp.directives;
if (parsedCsps.some(needsBaseUri) && !parsedCsps.some(hasBaseUri)) {
const description = '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'?`;
return [new Finding(
Type.MISSING_DIRECTIVES, description, Severity.HIGH,
Directive.BASE_URI)];
}
return [];
}
/**
* Checks if all necessary directives for preventing XSS are set.
* Findings of this check have a high severity and are FP free.
*
* Example policy where this check would trigger:
* script-src 'none'
*
* @param parsedCsp Parsed CSP.
*/
export function checkMissingDirectives(parsedCsp: Csp): Finding[] {
return [
...checkMissingObjectSrcDirective(parsedCsp),
...checkMissingScriptSrcDirective(parsedCsp),
...checkMissingBaseUriDirective(parsedCsp),
];
}
/**
* Checks if allowlisted origins are bypassable by JSONP/Angular endpoints.
* High severity findings of this check are FP free.
*
* Example policy where this check would trigger:
* default-src 'none'; script-src www.google.com
*
* @param parsedCsp Parsed CSP.
*/
export function checkScriptAllowlistBypass(parsedCsp: Csp): Finding[] {
const violations: Finding[] = [];
const effectiveScriptSrcDirective =
parsedCsp.getEffectiveDirective(Directive.SCRIPT_SRC);
const scriptSrcValues =
parsedCsp.directives[effectiveScriptSrcDirective] || [];
if (scriptSrcValues.includes(Keyword.NONE)) {
return violations;
}
for (const value of scriptSrcValues) {
if (value === Keyword.SELF) {
violations.push(new Finding(
Type.SCRIPT_ALLOWLIST_BYPASS,
`'self' can be problematic if you host JSONP, AngularJS or user ` +
'uploaded files.',
Severity.MEDIUM_MAYBE, effectiveScriptSrcDirective, value));
continue;
}
// Ignore keywords, nonces and hashes (they start with a single quote).
if (value.startsWith('\'')) {
continue;
}
// Ignore standalone schemes and things that don't look like URLs (no dot).
if (csp.isUrlScheme(value) || value.indexOf('.') === -1) {
continue;
}
const url = '//' + utils.getSchemeFreeUrl(value);
const angularBypass = utils.matchWildcardUrls(url, angular.URLS);
let jsonpBypass = utils.matchWildcardUrls(url, jsonp.URLS);
// Some JSONP bypasses only work in presence of unsafe-eval.
if (jsonpBypass) {
const evalRequired = jsonp.NEEDS_EVAL.includes(jsonpBypass.hostname);
const evalPresent = scriptSrcValues.includes(Keyword.UNSAFE_EVAL);
if (evalRequired && !evalPresent) {
jsonpBypass = null;
}
}
if (jsonpBypass || angularBypass) {
let bypassDomain = '';
let bypassTxt = '';
if (jsonpBypass) {
bypassDomain = jsonpBypass.hostname;
bypassTxt = ' JSONP endpoints';
}
if (angularBypass) {
bypassDomain = angularBypass.hostname;
bypassTxt += (bypassTxt.trim() === '') ? '' : ' and';
bypassTxt += ' Angular libraries';
}
violations.push(new Finding(
Type.SCRIPT_ALLOWLIST_BYPASS,
bypassDomain + ' is known to host' + bypassTxt +
' which allow to bypass this CSP.',
Severity.HIGH, effectiveScriptSrcDirective, value));
} else {
violations.push(new Finding(
Type.SCRIPT_ALLOWLIST_BYPASS,
`No bypass found; make sure that this URL doesn't serve JSONP ` +
'replies or Angular libraries.',
Severity.MEDIUM_MAYBE, effectiveScriptSrcDirective, value));
}
}
return violations;
}
/**
* Checks if allowlisted object-src origins are bypassable.
* Findings of this check have a high severity and are FP free.
*
* Example policy where this check would trigger:
* default-src 'none'; object-src ajax.googleapis.com
*
* @param parsedCsp Parsed CSP.
*/
export function checkFlashObjectAllowlistBypass(parsedCsp: Csp): Finding[] {
const violations = [];
const effectiveObjectSrcDirective =
parsedCsp.getEffectiveDirective(Directive.OBJECT_SRC);
const objectSrcValues =
parsedCsp.directives[effectiveObjectSrcDirective] || [];
// If flash is not allowed in plugin-types, continue.
const pluginTypes = parsedCsp.directives[Directive.PLUGIN_TYPES];
if (pluginTypes && !pluginTypes.includes('application/x-shockwave-flash')) {
return [];
}
for (const value of objectSrcValues) {
// Nothing to do here if 'none'.
if (value === Keyword.NONE) {
return [];
}
const url = '//' + utils.getSchemeFreeUrl(value);
const flashBypass = utils.matchWildcardUrls(url, flash.URLS);
if (flashBypass) {
violations.push(new Finding(
Type.OBJECT_ALLOWLIST_BYPASS,
flashBypass.hostname +
' is known to host Flash files which allow to bypass this CSP.',
Severity.HIGH, effectiveObjectSrcDirective, value));
} else if (effectiveObjectSrcDirective === Directive.OBJECT_SRC) {
violations.push(new Finding(
Type.OBJECT_ALLOWLIST_BYPASS,
`Can you restrict object-src to 'none' only?`, Severity.MEDIUM_MAYBE,
effectiveObjectSrcDirective, value));
}
}
return violations;
}
/**
* Returns whether the given string "looks" like an IP address. This function
* only uses basic heuristics and does not accept all valid IPs nor reject all
* invalid IPs.
*/
export function looksLikeIpAddress(maybeIp: string): boolean {
if (maybeIp.startsWith('[') && maybeIp.endsWith(']')) {
// Looks like an IPv6 address and not a hostname (though it may be some
// nonsense like `[foo]`)
return true;
}
if (/^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/.test(maybeIp)) {
// Looks like an IPv4 address (though it may be something like
// `500.600.700.800`
return true;
}
// Won't match IP addresses encoded in other manners (eg octal or
// decimal)
return false;
}
/**
* Checks if csp contains IP addresses.
* Findings of this check are informal only and are FP free.
*
* Example policy where this check would trigger:
* script-src 127.0.0.1
*
* @param parsedCsp Parsed CSP.
*/
export function checkIpSource(parsedCsp: Csp): Finding[] {
const violations: Finding[] = [];
// Function for checking if directive values contain IP addresses.
const checkIp = (directive: string, directiveValues: string[]) => {
for (const value of directiveValues) {
const host = utils.getHostname(value);
if (looksLikeIpAddress(host)) {
// Check if localhost.
// See 4.8 in https://www.w3.org/TR/CSP2/#match-source-expression
if (host === '127.0.0.1') {
violations.push(new Finding(
Type.IP_SOURCE,
directive + ' directive allows localhost as source. ' +
'Please make sure to remove this in production environments.',
Severity.INFO, directive, value));
} else {
violations.push(new Finding(
Type.IP_SOURCE,
directive + ' directive has an IP-Address as source: ' + host +
' (will be ignored by browsers!). ',
Severity.INFO, directive, value));
}
}
}
};
// Apply check to values of all directives.
utils.applyCheckFunktionToDirectives(parsedCsp, checkIp);
return violations;
}
/**
* Checks if csp contains directives that are deprecated in CSP3.
* Findings of this check are informal only and are FP free.
*
* Example policy where this check would trigger:
* report-uri foo.bar/csp
*
* @param parsedCsp Parsed CSP.
*/
export function checkDeprecatedDirective(parsedCsp: Csp): Finding[] {
const violations = [];
// More details: https://www.chromestatus.com/feature/5769374145183744
if (Directive.REFLECTED_XSS in parsedCsp.directives) {
violations.push(new Finding(
Type.DEPRECATED_DIRECTIVE,
'reflected-xss is deprecated since CSP2. ' +
'Please, use the X-XSS-Protection header instead.',
Severity.INFO, Directive.REFLECTED_XSS));
}
// More details: https://www.chromestatus.com/feature/5680800376815616
if (Directive.REFERRER in parsedCsp.directives) {
violations.push(new Finding(
Type.DEPRECATED_DIRECTIVE,
'referrer is deprecated since CSP2. ' +
'Please, use the Referrer-Policy header instead.',
Severity.INFO, Directive.REFERRER));
}
// More details: https://github.com/w3c/webappsec-csp/pull/327
if (Directive.DISOWN_OPENER in parsedCsp.directives) {
violations.push(new Finding(
Type.DEPRECATED_DIRECTIVE,
'disown-opener is deprecated since CSP3. ' +
'Please, use the Cross Origin Opener Policy header instead.',
Severity.INFO, Directive.DISOWN_OPENER));
}
return violations;
}
/**
* Checks if csp nonce is at least 8 characters long.
* Findings of this check are of medium severity and are FP free.
*
* Example policy where this check would trigger:
* script-src 'nonce-short'
*
* @param parsedCsp Parsed CSP.
*/
export function checkNonceLength(parsedCsp: Csp): Finding[] {
const noncePattern = new RegExp('^\'nonce-(.+)\'$');
const violations: Finding[] = [];
utils.applyCheckFunktionToDirectives(
parsedCsp, (directive, directiveValues) => {
for (const value of directiveValues) {
const match = value.match(noncePattern);
if (!match) {
continue;
}
// Not a nonce.
const nonceValue = match[1];
if (nonceValue.length < 8) {
violations.push(new Finding(
Type.NONCE_LENGTH,
'Nonces should be at least 8 characters long.', Severity.MEDIUM,
directive, value));
}
if (!csp.isNonce(value, true)) {
violations.push(new Finding(
Type.NONCE_CHARSET,
'Nonces should only use the base64 charset.', Severity.INFO,
directive, value));
}
}
});
return violations;
}
/**
* Checks if CSP allows sourcing from http://
* Findings of this check are of medium severity and are FP free.
*
* Example policy where this check would trigger:
* report-uri http://foo.bar/csp
*
* @param parsedCsp Parsed CSP.
*/
export function checkSrcHttp(parsedCsp: Csp): Finding[] {
const violations: Finding[] = [];
utils.applyCheckFunktionToDirectives(
parsedCsp, (directive, directiveValues) => {
for (const value of directiveValues) {
const description = directive === Directive.REPORT_URI ?
'Use HTTPS to send violation reports securely.' :
'Allow only resources downloaded over HTTPS.';
if (value.startsWith('http://')) {
violations.push(new Finding(
Type.SRC_HTTP, description, Severity.MEDIUM, directive, value));
}
}
});
return violations;
}
/**
* Checks if the policy has configured reporting in a robust manner.
*/
export function checkHasConfiguredReporting(parsedCsp: Csp): Finding[] {
const reportUriValues: string[] =
parsedCsp.directives[Directive.REPORT_URI] || [];
if (reportUriValues.length > 0) {
return [];
}
const reportToValues: string[] =
parsedCsp.directives[Directive.REPORT_TO] || [];
if (reportToValues.length > 0) {
return [new Finding(
Type.REPORT_TO_ONLY,
`This CSP policy only provides a reporting destination via the 'report-to' directive. This directive is only supported in Chromium-based browsers so it is recommended to also use a 'report-uri' directive.`,
Severity.INFO, Directive.REPORT_TO)];
}
return [new Finding(
Type.REPORTING_DESTINATION_MISSING,
'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.',
Severity.INFO, Directive.REPORT_URI)];
}

View File

@@ -0,0 +1,468 @@
/**
* @license
* Copyright 2016 Google Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @fileoverview Tests for CSP Evaluator Checks.
* @author lwe@google.com (Lukas Weichselbaum)
*/
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);
});
});

179
node_modules/csp_evaluator/checks/strictcsp_checks.ts generated vendored Normal file
View File

@@ -0,0 +1,179 @@
/**
* @fileoverview Collection of "strict" CSP and backward compatibility checks.
* A "strict" CSP is based on nonces or hashes and drops the allowlist.
* These checks ensure that 'strict-dynamic' and a CSP nonce/hash are present.
* Due to 'strict-dynamic' any allowlist will get dropped in CSP3.
* The backward compatibility checks ensure that the strict nonce/hash based CSP
* will be a no-op in older browsers by checking for presence of 'unsafe-inline'
* (will be dropped in newer browsers if a nonce or hash is present) and for
* prsensence of http: and https: url schemes (will be droped in the presence of
* 'strict-dynamic' in newer browsers).
*
* @author lwe@google.com (Lukas Weichselbaum)
*
* @license
* Copyright 2016 Google Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as csp from '../csp';
import {Csp, Keyword} from '../csp';
import {Finding, Severity, Type} from '../finding';
/**
* Checks if 'strict-dynamic' is present.
*
* Example policy where this check would trigger:
* script-src foo.bar
*
* @param parsedCsp A parsed csp.
*/
export function checkStrictDynamic(parsedCsp: Csp): Finding[] {
const directiveName =
parsedCsp.getEffectiveDirective(csp.Directive.SCRIPT_SRC);
const values: string[] = parsedCsp.directives[directiveName] || [];
const schemeOrHostPresent = values.some((v) => !v.startsWith('\''));
// Check if strict-dynamic is present in case a host/scheme allowlist is used.
if (schemeOrHostPresent && !values.includes(Keyword.STRICT_DYNAMIC)) {
return [new Finding(
Type.STRICT_DYNAMIC,
'Host allowlists can frequently be bypassed. Consider using ' +
'\'strict-dynamic\' in combination with CSP nonces or hashes.',
Severity.STRICT_CSP, directiveName)];
}
return [];
}
/**
* Checks if 'strict-dynamic' is only used together with a nonce or a hash.
*
* Example policy where this check would trigger:
* script-src 'strict-dynamic'
*
* @param parsedCsp A parsed csp.
*/
export function checkStrictDynamicNotStandalone(parsedCsp: Csp): Finding[] {
const directiveName =
parsedCsp.getEffectiveDirective(csp.Directive.SCRIPT_SRC);
const values: string[] = parsedCsp.directives[directiveName] || [];
if (values.includes(Keyword.STRICT_DYNAMIC) &&
(!parsedCsp.policyHasScriptNonces() &&
!parsedCsp.policyHasScriptHashes())) {
return [new Finding(
Type.STRICT_DYNAMIC_NOT_STANDALONE,
'\'strict-dynamic\' without a CSP nonce/hash will block all scripts.',
Severity.INFO, directiveName)];
}
return [];
}
/**
* Checks if the policy has 'unsafe-inline' when a nonce or hash are present.
* This will ensure backward compatibility to browser that don't support
* CSP nonces or hasehs.
*
* Example policy where this check would trigger:
* script-src 'nonce-test'
*
* @param parsedCsp A parsed csp.
*/
export function checkUnsafeInlineFallback(parsedCsp: Csp): Finding[] {
if (!parsedCsp.policyHasScriptNonces() &&
!parsedCsp.policyHasScriptHashes()) {
return [];
}
const directiveName =
parsedCsp.getEffectiveDirective(csp.Directive.SCRIPT_SRC);
const values: string[] = parsedCsp.directives[directiveName] || [];
if (!values.includes(Keyword.UNSAFE_INLINE)) {
return [new Finding(
Type.UNSAFE_INLINE_FALLBACK,
'Consider adding \'unsafe-inline\' (ignored by browsers supporting ' +
'nonces/hashes) to be backward compatible with older browsers.',
Severity.STRICT_CSP, directiveName)];
}
return [];
}
/**
* Checks if the policy has an allowlist fallback (* or http: and https:) when
* 'strict-dynamic' is present.
* This will ensure backward compatibility to browser that don't support
* 'strict-dynamic'.
*
* Example policy where this check would trigger:
* script-src 'nonce-test' 'strict-dynamic'
*
* @param parsedCsp A parsed csp.
*/
export function checkAllowlistFallback(parsedCsp: Csp): Finding[] {
const directiveName =
parsedCsp.getEffectiveDirective(csp.Directive.SCRIPT_SRC);
const values: string[] = parsedCsp.directives[directiveName] || [];
if (!values.includes(Keyword.STRICT_DYNAMIC)) {
return [];
}
// Check if there's already an allowlist (url scheme or url)
if (!values.some(
(v) => ['http:', 'https:', '*'].includes(v) || v.includes('.'))) {
return [new Finding(
Type.ALLOWLIST_FALLBACK,
'Consider adding https: and http: url schemes (ignored by browsers ' +
'supporting \'strict-dynamic\') to be backward compatible with older ' +
'browsers.',
Severity.STRICT_CSP, directiveName)];
}
return [];
}
/**
* Checks if the policy requires Trusted Types for scripts.
*
* I.e. the policy should have the following dirctive:
* require-trusted-types-for 'script'
*
* @param parsedCsp A parsed csp.
*/
export function checkRequiresTrustedTypesForScripts(parsedCsp: Csp): Finding[] {
const directiveName =
parsedCsp.getEffectiveDirective(csp.Directive.REQUIRE_TRUSTED_TYPES_FOR);
const values: string[] = parsedCsp.directives[directiveName] || [];
if (!values.includes(csp.TrustedTypesSink.SCRIPT)) {
return [new Finding(
Type.REQUIRE_TRUSTED_TYPES_FOR_SCRIPTS,
'Consider requiring Trusted Types for scripts to lock down DOM XSS ' +
'injection sinks. You can do this by adding ' +
'"require-trusted-types-for \'script\'" to your policy.',
Severity.INFO, csp.Directive.REQUIRE_TRUSTED_TYPES_FOR)];
}
return [];
}

View File

@@ -0,0 +1,112 @@
/**
* @license
* Copyright 2016 Google Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @fileoverview Tests for strict CSP checks.
* @author lwe@google.com (Lukas Weichselbaum)
*/
import {Finding, Severity} from '../finding';
import {CspParser} from '../parser';
import {CheckerFunction} from './checker';
import * as strictcspChecks from './strictcsp_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 strictcsp checks', () => {
/** Tests for csp.strictcspChecks.checkStrictDynamic */
it('CheckStrictDynamic', () => {
const test = 'script-src foo.bar';
const violations = checkCsp(test, strictcspChecks.checkStrictDynamic);
expect(violations.length).toBe(1);
expect(violations[0].severity).toBe(Severity.STRICT_CSP);
});
/** Tests for csp.strictcspChecks.checkStrictDynamicNotStandalone */
it('CheckStrictDynamicNotStandalone', () => {
const test = 'script-src \'strict-dynamic\'';
const violations =
checkCsp(test, strictcspChecks.checkStrictDynamicNotStandalone);
expect(violations[0].severity).toBe(Severity.INFO);
});
it('CheckStrictDynamicNotStandaloneDoesntFireIfNoncePresent', () => {
const test = 'script-src \'strict-dynamic\' \'nonce-foobar\'';
const violations =
checkCsp(test, strictcspChecks.checkStrictDynamicNotStandalone);
expect(violations.length).toBe(0);
});
/** Tests for csp.strictcspChecks.checkUnsafeInlineFallback */
it('CheckUnsafeInlineFallback', () => {
const test = 'script-src \'nonce-test\'';
const violations =
checkCsp(test, strictcspChecks.checkUnsafeInlineFallback);
expect(violations.length).toBe(1);
expect(violations[0].severity).toBe(Severity.STRICT_CSP);
});
it('CheckUnsafeInlineFallbackDoesntFireIfFallbackPresent', () => {
const test = 'script-src \'nonce-test\' \'unsafe-inline\'';
const violations =
checkCsp(test, strictcspChecks.checkUnsafeInlineFallback);
expect(violations.length).toBe(0);
});
/** Tests for csp.strictcspChecks.checkAllowlistFallback */
it('checkAllowlistFallback', () => {
const test = 'script-src \'nonce-test\' \'strict-dynamic\'';
const violations = checkCsp(test, strictcspChecks.checkAllowlistFallback);
expect(violations.length).toBe(1);
expect(violations[0].severity).toBe(Severity.STRICT_CSP);
});
it('checkAllowlistFallbackDoesntFireIfSchemeFallbackPresent', () => {
const test = 'script-src \'nonce-test\' \'strict-dynamic\' https:';
const violations = checkCsp(test, strictcspChecks.checkAllowlistFallback);
expect(violations.length).toBe(0);
});
it('checkAllowlistFallbackDoesntFireIfURLFallbackPresent', () => {
const test = 'script-src \'nonce-test\' \'strict-dynamic\' foo.bar';
const violations = checkCsp(test, strictcspChecks.checkAllowlistFallback);
expect(violations.length).toBe(0);
});
it('checkAllowlistFallbackDoesntFireInAbsenceOfStrictDynamic', () => {
const test = 'script-src \'nonce-test\'';
const violations = checkCsp(test, strictcspChecks.checkAllowlistFallback);
expect(violations.length).toBe(0);
});
});