csp_evaluator
Version:
Evaluate Content Security Policies for a wide range of bypasses and weaknesses
444 lines (389 loc) • 14.1 kB
text/typescript
/**
* @fileoverview CSP definitions and helper functions.
* @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 {Finding, Severity, Type} from './finding';
/**
* Content Security Policy object.
* List of valid CSP directives:
* - http://www.w3.org/TR/CSP2/#directives
* - https://www.w3.org/TR/upgrade-insecure-requests/
*/
export class Csp {
directives: Record<string, string[]|undefined> = {};
/**
* Creates a CSP object from a list of directives.
* @param directives CSP directives.
*/
constructor(directives: Record<string, string[]|undefined> = {}) {
for (const [directive, directiveValues] of Object.entries(directives)) {
if (directiveValues) {
this.directives[directive] = [...directiveValues];
}
}
}
/**
* Clones a CSP object.
* @return clone of parsedCsp.
*/
clone(): Csp {
// Use the constructor that takes in directives to create a deep copy.
return new Csp(this.directives);
}
/**
* Converts this CSP back into a string.
* @return CSP string.
*/
convertToString(): string {
let cspString = '';
for (const [directive, directiveValues] of Object.entries(
this.directives)) {
cspString += directive;
if (directiveValues !== undefined) {
for (let value, i = 0; (value = directiveValues[i]); i++) {
cspString += ' ';
cspString += value;
}
}
cspString += '; ';
}
return cspString;
}
/**
* Returns CSP as it would be seen by a UA supporting a specific CSP version.
* @param cspVersion CSP.
* @param optFindings findings about ignored directive values will be added
* to this array, if passed. (e.g. CSP2 ignores 'unsafe-inline' in
* presence of a nonce or a hash)
* @return The effective CSP.
*/
getEffectiveCsp(cspVersion: Version, optFindings?: Finding[]): Csp {
const findings = optFindings || [];
const effectiveCsp = this.clone();
[Directive.SCRIPT_SRC, Directive.SCRIPT_SRC_ATTR, Directive.SCRIPT_SRC_ELEM]
.forEach(directiveToNormalize => {
const directive = effectiveCsp.getEffectiveDirective(
directiveToNormalize) as Directive;
const values = this.directives[directive] || [];
const effectiveCspValues = effectiveCsp.directives[directive];
if (effectiveCspValues &&
(effectiveCsp.policyHasScriptNonces(directive) ||
effectiveCsp.policyHasScriptHashes(directive))) {
if (cspVersion >= Version.CSP2) {
// Ignore 'unsafe-inline' in CSP >= v2, if a nonce or a hash is
// present.
if (values.includes(Keyword.UNSAFE_INLINE)) {
arrayRemove(effectiveCspValues, Keyword.UNSAFE_INLINE);
findings.push(new Finding(
Type.IGNORED,
'unsafe-inline is ignored if a nonce or a hash is present. ' +
'(CSP2 and above)',
Severity.NONE, directive, Keyword.UNSAFE_INLINE));
}
} else {
// remove nonces and hashes (not supported in CSP < v2).
for (const value of values) {
if (value.startsWith('\'nonce-') || value.startsWith('\'sha')) {
arrayRemove(effectiveCspValues, value);
}
}
}
}
if (effectiveCspValues && this.policyHasStrictDynamic(directive)) {
// Ignore allowlist in CSP >= v3 in presence of 'strict-dynamic'.
if (cspVersion >= Version.CSP3) {
for (const value of values) {
// Because of 'strict-dynamic' all host-source and scheme-source
// expressions, as well as the "'unsafe-inline'" and "'self'
// keyword-sources will be ignored.
// https://w3c.github.io/webappsec-csp/#strict-dynamic-usage
if (!value.startsWith('\'') || value === Keyword.SELF ||
value === Keyword.UNSAFE_INLINE) {
arrayRemove(effectiveCspValues, value);
findings.push(new Finding(
Type.IGNORED,
'Because of strict-dynamic this entry is ignored in CSP3 and above',
Severity.NONE, directive, value));
}
}
} else {
// strict-dynamic not supported.
arrayRemove(effectiveCspValues, Keyword.STRICT_DYNAMIC);
}
}
});
if (cspVersion < Version.CSP3) {
// Remove CSP3 directives from pre-CSP3 policies.
// https://w3c.github.io/webappsec-csp/#changes-from-level-2
delete effectiveCsp.directives[Directive.REPORT_TO];
delete effectiveCsp.directives[Directive.WORKER_SRC];
delete effectiveCsp.directives[Directive.MANIFEST_SRC];
delete effectiveCsp.directives[Directive.TRUSTED_TYPES];
delete effectiveCsp.directives[Directive.REQUIRE_TRUSTED_TYPES_FOR];
delete effectiveCsp.directives[Directive.SCRIPT_SRC_ATTR];
delete effectiveCsp.directives[Directive.SCRIPT_SRC_ELEM];
delete effectiveCsp.directives[Directive.STYLE_SRC_ATTR];
delete effectiveCsp.directives[Directive.STYLE_SRC_ELEM];
}
return effectiveCsp;
}
/**
* Returns default-src if directive is a fetch directive and is not present in
* this CSP. Otherwise the provided directive is returned.
* @param directive CSP.
* @return The effective directive.
*/
getEffectiveDirective(directive: string): string {
if (directive in this.directives) {
return directive;
}
if ((directive === Directive.SCRIPT_SRC_ATTR ||
directive === Directive.SCRIPT_SRC_ELEM) &&
Directive.SCRIPT_SRC in this.directives) {
return Directive.SCRIPT_SRC;
}
if ((directive === Directive.STYLE_SRC_ATTR ||
directive === Directive.STYLE_SRC_ELEM) &&
Directive.STYLE_SRC in this.directives) {
return Directive.STYLE_SRC;
}
// Only fetch directives default to default-src.
if (FETCH_DIRECTIVES.includes(directive as Directive)) {
return Directive.DEFAULT_SRC;
}
return directive;
}
/**
* Returns the passed directives if present in this CSP or default-src
* otherwise.
* @param directives CSP.
* @return The effective directives.
*/
getEffectiveDirectives(directives: string[]): string[] {
const effectiveDirectives =
new Set(directives.map((val) => this.getEffectiveDirective(val)));
return [...effectiveDirectives];
}
/**
* Checks if this CSP is using nonces for scripts.
* @return true, if this CSP is using script nonces.
*/
policyHasScriptNonces(directive?: Directive): boolean {
const directiveName =
this.getEffectiveDirective(directive || Directive.SCRIPT_SRC);
const values = this.directives[directiveName] || [];
return values.some((val) => isNonce(val));
}
/**
* Checks if this CSP is using hashes for scripts.
* @return true, if this CSP is using script hashes.
*/
policyHasScriptHashes(directive?: Directive): boolean {
const directiveName =
this.getEffectiveDirective(directive || Directive.SCRIPT_SRC);
const values = this.directives[directiveName] || [];
return values.some((val) => isHash(val));
}
/**
* Checks if this CSP is using strict-dynamic.
* @return true, if this CSP is using CSP nonces.
*/
policyHasStrictDynamic(directive?: Directive): boolean {
const directiveName =
this.getEffectiveDirective(directive || Directive.SCRIPT_SRC);
const values = this.directives[directiveName] || [];
return values.includes(Keyword.STRICT_DYNAMIC);
}
}
/**
* CSP directive source keywords.
*/
export enum Keyword {
SELF = '\'self\'',
NONE = '\'none\'',
UNSAFE_INLINE = '\'unsafe-inline\'',
UNSAFE_EVAL = '\'unsafe-eval\'',
WASM_EVAL = '\'wasm-eval\'',
WASM_UNSAFE_EVAL = '\'wasm-unsafe-eval\'',
STRICT_DYNAMIC = '\'strict-dynamic\'',
UNSAFE_HASHED_ATTRIBUTES = '\'unsafe-hashed-attributes\'',
UNSAFE_HASHES = '\'unsafe-hashes\'',
REPORT_SAMPLE = '\'report-sample\'',
BLOCK = '\'block\'',
ALLOW = '\'allow\'',
INLINE_SPECULATION_RULES = '\'inline-speculation-rules\'',
}
/**
* CSP directive source keywords.
*/
export enum TrustedTypesSink {
SCRIPT = '\'script\'',
}
/**
* CSP v3 directives.
* List of valid CSP directives:
* - http://www.w3.org/TR/CSP2/#directives
* - https://www.w3.org/TR/upgrade-insecure-requests/
*
*/
export enum Directive {
// Fetch directives
CHILD_SRC = 'child-src',
CONNECT_SRC = 'connect-src',
DEFAULT_SRC = 'default-src',
FONT_SRC = 'font-src',
FRAME_SRC = 'frame-src',
IMG_SRC = 'img-src',
MEDIA_SRC = 'media-src',
OBJECT_SRC = 'object-src',
SCRIPT_SRC = 'script-src',
SCRIPT_SRC_ATTR = 'script-src-attr',
SCRIPT_SRC_ELEM = 'script-src-elem',
STYLE_SRC = 'style-src',
STYLE_SRC_ATTR = 'style-src-attr',
STYLE_SRC_ELEM = 'style-src-elem',
PREFETCH_SRC = 'prefetch-src',
MANIFEST_SRC = 'manifest-src',
WORKER_SRC = 'worker-src',
// Document directives
BASE_URI = 'base-uri',
PLUGIN_TYPES = 'plugin-types',
SANDBOX = 'sandbox',
DISOWN_OPENER = 'disown-opener',
// Navigation directives
FORM_ACTION = 'form-action',
FRAME_ANCESTORS = 'frame-ancestors',
NAVIGATE_TO = 'navigate-to',
// Reporting directives
REPORT_TO = 'report-to',
REPORT_URI = 'report-uri',
// Other directives
BLOCK_ALL_MIXED_CONTENT = 'block-all-mixed-content',
UPGRADE_INSECURE_REQUESTS = 'upgrade-insecure-requests',
REFLECTED_XSS = 'reflected-xss',
REFERRER = 'referrer',
REQUIRE_SRI_FOR = 'require-sri-for',
TRUSTED_TYPES = 'trusted-types',
// https://github.com/WICG/trusted-types
REQUIRE_TRUSTED_TYPES_FOR = 'require-trusted-types-for',
WEBRTC = 'webrtc',
}
/**
* CSP v3 fetch directives.
* Fetch directives control the locations from which resources may be loaded.
* https://w3c.github.io/webappsec-csp/#directives-fetch
*
*/
export const FETCH_DIRECTIVES: Directive[] = [
Directive.CHILD_SRC, Directive.CONNECT_SRC, Directive.DEFAULT_SRC,
Directive.FONT_SRC, Directive.FRAME_SRC, Directive.IMG_SRC,
Directive.MANIFEST_SRC, Directive.MEDIA_SRC, Directive.OBJECT_SRC,
Directive.SCRIPT_SRC, Directive.SCRIPT_SRC_ATTR, Directive.SCRIPT_SRC_ELEM,
Directive.STYLE_SRC, Directive.STYLE_SRC_ATTR, Directive.STYLE_SRC_ELEM,
Directive.WORKER_SRC
];
/**
* CSP version.
*/
export enum Version {
CSP1 = 1,
CSP2,
CSP3
}
/**
* Checks if a string is a valid CSP directive.
* @param directive value to check.
* @return True if directive is a valid CSP directive.
*/
export function isDirective(directive: string): boolean {
return Object.values(Directive).includes(directive as Directive);
}
/**
* Checks if a string is a valid CSP keyword.
* @param keyword value to check.
* @return True if keyword is a valid CSP keyword.
*/
export function isKeyword(keyword: string): boolean {
return Object.values(Keyword).includes(keyword as Keyword);
}
/**
* Checks if a string is a valid URL scheme.
* Scheme part + ":"
* For scheme part see https://tools.ietf.org/html/rfc3986#section-3.1
* @param urlScheme value to check.
* @return True if urlScheme has a valid scheme.
*/
export function isUrlScheme(urlScheme: string): boolean {
const pattern = new RegExp('^[a-zA-Z][+a-zA-Z0-9.-]*:$');
return pattern.test(urlScheme);
}
/**
* A regex pattern to check nonce prefix and Base64 formatting of a nonce value.
*/
export const STRICT_NONCE_PATTERN =
new RegExp('^\'nonce-[a-zA-Z0-9+/_-]+[=]{0,2}\'$');
/** A regex pattern for checking if nonce prefix. */
export const NONCE_PATTERN = new RegExp('^\'nonce-(.+)\'$');
/**
* Checks if a string is a valid CSP nonce.
* See http://www.w3.org/TR/CSP2/#nonce_value
* @param nonce value to check.
* @param strictCheck Check if the nonce uses the base64 charset.
* @return True if nonce is has a valid CSP nonce.
*/
export function isNonce(nonce: string, strictCheck?: boolean): boolean {
const pattern = strictCheck ? STRICT_NONCE_PATTERN : NONCE_PATTERN;
return pattern.test(nonce);
}
/**
* A regex pattern to check hash prefix and Base64 formatting of a hash value.
*/
export const STRICT_HASH_PATTERN =
new RegExp('^\'(sha256|sha384|sha512)-[a-zA-Z0-9+/]+[=]{0,2}\'$');
/** A regex pattern to check hash prefix. */
export const HASH_PATTERN = new RegExp('^\'(sha256|sha384|sha512)-(.+)\'$');
/**
* Checks if a string is a valid CSP hash.
* See http://www.w3.org/TR/CSP2/#hash_value
* @param hash value to check.
* @param strictCheck Check if the hash uses the base64 charset.
* @return True if hash is has a valid CSP hash.
*/
export function isHash(hash: string, strictCheck?: boolean): boolean {
const pattern = strictCheck ? STRICT_HASH_PATTERN : HASH_PATTERN;
return pattern.test(hash);
}
/**
* Class to represent all generic CSP errors.
*/
export class CspError extends Error {
/**
* @param message An optional error message.
*/
constructor(message?: string) {
super(message);
}
}
/**
* Mutate the given array to remove the first instance of the given item
*/
function arrayRemove<T>(arr: T[], item: T): void {
if (arr.includes(item)) {
const idx = arr.findIndex(elem => item === elem);
arr.splice(idx, 1);
}
}