Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit d844e00

Browse files
authored
Merge pull request #3651 from esbena/js/bad-multicharacter-sanitization
JS: initial version of IncompleteMultiCharacterSanitization.ql
2 parents 01c51ee + 678bb7c commit d844e00

5 files changed

Lines changed: 255 additions & 0 deletions

File tree

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
6+
<overview>
7+
8+
</overview>
9+
10+
<recommendation>
11+
12+
</recommendation>
13+
14+
<example>
15+
16+
</example>
17+
18+
<references>
19+
20+
<li>OWASP Top 10: <a href="https://www.owasp.org/index.php/Top_10-2017_A1-Injection">A1 Injection</a>.</li>
21+
22+
</references>
23+
24+
</qhelp>
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/**
2+
* @name Incomplete multi-character sanitization
3+
* @description A sanitizer that removes a sequence of characters may reintroduce the dangerous sequence.
4+
* @kind problem
5+
* @problem.severity warning
6+
* @precision high
7+
* @id js/incomplete-multi-character-sanitization
8+
* @tags correctness
9+
* security
10+
* external/cwe/cwe-116
11+
* external/cwe/cwe-20
12+
*/
13+
14+
import javascript
15+
import semmle.javascript.security.IncompleteBlacklistSanitizer
16+
17+
predicate isDangerous(RegExpTerm t) {
18+
// path traversals
19+
t.getAMatchedString() = ["..", "/..", "../"]
20+
or
21+
exists(RegExpTerm start |
22+
start = t.(RegExpSequence).getAChild() and
23+
start.getConstantValue() = "." and
24+
start.getSuccessor().getConstantValue() = "." and
25+
not [start.getPredecessor(), start.getSuccessor().getSuccessor()].getConstantValue() = "."
26+
)
27+
or
28+
// HTML comments
29+
t.getAMatchedString() = "<!--"
30+
or
31+
// HTML scripts
32+
t.getAMatchedString().regexpMatch("(?i)<script.*")
33+
or
34+
exists(RegExpSequence seq | seq = t |
35+
t.getChild(0).getConstantValue() = "<" and
36+
// the `cript|scrip` case has been observed in the wild, not sure what the goal of that pattern is...
37+
t
38+
.getChild(0)
39+
.getSuccessor+()
40+
.getAMatchedString()
41+
.regexpMatch("(?i)iframe|script|cript|scrip|style")
42+
)
43+
or
44+
// HTML attributes
45+
exists(string dangerousPrefix | dangerousPrefix = ["ng-", "on"] |
46+
t.getAMatchedString().regexpMatch("(i?)" + dangerousPrefix + "[a-z]+")
47+
or
48+
exists(RegExpTerm start, RegExpTerm event | start = t.getAChild() |
49+
start.getConstantValue().regexpMatch("(?i)[^a-z]*" + dangerousPrefix) and
50+
event = start.getSuccessor() and
51+
exists(RegExpTerm quantified | quantified = event.(RegExpQuantifier).getChild(0) |
52+
quantified
53+
.(RegExpCharacterClass)
54+
.getAChild()
55+
.(RegExpCharacterRange)
56+
.isRange(["a", "A"], ["z", "Z"]) or
57+
[quantified, quantified.(RegExpRange).getAChild()].(RegExpCharacterClassEscape).getValue() =
58+
"w"
59+
)
60+
)
61+
)
62+
}
63+
64+
from StringReplaceCall replace, RegExpTerm regexp, RegExpTerm dangerous
65+
where
66+
[replace.getRawReplacement(), replace.getCallback(1).getAReturn()].mayHaveStringValue("") and
67+
replace.isGlobal() and
68+
regexp = replace.getRegExp().getRoot() and
69+
dangerous.getRootTerm() = regexp and
70+
isDangerous(dangerous) and
71+
// avoid anchored terms
72+
not exists(RegExpAnchor a | a.getRootTerm() = regexp) and
73+
// avoid flagging wrappers
74+
not (
75+
dangerous instanceof RegExpAlt or
76+
dangerous instanceof RegExpGroup
77+
) and
78+
// don't flag replace operations in a loop
79+
not replace.getReceiver().getALocalSource() = replace
80+
select replace, "The replaced string may still contain a substring that starts matching at $@.",
81+
dangerous, dangerous.toString()
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
| tst-multi-character-sanitization.js:3:13:3:57 | content ... gi, "") | The replaced string may still contain a substring that starts matching at $@. | tst-multi-character-sanitization.js:3:30:3:49 | <.*cript.*\\/scrip.*> | <.*cript.*\\/scrip.*> |
2+
| tst-multi-character-sanitization.js:4:13:4:47 | content ... /g, "") | The replaced string may still contain a substring that starts matching at $@. | tst-multi-character-sanitization.js:4:30:4:40 | on\\w+=".*" | on\\w+=".*" |
3+
| tst-multi-character-sanitization.js:5:13:5:49 | content ... /g, "") | The replaced string may still contain a substring that starts matching at $@. | tst-multi-character-sanitization.js:5:30:5:42 | on\\w+=\\'.*\\' | on\\w+=\\'.*\\' |
4+
| tst-multi-character-sanitization.js:9:13:9:47 | content ... gi, "") | The replaced string may still contain a substring that starts matching at $@. | tst-multi-character-sanitization.js:9:30:9:39 | <.*cript.* | <.*cript.* |
5+
| tst-multi-character-sanitization.js:10:13:10:49 | content ... /g, "") | The replaced string may still contain a substring that starts matching at $@. | tst-multi-character-sanitization.js:10:30:10:42 | .on\\w+=.*".*" | .on\\w+=.*".*" |
6+
| tst-multi-character-sanitization.js:11:13:11:51 | content ... /g, "") | The replaced string may still contain a substring that starts matching at $@. | tst-multi-character-sanitization.js:11:30:11:44 | .on\\w+=.*\\'.*\\' | .on\\w+=.*\\'.*\\' |
7+
| tst-multi-character-sanitization.js:19:3:19:35 | respons ... pt, "") | The replaced string may still contain a substring that starts matching at $@. | tst-multi-character-sanitization.js:18:18:18:24 | <script | <script |
8+
| tst-multi-character-sanitization.js:25:10:25:40 | text.re ... /g, "") | The replaced string may still contain a substring that starts matching at $@. | tst-multi-character-sanitization.js:25:24:25:27 | <!-- | <!-- |
9+
| tst-multi-character-sanitization.js:38:8:38:30 | id.repl ... /g, "") | The replaced string may still contain a substring that starts matching at $@. | tst-multi-character-sanitization.js:38:20:38:23 | \\.\\. | \\.\\. |
10+
| tst-multi-character-sanitization.js:49:13:49:43 | req.url ... EL, "") | The replaced string may still contain a substring that starts matching at $@. | tst-multi-character-sanitization.js:48:21:48:31 | (\\/)?\\.\\.\\/ | (\\/)?\\.\\.\\/ |
11+
| tst-multi-character-sanitization.js:64:7:64:73 | x.repla ... /g, "") | The replaced string may still contain a substring that starts matching at $@. | tst-multi-character-sanitization.js:64:18:64:24 | <script | <script |
12+
| tst-multi-character-sanitization.js:66:7:66:56 | x.repla ... /g, "") | The replaced string may still contain a substring that starts matching at $@. | tst-multi-character-sanitization.js:66:18:66:49 | (\\/\|\\s)on\\w+=(\\'\|")?[^"]*(\\'\|")? | (\\/\|\\s)on\\w+=(\\'\|")?[^"]*(\\'\|")? |
13+
| tst-multi-character-sanitization.js:75:7:75:37 | x.repla ... gm, "") | The replaced string may still contain a substring that starts matching at $@. | tst-multi-character-sanitization.js:75:18:75:21 | <!-- | <!-- |
14+
| tst-multi-character-sanitization.js:77:7:77:36 | x.repla ... /g, "") | The replaced string may still contain a substring that starts matching at $@. | tst-multi-character-sanitization.js:77:18:77:29 | \\sng-[a-z-]+ | \\sng-[a-z-]+ |
15+
| tst-multi-character-sanitization.js:81:7:81:58 | x.repla ... /g, "") | The replaced string may still contain a substring that starts matching at $@. | tst-multi-character-sanitization.js:81:18:81:24 | <script | <script |
16+
| tst-multi-character-sanitization.js:81:7:81:58 | x.repla ... /g, "") | The replaced string may still contain a substring that starts matching at $@. | tst-multi-character-sanitization.js:81:36:81:39 | only | only |
17+
| tst-multi-character-sanitization.js:82:7:82:50 | x.repla ... /g, "") | The replaced string may still contain a substring that starts matching at $@. | tst-multi-character-sanitization.js:82:18:82:30 | <script async | <script async |
18+
| tst-multi-character-sanitization.js:83:7:83:63 | x.repla ... gi, "") | The replaced string may still contain a substring that starts matching at $@. | tst-multi-character-sanitization.js:83:18:83:21 | <!-- | <!-- |
19+
| tst-multi-character-sanitization.js:85:7:85:48 | x.repla ... /g, "") | The replaced string may still contain a substring that starts matching at $@. | tst-multi-character-sanitization.js:85:18:85:41 | \\x2E\\x2E\\x2F\\x2E\\x2E\\x2F | \\x2E\\x2E\\x2F\\x2E\\x2E\\x2F |
20+
| tst-multi-character-sanitization.js:87:7:87:47 | x.repla ... gi, "") | The replaced string may still contain a substring that starts matching at $@. | tst-multi-character-sanitization.js:87:18:87:24 | <script | <script |
21+
| tst-multi-character-sanitization.js:92:7:96:4 | x.repla ... ";\\n }) | The replaced string may still contain a substring that starts matching at $@. | tst-multi-character-sanitization.js:92:18:92:24 | <script | <script |
22+
| tst-multi-character-sanitization.js:100:7:100:28 | x.repla ... /g, "") | The replaced string may still contain a substring that starts matching at $@. | tst-multi-character-sanitization.js:100:18:100:21 | \\.\\. | \\.\\. |
23+
| tst-multi-character-sanitization.js:101:7:101:30 | x.repla ... /g, "") | The replaced string may still contain a substring that starts matching at $@. | tst-multi-character-sanitization.js:101:18:101:23 | \\.\\.\\/ | \\.\\.\\/ |
24+
| tst-multi-character-sanitization.js:102:7:102:30 | x.repla ... /g, "") | The replaced string may still contain a substring that starts matching at $@. | tst-multi-character-sanitization.js:102:18:102:23 | \\/\\.\\. | \\/\\.\\. |
25+
| tst-multi-character-sanitization.js:104:7:104:58 | x.repla ... gi, "") | The replaced string may still contain a substring that starts matching at $@. | tst-multi-character-sanitization.js:104:18:104:24 | <script | <script |
26+
| tst-multi-character-sanitization.js:106:7:106:64 | x.repla ... gi, "") | The replaced string may still contain a substring that starts matching at $@. | tst-multi-character-sanitization.js:106:18:106:56 | <(script\|del)(?=[\\s>])[\\w\\W]*?<\\/\\1\\s*> | <(script\|del)(?=[\\s>])[\\w\\W]*?<\\/\\1\\s*> |
27+
| tst-multi-character-sanitization.js:107:7:107:62 | x.repla ... /g, "") | The replaced string may still contain a substring that starts matching at $@. | tst-multi-character-sanitization.js:107:18:107:55 | \\<script[\\s\\S]*?\\>[\\s\\S]*?\\<\\/script\\> | \\<script[\\s\\S]*?\\>[\\s\\S]*?\\<\\/script\\> |
28+
| tst-multi-character-sanitization.js:108:7:108:75 | x.repla ... gm, "") | The replaced string may still contain a substring that starts matching at $@. | tst-multi-character-sanitization.js:108:18:108:67 | <(script\|style\|title)[^<]+<\\/(script\|style\|title)> | <(script\|style\|title)[^<]+<\\/(script\|style\|title)> |
29+
| tst-multi-character-sanitization.js:109:7:109:58 | x.repla ... gi, "") | The replaced string may still contain a substring that starts matching at $@. | tst-multi-character-sanitization.js:109:18:109:24 | <script | <script |
30+
| tst-multi-character-sanitization.js:110:7:110:50 | x.repla ... gi, "") | The replaced string may still contain a substring that starts matching at $@. | tst-multi-character-sanitization.js:110:18:110:24 | <script | <script |
31+
| tst-multi-character-sanitization.js:111:7:111:32 | x.repla ... /g, "") | The replaced string may still contain a substring that starts matching at $@. | tst-multi-character-sanitization.js:111:20:111:23 | <!-- | <!-- |
32+
| tst-multi-character-sanitization.js:112:7:112:50 | x.repla ... /g, "") | The replaced string may still contain a substring that starts matching at $@. | tst-multi-character-sanitization.js:112:18:112:43 | require\\('\\.\\.\\/common'\\); | require\\('\\.\\.\\/common'\\); |
33+
| tst-multi-character-sanitization.js:113:7:113:41 | x.repla ... /g, "") | The replaced string may still contain a substring that starts matching at $@. | tst-multi-character-sanitization.js:113:18:113:34 | \\.\\.\\/\\.\\.\\/lib\\/ | \\.\\.\\/\\.\\.\\/lib\\/ |
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Security/CWE-116/IncompleteMultiCharacterSanitization.ql
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
// CVE-2019-10756
2+
(function(content) {
3+
content = content.replace(/<.*cript.*\/scrip.*>/gi, ""); // NOT OK
4+
content = content.replace(/ on\w+=".*"/g, ""); // NOT OK
5+
content = content.replace(/ on\w+=\'.*\'/g, ""); // NOT OK
6+
return content;
7+
});
8+
(function(content) {
9+
content = content.replace(/<.*cript.*/gi, ""); // NOT OK
10+
content = content.replace(/.on\w+=.*".*"/g, ""); // NOT OK
11+
content = content.replace(/.on\w+=.*\'.*\'/g, ""); // NOT OK
12+
13+
return content;
14+
});
15+
16+
// CVE-2020-7656
17+
(function(responseText) {
18+
var rscript = /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi;
19+
responseText.replace(rscript, ""); // NOT OK
20+
return responseText;
21+
});
22+
23+
// CVE-2019-1010091
24+
(function(text) {
25+
text = text.replace(/<!--|--!?>/g, ""); // NOT OK
26+
return text;
27+
});
28+
(function(text) {
29+
while (/<!--|--!?>/g.test(text)) {
30+
text = text.replace(/<!--|--!?>/g, ""); // OK
31+
}
32+
33+
return text;
34+
});
35+
36+
// CVE-2019-10767
37+
(function(id) {
38+
id = id.replace(/\.\./g, ""); // NOT OK
39+
return id;
40+
});
41+
(function(id) {
42+
id = id.replace(/[\]\[*,;'"`<>\\?\/]/g, ""); // OK (or is it?)
43+
return id;
44+
});
45+
46+
// CVE-2019-8903
47+
(function(req) {
48+
var REG_TRAVEL = /(\/)?\.\.\//g;
49+
req.url = req.url.replace(REG_TRAVEL, ""); // NOT OK
50+
});
51+
(function(req) {
52+
var beg;
53+
for (var i = 0; i < req.url.length; i++) {
54+
if (req.url[i] === "." && req.url[i + 1] === "/") beg = i + 1;
55+
else if (req.url[i] === "?") break;
56+
}
57+
58+
if (beg) req.url = req.url.substring(beg);
59+
});
60+
61+
// New cases
62+
63+
(function(x) {
64+
x = x.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/g, ""); // NOT OK
65+
66+
x = x.replace(/(\/|\s)on\w+=(\'|")?[^"]*(\'|")?/g, ""); // NOT OK
67+
68+
x = x.replace(/<\/script>/g, ""); // OK
69+
70+
x = x.replace(/<(.)?br(.)?>/g, ""); // OK
71+
x = x.replace(/<\/?b>/g, ""); // OK
72+
x = x.replace(/<(ul|ol)><\/(ul|ol)>/gi, ""); // OK
73+
x = x.replace(/<li><\/li>/gi, ""); // OK
74+
75+
x = x.replace(/<!--(.*?)-->/gm, ""); // NOT OK
76+
x = x.replace(/\sng-[a-z-]+/, ""); // OK (single ng-attribute, should be flagged by some other query!)
77+
x = x.replace(/\sng-[a-z-]+/g, ""); // NOT OK (ng-attributes)
78+
79+
x = x.replace(/(<!--\[CDATA\[|\]\]-->)/g, "\n"); // OK: not a sanitizer
80+
81+
x = x.replace(/<script.+desktop\-only.+<\/script>/g, ""); // OK, but still flagged [INCONSISTENCY]
82+
x = x.replace(/<script async.+?<\/script>/g, ""); // OK, but still flagged [INCONSISTENCY]
83+
x = x.replace(/<!--[\s\S]*?-->|<\?(?:php)?[\s\S]*?\?>/gi, ""); // NOT OK
84+
85+
x = x.replace(/\x2E\x2E\x2F\x2E\x2E\x2F/g, ""); // NOT OK (matches "../../")
86+
87+
x = x.replace(/<script.*>.*<\/script>/gi, ""); // NOT OK
88+
89+
x = x.replace(/^(\.\.\/?)+/g, ""); // OK
90+
91+
// NOT OK
92+
x = x.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/g, function(
93+
$0
94+
) {
95+
return unknown ? $0 : "";
96+
});
97+
98+
x = x.replace(/<\/?([a-z][a-z0-9]*)\b[^>]*>/gi, ""); // NOT OK [INCONSISTENCY]
99+
100+
x = x.replace(/\.\./g, ""); // NOT OK
101+
x = x.replace(/\.\.\//g, ""); // NOT OK
102+
x = x.replace(/\/\.\./g, ""); // NOT OK
103+
104+
x = x.replace(/<script(.*?)>([\s\S]*?)<\/script>/gi, ""); // NOT OK
105+
106+
x = x.replace(/<(script|del)(?=[\s>])[\w\W]*?<\/\1\s*>/gi, ""); // NOT OK
107+
x = x.replace(/\<script[\s\S]*?\>[\s\S]*?\<\/script\>/g, ""); // NOT OK
108+
x = x.replace(/<(script|style|title)[^<]+<\/(script|style|title)>/gm, ""); // NOT OK
109+
x = x.replace(/<script[^>]*>([\s\S]*?)<\/script>/gi, ""); // NOT OK
110+
x = x.replace(/<script[\s\S]*?<\/script>/gi, ""); // NOT OK
111+
x = x.replace(/ ?<!-- ?/g, ""); // NOT OK
112+
x = x.replace(/require\('\.\.\/common'\);/g, ""); // OK [INCONSISTENCY] permit alphanum-suffix after the dots?
113+
x = x.replace(/\.\.\/\.\.\/lib\//g, ""); // OK [INCONSISTENCY] permit alphanum-suffix after the dots?
114+
115+
return x;
116+
});

0 commit comments

Comments
 (0)