Skip to content

Instantly share code, notes, and snippets.

@oreoshake
Created November 12, 2013 16:47
Show Gist options
  • Save oreoshake/7434316 to your computer and use it in GitHub Desktop.
Save oreoshake/7434316 to your computer and use it in GitHub Desktop.
This was meant to be a CSP parser/validator with the ability to explain a policy and a violation report. It has support for the old school firefox headers and the standard header.
policy
= directive (" "? ";" " "? directive?)*
directive
= report_uri_directive / declaritive_directive
report_uri_directive
= "report-uri " host_source? [a-zA-Z/_\-.]*
declaritive_directive
= name:directive_name " " sources:source_list {
var winston = require("winston");
// inline/eval values are only valid in style/script/default blocks
if(name === 'options') {
for(var i=0; i < sources.length; i++) {
var s = sources[i][0];
if(s !== "inline-script" && s !== "eval-script") {
winston.error("Invalid value in 'options' directive: " + s);
return null;
}
}
} else if(name === 'style-src') {
if(sources.indexOf('inline-script') > -1) {
winston.warn("inline-script is not honored in the style-src directive (FF bug)");
}
}
// verify inline-script/eval is in the right place
for(var i=0; i < sources.length; i++) {
var s = sources[i][0];
if(name !== 'options' && (s === 'inline-script' || s === 'eval-script')) {
winston.error(s + " is not allowed in the " + name + " directive, it only works in the 'options' directive");
return null;
}
}
return name + " " + sources;
}
directive_name
= "default-src" / "allow" / "options" / "script-src" / "object-src" / "style-src" / "img-src" / "media-src" / "frame-src" / "font-src" / "xhr-src" / "frame-ancestors" / "form-action"
source_list
= "'none'" / (source_expression [ ]?)*
source_expression
= keyword_source / scheme_source / host_source
host_source
= (scheme "://" / "/")? host (port)?
scheme_source
= scheme_only ":"
keyword_source
= "'self'" / "inline-script" / "eval-script"
scheme_only
= "data" / "javascript" / "blob" / "about"
scheme
= "https" / "http" / "ws"
host
= ("*.")? host_char+ ("." host_char+)* / "*"
host_char
= [a-zA-Z] / [0-9] / '-'
port
= (":" [0-9]+) / ":*"
'use strict';
var winston = require('winston');
var path = require('path');
var fs = require('fs');
var PEG = require("pegjs");
var csp_util = {};
var _loadParser = function(spec) {
winston.debug("loading and parsing spec from " + spec);
var grammar = fs.readFileSync(path.join(__dirname, '..', 'parsers', spec + '.spec')).toString();
return PEG.buildParser(grammar, {'trackLineAndColumn':true});
};
csp_util.printError = function(policy, error) {
winston.error(error.message);
if(policy.length < 80) {
winston.error(policy);
var pointer = "";
for(var i=0; i < error.offset; ++i) {
pointer += " ";
}
pointer += "^";
winston.error(pointer);
} else {
winston.error(policy.slice(0, error.offset) + '\u001b[31m<<<<\u001b[0m' + policy[error.offset] + '\u001b[31m>>>>\u001b[0m' + policy.slice(error.offset + 1));
}
};
csp_util.generateParser = function(parser) {
var grammar = parser;
if(typeof grammar === 'undefined') {
grammar = 'w3_1.0';
}
return _loadParser(grammar);
};
module.exports = csp_util;
'use strict';
var NAMES = {
"img-src":"Images",
"frame-src":"Frames",
"style-src":"Stylesheets",
"script-src":"Javascript files",
"font-src":"Font files",
"object-src":"Applets/flash files",
"connect-src":"XHR requests",
"media-src":"Videos"
};
var _unusedDirectives = function(policy) {
var directives = policy.split(";");
var bingo = JSON.parse(JSON.stringify(NAMES));
for(var i=0; i<directives.length; i++) {
var directive = directives[0];
delete bingo[directive[0]];
}
return bingo;
};
var _generateWarnings = function(policy) {
var splitPolicy = policy.split(";");
var explanations = [];
var match = /'unsafe-inline'/.exec(policy);
if(match !== null) {
// TODO we know the inline is in the right place, need to separate default, script, style
explanations.push("Inline javascript or styles are enabled");
}
match = /'unsafe-eval'/.exec(policy);
if(match !== null) {
explanations.push("Eval is enabled");
}
match = /[^;](\w*) http:\/\/*[ ;]/.exec(policy);
if(match !== null) {
// TODO this is weak, specify where allowed?
explanations.push("http resources are enabled - you are loading any http resource for " + match[1]);
}
for(var i = 0; i<splitPolicy.length; i++) {
var directive = splitPolicy[i];
var tokens = directive.trim().split(" ");
for(var j=1; j<tokens.length; j++) {
if(tokens[j] === "http://*") {
explanations.push("This allows any " + tokens[0].replace('-src', '') + " to be fetched over plain text.");
}
}
}
return explanations;
};
var _generatePermissions = function(policy) {
var permissions = [];
var directives = policy.split(";");
for(var i=0; i<directives.length; i++) {
var directive = directives[i].split(" ");
var name = directive[0];
var whitelistedHosts = [];
for(var i=1; i<directive.length; i++) {
var token = directive[i];
var hostInfo;
if(["'unsafe-inline'", "'unsafe-eval'"].indexOf(token) > -1) {
hostInfo = undefined; // for some reason, this was needed. hostInfo was holding on to the previous value.
} else if(token === "'self'") {
hostInfo = "the host serving this resource";
} else if(token === 'http://*') {
hostInfo = "any plaintext connection";
} else if(token === "'none'") {
hostInfo = "nowhere";
} else if(token === "https://*") {
hostInfo = "any https (SSL) connection";
} else {
hostInfo = token;
}
if(typeof hostInfo !== 'undefined') {
whitelistedHosts.push(hostInfo);
}
}
var permission = NAMES[name] + " from " + whitelistedHosts.join(", ");
permissions.push(permission);
}
return permissions;
};
var _generateGeneralMessage = function(policy) {
var messages = [];
var match = /default-src ([^;]*)/.exec(policy);
var unusedDirectives = _unusedDirectives(policy);
if(match != null) {
if(Object.keys(unusedDirectives).length > 0) {
messages.push("No config was provided for " + Object.keys(unusedDirectives).join(', ') + ". These values will inherit the default-src value(" + match[1] +")");
}
} else {
messages.push("No default-src was provided, defaulting to default-src 'self'");
}
return messages;
};
var _urlParts = function(url) {
var uriRegex = /^(http[s]?):\/\/([^\/]*).*$/;
return uriRegex.exec(url);
};
var _directiveName = function(directive, trimSrc) {
var name = directive.split(" ")[0];
if(trimSrc) {
name = name.substring(0, name.indexOf('-'));
}
return name;
};
var CSPValidator = function(parser) {
this.parser = parser;
};
CSPValidator.prototype.validate = function(policy) {
if(typeof policy === 'undefined') {
console.log("Damn dawg, supply a policy");
return null;
}
policy = policy.replace(" ", " ");
return this.parser.parse(policy);
};
CSPValidator.prototype.explain = function(policy) {
var parsed = this.validate(policy);
var warnings = _generateWarnings(policy);
var permissions = _generatePermissions(policy);
var general = _generateGeneralMessage(policy);
return {"permissions":permissions, "warnings":warnings, "general":general};
};
CSPValidator.prototype.why = function(cspReport) {
var directiveName = _directiveName(cspReport['violated-directive'], true);
var explanation = [];
var suggestion;
if(cspReport['violated-directive'] == 'eval script base restriction') {
explanation.push("Eval was called when the policy did not allow it.");
suggestion = "NOTE: A lot of eval violations are generated by plugins. You can either remove the eval call or whitelist eval ('unsafe-eval').";
} else if (typeof cspReport['script-sample'] !== 'undefined') {
explanation.push("This violation occurred because of " + cspReport['violated-directive'] + ". Sample code is: " + cspReport['script-sample']);
suggestion = "NOTE: A lot of inline script violations are generated by plugins. You can either remove the inline script or whitelist inline javascript.";
} else if(typeof cspReport['line-number'] !== 'undefined' || typeof cspReport['source-file'] !== 'undefined') {
explanation.push("This violation occurred because your page tried to execute inline " + directiveName + " in " + cspReport['source-file'] + " on line " + cspReport['line-number']);
suggestion = "NOTE: A lot of inline " + directiveName + " violations are generated by plugins. Inspect the given file name and line number to look for potential violations.";
} else if(directiveName === 'script' && (typeof cspReport['blocked-uri'] === 'undefined' || cspReport['blocked-uri'] === '')) {
explanation.push("A script-src violation occurred. There is not enough data to determine the cause. It could be inline javascript, javascript: in a link href, or an onclick event.");
suggestion = "Investigate the page and check for javascript:/onclick/etc. You can allow javascript: or whitelist inline script as well.";
} else if(/chrome-extension/.test(cspReport['blocked-uri'])) {
explanation.push("Your policy blocked the " + cspReport['blocked-uri']);
suggestion = "Add chrome-extension: to your " + directiveName + " settings";
} else if(typeof cspReport['blocked-uri'] !== 'undefined') {
var violatedDirective = cspReport['violated-directive'].split(' ');
explanation.push("This violation occurred because the host for " + cspReport['blocked-uri'] + " was not whitelisted in " + violatedDirective[0]);
var match = _urlParts(cspReport['blocked-uri']);
var protocol = match[1];
var host = match[2];
suggestion = "Add " + host + " to the whitelist " + _directiveName(cspReport['violated-directive']) + " or consider allowing all resources from " + protocol;
} else {
explanation.push("Disclaimer: this is the fall thru case. I dunno what to do.");
suggestion = "File a bug? (inline style violations not supported)";
}
return {"reasons":explanation, "violated-directive":cspReport['violated-directive'], "suggestion":suggestion, "report":cspReport};
};
module.exports = CSPValidator;
policy
= directive (" "? ";" " "? directive?)*
directive
= report_uri_directive / declaritive_directive
report_uri_directive
= "report-uri " host_source? [a-zA-Z/_-]*
declaritive_directive
= name:directive_name " " sources:source_list {
// inline/eval values are only valid in style/script/default blocks
switch(name) {
case "default-src":
case "script-src":
case "style-src":
break;
default:
for(var i=0; i < sources.length; i++) {
var s = sources[i][0];
if(s === "'unsafe-inline'" || s === "'unsafe-eval'") {
console.log(name + " doesn't honor " + s);
return null;
}
}
}
return name + " " + sources;
}
directive_name
= "default-src" / "script-src" / "object-src" / "style-src" / "img-src" / "media-src" / "frame-src" / "font-src" / "connect-src"
source_list
= "'none'" / (source_expression [ ]?)* // space is only optional if before semi-colon :-/
source_expression
= keyword_source / scheme_source / host_source // jank covers up javascript/blob/etc
host_source
= (scheme "://" / "/")? host (port)?
scheme_source
= scheme_only ":"
keyword_source
= "'self'" / "'unsafe-inline'" / "'unsafe-eval'"
scheme_only
= "data" / "javascript" / "blob" / "chrome-extension" / "about"
scheme
= "https" / "http" / "ws"
host
= ("*.")? host_char+ ("." host_char+)* / "*"
host_char
= [a-zA-Z] / [0-9] / '-'
port
= (":" [0-9]+) / ":*"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment