Created
November 12, 2013 16:47
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]+) / ":*" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'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; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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