mirror of
https://github.com/github/codeql.git
synced 2025-12-16 16:53:25 +01:00
JavaScript: Add library for HTML sanitizers
This commit is contained in:
@@ -21,6 +21,7 @@ import semmle.javascript.Externs
|
||||
import semmle.javascript.Files
|
||||
import semmle.javascript.Functions
|
||||
import semmle.javascript.HTML
|
||||
import semmle.javascript.HtmlSanitizers
|
||||
import semmle.javascript.JSDoc
|
||||
import semmle.javascript.JSON
|
||||
import semmle.javascript.JsonParsers
|
||||
|
||||
46
javascript/ql/src/semmle/javascript/HtmlSanitizers.qll
Normal file
46
javascript/ql/src/semmle/javascript/HtmlSanitizers.qll
Normal file
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Provides classes for working with HTML sanitizers.
|
||||
*/
|
||||
import javascript
|
||||
|
||||
/**
|
||||
* A call that sanitizes HTML in a string, either by replacing
|
||||
* meta characters with their HTML entities, or by removing
|
||||
* certain HTML tags entirely.
|
||||
*/
|
||||
abstract class HtmlSanitizerCall extends DataFlow::CallNode {
|
||||
/**
|
||||
* Gets the data flow node referring to the input that gets sanitized.
|
||||
*/
|
||||
abstract DataFlow::Node getInput();
|
||||
}
|
||||
|
||||
private class DefaultHtmlSanitizerCall extends HtmlSanitizerCall {
|
||||
DefaultHtmlSanitizerCall() {
|
||||
exists (DataFlow::SourceNode callee | this = callee.getACall() |
|
||||
callee = DataFlow::moduleMember("ent", "encode") or
|
||||
callee = DataFlow::moduleMember("entities", "encodeHTML") or
|
||||
callee = DataFlow::moduleMember("entities", "encodeXML") or
|
||||
callee = DataFlow::moduleMember("escape-goat", "escape") or
|
||||
callee = DataFlow::moduleMember("he", "encode") or
|
||||
callee = DataFlow::moduleMember("he", "escape") or
|
||||
callee = DataFlow::moduleImport("sanitize-html") or
|
||||
callee = DataFlow::moduleMember("sanitizer", "escape") or
|
||||
callee = DataFlow::moduleMember("sanitizer", "sanitize") or
|
||||
callee = DataFlow::moduleMember("validator", "escape") or
|
||||
callee = DataFlow::moduleImport("xss") or
|
||||
callee = DataFlow::moduleMember("xss-filters", _) or
|
||||
callee = LodashUnderscore::member("escape") or
|
||||
exists (string name | name = "encode" or name = "encodeNonUTF" |
|
||||
callee = DataFlow::moduleMember("html-entities", _).getAnInstantiation().getAPropertyRead(name) or
|
||||
callee = DataFlow::moduleMember("html-entities", _).getAPropertyRead(name))
|
||||
)
|
||||
or
|
||||
// Match home-made sanitizers by name.
|
||||
exists (string calleeName | calleeName = getCalleeName() |
|
||||
calleeName.regexpMatch("(?i).*html.*") and
|
||||
calleeName.regexpMatch("(?i).*(?<!un)(saniti[sz]|escape|strip).*"))
|
||||
}
|
||||
|
||||
override DataFlow::Node getInput() { result = getArgument(0) }
|
||||
}
|
||||
@@ -43,12 +43,7 @@ module CodeInjection {
|
||||
|
||||
override predicate isAdditionalTaintStep(DataFlow::Node src, DataFlow::Node trg) {
|
||||
// HTML sanitizers are insufficient protection against code injection
|
||||
exists(CallExpr htmlSanitizer, string calleeName |
|
||||
calleeName = htmlSanitizer.getCalleeName() and
|
||||
calleeName.regexpMatch("(?i).*html.*") and
|
||||
calleeName.regexpMatch("(?i).*(saniti[sz]|escape|strip).*") and
|
||||
trg.asExpr() = htmlSanitizer and src.asExpr() = htmlSanitizer.getArgument(0)
|
||||
)
|
||||
src = trg.(HtmlSanitizerCall).getInput()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
| tst.js:17:1:17:47 | checkEs ... ipt>')) | OK |
|
||||
| tst.js:18:1:18:56 | checkEs ... ipt>')) | OK |
|
||||
| tst.js:19:1:19:55 | checkEs ... ipt>')) | OK |
|
||||
| tst.js:20:1:20:55 | checkEs ... ipt>')) | OK |
|
||||
| tst.js:21:1:21:46 | checkEs ... ipt>')) | OK |
|
||||
| tst.js:22:1:22:46 | checkEs ... ipt>')) | OK |
|
||||
| tst.js:23:1:23:50 | checkEs ... ipt>')) | OK |
|
||||
| tst.js:24:1:24:53 | checkEs ... ipt>')) | OK |
|
||||
| tst.js:25:1:25:54 | checkEs ... ipt>')) | OK |
|
||||
| tst.js:26:1:26:53 | checkEs ... ipt>')) | OK |
|
||||
| tst.js:27:1:27:40 | checkEs ... ipt>')) | OK |
|
||||
| tst.js:28:1:28:59 | checkEs ... ipt>')) | OK |
|
||||
| tst.js:29:1:29:51 | checkSt ... ipt>')) | OK |
|
||||
| tst.js:30:1:30:56 | checkSt ... ipt>')) | OK |
|
||||
| tst.js:33:1:33:47 | checkEs ... ipt>')) | OK |
|
||||
| tst.js:34:1:34:53 | checkEs ... ipt>')) | OK |
|
||||
| tst.js:35:1:35:41 | checkEs ... ipt>')) | OK |
|
||||
| tst.js:36:1:36:47 | checkEs ... ipt>')) | OK |
|
||||
| tst.js:38:1:38:58 | checkNo ... ipt>')) | OK |
|
||||
@@ -0,0 +1,25 @@
|
||||
import javascript
|
||||
|
||||
class Assertion extends DataFlow::CallNode {
|
||||
Assertion() {
|
||||
getCalleeName() = "checkEscaped" or
|
||||
getCalleeName() = "checkStripped" or
|
||||
getCalleeName() = "checkNotEscaped"
|
||||
}
|
||||
|
||||
predicate shouldBeSanitizer() {
|
||||
getCalleeName() != "checkNotEscaped"
|
||||
}
|
||||
|
||||
string getMessage() {
|
||||
if shouldBeSanitizer() and not getArgument(0) instanceof HtmlSanitizerCall then
|
||||
result = "Should be marked as sanitizer"
|
||||
else if not shouldBeSanitizer() and getArgument(0) instanceof HtmlSanitizerCall then
|
||||
result = "Should not be marked as sanitizer"
|
||||
else
|
||||
result = "OK"
|
||||
}
|
||||
}
|
||||
|
||||
from Assertion assertion
|
||||
select assertion, assertion.getMessage()
|
||||
17
javascript/ql/test/library-tests/HtmlSanitizers/package.json
Normal file
17
javascript/ql/test/library-tests/HtmlSanitizers/package.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"ent": "^2.2.0",
|
||||
"entities": "^1.1.1",
|
||||
"escape-goat": "^1.3.0",
|
||||
"he": "^1.1.1",
|
||||
"html-entities": "^1.2.1",
|
||||
"lodash": "^4.17.10",
|
||||
"sanitize-html": "^1.18.2",
|
||||
"sanitizer": "^0.1.3",
|
||||
"underscore": "^1.9.1",
|
||||
"validator": "^10.4.0",
|
||||
"xss": "^1.0.3",
|
||||
"xss-filters": "^1.2.7"
|
||||
}
|
||||
}
|
||||
38
javascript/ql/test/library-tests/HtmlSanitizers/tst.js
Normal file
38
javascript/ql/test/library-tests/HtmlSanitizers/tst.js
Normal file
@@ -0,0 +1,38 @@
|
||||
function checkEscaped(str) {
|
||||
if (str !== '<script>' && str !== '<script>' && str !== '<script>' && str !== '<script>') {
|
||||
throw new Error('Not escaped: ' + str);
|
||||
}
|
||||
}
|
||||
function checkStripped(str) {
|
||||
if (str !== '') {
|
||||
throw new Error('Not stripped: ' + str);
|
||||
}
|
||||
}
|
||||
function checkNotEscaped(str) {
|
||||
if (str !== '<script>') {
|
||||
throw new Error('Escaped: ' + str);
|
||||
}
|
||||
}
|
||||
|
||||
checkEscaped(require('ent').encode('<script>'));
|
||||
checkEscaped(require('entities').encodeHTML('<script>'));
|
||||
checkEscaped(require('entities').encodeXML('<script>'));
|
||||
checkEscaped(require('escape-goat').escape('<script>'));
|
||||
checkEscaped(require('he').encode('<script>'));
|
||||
checkEscaped(require('he').escape('<script>'));
|
||||
checkEscaped(require('lodash').escape('<script>'));
|
||||
checkEscaped(require('sanitizer').escape('<script>'));
|
||||
checkEscaped(require('underscore').escape('<script>'));
|
||||
checkEscaped(require('validator').escape('<script>'));
|
||||
checkEscaped(require('xss')('<script>'));
|
||||
checkEscaped(require('xss-filters').inHTMLData('<script>'));
|
||||
checkStripped(require('sanitize-html')('<script>'));
|
||||
checkStripped(require('sanitizer').sanitize('<script>'));
|
||||
|
||||
let Entities = require('html-entities').Html5Entities;
|
||||
checkEscaped(new Entities().encode('<script>'));
|
||||
checkEscaped(new Entities().encodeNonUTF('<script>'));
|
||||
checkEscaped(Entities.encode('<script>'));
|
||||
checkEscaped(Entities.encodeNonUTF('<script>'));
|
||||
|
||||
checkNotEscaped(new Entities().encodeNonASCII('<script>'));
|
||||
Reference in New Issue
Block a user