UNPKG

csp_evaluator

Version:

Evaluate Content Security Policies for a wide range of bypasses and weaknesses

327 lines 17.9 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.checkHasConfiguredReporting = exports.checkSrcHttp = exports.checkNonceLength = exports.checkDeprecatedDirective = exports.checkIpSource = exports.looksLikeIpAddress = exports.checkFlashObjectAllowlistBypass = exports.checkScriptAllowlistBypass = exports.checkMissingDirectives = exports.checkMultipleMissingBaseUriDirective = exports.checkMissingBaseUriDirective = exports.checkMissingScriptSrcDirective = exports.checkMissingObjectSrcDirective = exports.checkWildcards = exports.checkPlainUrlSchemes = exports.checkScriptUnsafeEval = exports.checkScriptUnsafeInline = exports.URL_SCHEMES_CAUSING_XSS = exports.DIRECTIVES_CAUSING_XSS = void 0; const angular = __importStar(require("../allowlist_bypasses/angular")); const flash = __importStar(require("../allowlist_bypasses/flash")); const jsonp = __importStar(require("../allowlist_bypasses/jsonp")); const csp = __importStar(require("../csp")); const csp_1 = require("../csp"); const finding_1 = require("../finding"); const utils = __importStar(require("../utils")); exports.DIRECTIVES_CAUSING_XSS = [ csp_1.Directive.SCRIPT_SRC, csp_1.Directive.SCRIPT_SRC_ATTR, csp_1.Directive.SCRIPT_SRC_ELEM, csp_1.Directive.OBJECT_SRC, csp_1.Directive.BASE_URI ]; exports.URL_SCHEMES_CAUSING_XSS = ['data:', 'http:', 'https:']; function checkScriptUnsafeInline(effectiveCsp) { const violations = []; const directivesToCheck = effectiveCsp.getEffectiveDirectives([ csp_1.Directive.SCRIPT_SRC, csp_1.Directive.SCRIPT_SRC_ATTR, csp_1.Directive.SCRIPT_SRC_ELEM ]); for (const directive of directivesToCheck) { const values = effectiveCsp.directives[directive] || []; if (values.includes(csp_1.Keyword.UNSAFE_INLINE)) { violations.push(new finding_1.Finding(finding_1.Type.SCRIPT_UNSAFE_INLINE, `'unsafe-inline' allows the execution of unsafe in-page scripts ` + 'and event handlers.', finding_1.Severity.HIGH, directive, csp_1.Keyword.UNSAFE_INLINE)); } if (values.includes(csp_1.Keyword.UNSAFE_HASHES)) { violations.push(new finding_1.Finding(finding_1.Type.SCRIPT_UNSAFE_HASHES, `'unsafe-hashes', while safer than 'unsafe-inline', allows the execution of unsafe in-page scripts and event handlers as long as their hashes appear in the CSP. Please refactor them to no longer use inline scripts if possible.`, finding_1.Severity.MEDIUM_MAYBE, directive, csp_1.Keyword.UNSAFE_HASHES)); } } return violations; } exports.checkScriptUnsafeInline = checkScriptUnsafeInline; function checkScriptUnsafeEval(parsedCsp) { const violations = []; const directivesToCheck = parsedCsp.getEffectiveDirectives([ csp_1.Directive.SCRIPT_SRC, csp_1.Directive.SCRIPT_SRC_ATTR, csp_1.Directive.SCRIPT_SRC_ELEM ]); for (const directive of directivesToCheck) { const values = parsedCsp.directives[directive] || []; if (values.includes(csp_1.Keyword.UNSAFE_EVAL)) { violations.push(new finding_1.Finding(finding_1.Type.SCRIPT_UNSAFE_EVAL, `'unsafe-eval' allows the execution of code injected into DOM APIs ` + 'such as eval().', finding_1.Severity.MEDIUM_MAYBE, directive, csp_1.Keyword.UNSAFE_EVAL)); } } return violations; } exports.checkScriptUnsafeEval = checkScriptUnsafeEval; function checkPlainUrlSchemes(parsedCsp) { const violations = []; const directivesToCheck = parsedCsp.getEffectiveDirectives(exports.DIRECTIVES_CAUSING_XSS); for (const directive of directivesToCheck) { const values = parsedCsp.directives[directive] || []; for (const value of values) { if (exports.URL_SCHEMES_CAUSING_XSS.includes(value)) { violations.push(new finding_1.Finding(finding_1.Type.PLAIN_URL_SCHEMES, value + ' URI in ' + directive + ' allows the execution of ' + 'unsafe scripts.', finding_1.Severity.HIGH, directive, value)); } } } return violations; } exports.checkPlainUrlSchemes = checkPlainUrlSchemes; function checkWildcards(parsedCsp) { const violations = []; const directivesToCheck = parsedCsp.getEffectiveDirectives(exports.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_1.Finding(finding_1.Type.PLAIN_WILDCARD, directive + ` should not allow '*' as source`, finding_1.Severity.HIGH, directive, value)); continue; } } } return violations; } exports.checkWildcards = checkWildcards; function checkMissingObjectSrcDirective(parsedCsp) { let objectRestrictions = []; if (csp_1.Directive.OBJECT_SRC in parsedCsp.directives) { objectRestrictions = parsedCsp.directives[csp_1.Directive.OBJECT_SRC]; } else if (csp_1.Directive.DEFAULT_SRC in parsedCsp.directives) { objectRestrictions = parsedCsp.directives[csp_1.Directive.DEFAULT_SRC]; } if (objectRestrictions !== undefined && objectRestrictions.length >= 1) { return []; } return [new finding_1.Finding(finding_1.Type.MISSING_DIRECTIVES, `Missing object-src allows the injection of plugins which can execute JavaScript. Can you set it to 'none'?`, finding_1.Severity.HIGH, csp_1.Directive.OBJECT_SRC)]; } exports.checkMissingObjectSrcDirective = checkMissingObjectSrcDirective; function checkMissingScriptSrcDirective(parsedCsp) { if (csp_1.Directive.SCRIPT_SRC in parsedCsp.directives || csp_1.Directive.DEFAULT_SRC in parsedCsp.directives) { return []; } return [new finding_1.Finding(finding_1.Type.MISSING_DIRECTIVES, 'script-src directive is missing.', finding_1.Severity.HIGH, csp_1.Directive.SCRIPT_SRC)]; } exports.checkMissingScriptSrcDirective = checkMissingScriptSrcDirective; function checkMissingBaseUriDirective(parsedCsp) { return checkMultipleMissingBaseUriDirective([parsedCsp]); } exports.checkMissingBaseUriDirective = checkMissingBaseUriDirective; function checkMultipleMissingBaseUriDirective(parsedCsps) { const needsBaseUri = (csp) => (csp.policyHasScriptNonces() || (csp.policyHasScriptHashes() && csp.policyHasStrictDynamic())); const hasBaseUri = (csp) => csp_1.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_1.Finding(finding_1.Type.MISSING_DIRECTIVES, description, finding_1.Severity.HIGH, csp_1.Directive.BASE_URI)]; } return []; } exports.checkMultipleMissingBaseUriDirective = checkMultipleMissingBaseUriDirective; function checkMissingDirectives(parsedCsp) { return [ ...checkMissingObjectSrcDirective(parsedCsp), ...checkMissingScriptSrcDirective(parsedCsp), ...checkMissingBaseUriDirective(parsedCsp), ]; } exports.checkMissingDirectives = checkMissingDirectives; function checkScriptAllowlistBypass(parsedCsp) { const violations = []; parsedCsp .getEffectiveDirectives([csp_1.Directive.SCRIPT_SRC, csp_1.Directive.SCRIPT_SRC_ELEM]) .forEach(effectiveScriptSrcDirective => { const scriptSrcValues = parsedCsp.directives[effectiveScriptSrcDirective] || []; if (scriptSrcValues.includes(csp_1.Keyword.NONE)) { return; } for (const value of scriptSrcValues) { if (value === csp_1.Keyword.SELF) { violations.push(new finding_1.Finding(finding_1.Type.SCRIPT_ALLOWLIST_BYPASS, `'self' can be problematic if you host JSONP, AngularJS or user ` + 'uploaded files.', finding_1.Severity.MEDIUM_MAYBE, effectiveScriptSrcDirective, value)); continue; } if (value.startsWith('\'')) { continue; } 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); if (jsonpBypass) { const evalRequired = jsonp.NEEDS_EVAL.includes(jsonpBypass.hostname); const evalPresent = scriptSrcValues.includes(csp_1.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_1.Finding(finding_1.Type.SCRIPT_ALLOWLIST_BYPASS, bypassDomain + ' is known to host' + bypassTxt + ' which allow to bypass this CSP.', finding_1.Severity.HIGH, effectiveScriptSrcDirective, value)); } else { violations.push(new finding_1.Finding(finding_1.Type.SCRIPT_ALLOWLIST_BYPASS, `No bypass found; make sure that this URL doesn't serve JSONP ` + 'replies or Angular libraries.', finding_1.Severity.MEDIUM_MAYBE, effectiveScriptSrcDirective, value)); } } }); return violations; } exports.checkScriptAllowlistBypass = checkScriptAllowlistBypass; function checkFlashObjectAllowlistBypass(parsedCsp) { const violations = []; const effectiveObjectSrcDirective = parsedCsp.getEffectiveDirective(csp_1.Directive.OBJECT_SRC); const objectSrcValues = parsedCsp.directives[effectiveObjectSrcDirective] || []; const pluginTypes = parsedCsp.directives[csp_1.Directive.PLUGIN_TYPES]; if (pluginTypes && !pluginTypes.includes('application/x-shockwave-flash')) { return []; } for (const value of objectSrcValues) { if (value === csp_1.Keyword.NONE) { return []; } const url = '//' + utils.getSchemeFreeUrl(value); const flashBypass = utils.matchWildcardUrls(url, flash.URLS); if (flashBypass) { violations.push(new finding_1.Finding(finding_1.Type.OBJECT_ALLOWLIST_BYPASS, flashBypass.hostname + ' is known to host Flash files which allow to bypass this CSP.', finding_1.Severity.HIGH, effectiveObjectSrcDirective, value)); } else if (effectiveObjectSrcDirective === csp_1.Directive.OBJECT_SRC) { violations.push(new finding_1.Finding(finding_1.Type.OBJECT_ALLOWLIST_BYPASS, `Can you restrict object-src to 'none' only?`, finding_1.Severity.MEDIUM_MAYBE, effectiveObjectSrcDirective, value)); } } return violations; } exports.checkFlashObjectAllowlistBypass = checkFlashObjectAllowlistBypass; function looksLikeIpAddress(maybeIp) { if (maybeIp.startsWith('[') && maybeIp.endsWith(']')) { return true; } if (/^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/.test(maybeIp)) { return true; } return false; } exports.looksLikeIpAddress = looksLikeIpAddress; function checkIpSource(parsedCsp) { const violations = []; const checkIp = (directive, directiveValues) => { for (const value of directiveValues) { const host = utils.getHostname(value); if (looksLikeIpAddress(host)) { if (host === '127.0.0.1') { violations.push(new finding_1.Finding(finding_1.Type.IP_SOURCE, directive + ' directive allows localhost as source. ' + 'Please make sure to remove this in production environments.', finding_1.Severity.INFO, directive, value)); } else { violations.push(new finding_1.Finding(finding_1.Type.IP_SOURCE, directive + ' directive has an IP-Address as source: ' + host + ' (will be ignored by browsers!). ', finding_1.Severity.INFO, directive, value)); } } } }; utils.applyCheckFunktionToDirectives(parsedCsp, checkIp); return violations; } exports.checkIpSource = checkIpSource; function checkDeprecatedDirective(parsedCsp) { const violations = []; if (csp_1.Directive.REFLECTED_XSS in parsedCsp.directives) { violations.push(new finding_1.Finding(finding_1.Type.DEPRECATED_DIRECTIVE, 'reflected-xss is deprecated since CSP2. ' + 'Please, use the X-XSS-Protection header instead.', finding_1.Severity.INFO, csp_1.Directive.REFLECTED_XSS)); } if (csp_1.Directive.REFERRER in parsedCsp.directives) { violations.push(new finding_1.Finding(finding_1.Type.DEPRECATED_DIRECTIVE, 'referrer is deprecated since CSP2. ' + 'Please, use the Referrer-Policy header instead.', finding_1.Severity.INFO, csp_1.Directive.REFERRER)); } if (csp_1.Directive.DISOWN_OPENER in parsedCsp.directives) { violations.push(new finding_1.Finding(finding_1.Type.DEPRECATED_DIRECTIVE, 'disown-opener is deprecated since CSP3. ' + 'Please, use the Cross Origin Opener Policy header instead.', finding_1.Severity.INFO, csp_1.Directive.DISOWN_OPENER)); } if (csp_1.Directive.PREFETCH_SRC in parsedCsp.directives) { violations.push(new finding_1.Finding(finding_1.Type.DEPRECATED_DIRECTIVE, 'prefetch-src is deprecated since CSP3. ' + 'Be aware that this feature may cease to work at any time.', finding_1.Severity.INFO, csp_1.Directive.PREFETCH_SRC)); } return violations; } exports.checkDeprecatedDirective = checkDeprecatedDirective; function checkNonceLength(parsedCsp) { const noncePattern = new RegExp('^\'nonce-(.+)\'$'); const violations = []; utils.applyCheckFunktionToDirectives(parsedCsp, (directive, directiveValues) => { for (const value of directiveValues) { const match = value.match(noncePattern); if (!match) { continue; } const nonceValue = match[1]; if (nonceValue.length < 8) { violations.push(new finding_1.Finding(finding_1.Type.NONCE_LENGTH, 'Nonces should be at least 8 characters long.', finding_1.Severity.MEDIUM, directive, value)); } if (!csp.isNonce(value, true)) { violations.push(new finding_1.Finding(finding_1.Type.NONCE_CHARSET, 'Nonces should only use the base64 charset.', finding_1.Severity.INFO, directive, value)); } } }); return violations; } exports.checkNonceLength = checkNonceLength; function checkSrcHttp(parsedCsp) { const violations = []; utils.applyCheckFunktionToDirectives(parsedCsp, (directive, directiveValues) => { for (const value of directiveValues) { const description = directive === csp_1.Directive.REPORT_URI ? 'Use HTTPS to send violation reports securely.' : 'Allow only resources downloaded over HTTPS.'; if (value.startsWith('http://')) { violations.push(new finding_1.Finding(finding_1.Type.SRC_HTTP, description, finding_1.Severity.MEDIUM, directive, value)); } } }); return violations; } exports.checkSrcHttp = checkSrcHttp; function checkHasConfiguredReporting(parsedCsp) { const reportUriValues = parsedCsp.directives[csp_1.Directive.REPORT_URI] || []; if (reportUriValues.length > 0) { return []; } const reportToValues = parsedCsp.directives[csp_1.Directive.REPORT_TO] || []; if (reportToValues.length > 0) { return [new finding_1.Finding(finding_1.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.`, finding_1.Severity.INFO, csp_1.Directive.REPORT_TO)]; } return [new finding_1.Finding(finding_1.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.', finding_1.Severity.INFO, csp_1.Directive.REPORT_URI)]; } exports.checkHasConfiguredReporting = checkHasConfiguredReporting; //# sourceMappingURL=security_checks.js.map