-
Notifications
You must be signed in to change notification settings - Fork 2k
Expand file tree
/
Copy pathIncompleteSanitization.ql
More file actions
128 lines (120 loc) · 4.06 KB
/
IncompleteSanitization.ql
File metadata and controls
128 lines (120 loc) · 4.06 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
/**
* @name Incomplete string escaping or encoding
* @description A string transformer that does not replace or escape all occurrences of a
* meta-character may be ineffective.
* @kind problem
* @problem.severity warning
* @precision high
* @id js/incomplete-sanitization
* @tags correctness
* security
* external/cwe/cwe-116
* external/cwe/cwe-20
*/
import javascript
/**
* Gets a character that is commonly used as a meta-character.
*/
string metachar() {
result = "'\"\\&<>\n\r\t*|{}[]%$".charAt(_)
}
/** Gets a string matched by `e` in a `replace` call. */
string getAMatchedString(Expr e) {
result = getAMatchedConstant(e.(RegExpLiteral).getRoot()).getValue()
or
result = e.getStringValue()
}
/** Gets a constant matched by `t`. */
RegExpConstant getAMatchedConstant(RegExpTerm t) {
result = t
or
result = getAMatchedConstant(t.(RegExpAlt).getAlternative())
or
result = getAMatchedConstant(t.(RegExpGroup).getAChild())
or
exists (RegExpCharacterClass recc | recc = t and not recc.isInverted() |
result = getAMatchedConstant(recc.getAChild())
)
}
/** Holds if `t` is simple, that is, a union of constants. */
predicate isSimple(RegExpTerm t) {
t instanceof RegExpConstant
or
isSimple(t.(RegExpGroup).getAChild())
or
(
t instanceof RegExpAlt or
t instanceof RegExpCharacterClass and not t.(RegExpCharacterClass).isInverted()
) and
forall (RegExpTerm ch | ch = t.getAChild() | isSimple(ch))
}
/**
* Holds if `mce` is of the form `x.replace(re, new)`, where `re` is a global
* regular expression and `new` prefixes the matched string with a backslash.
*/
predicate isBackslashEscape(MethodCallExpr mce, RegExpLiteral re) {
mce.getMethodName() = "replace" and
re = mce.getArgument(0) and
re.isGlobal() and
exists (string new | new = mce.getArgument(1).getStringValue() |
// `new` is `\$&`, `\$1` or similar
new.regexpMatch("\\\\\\$(&|\\d)")
or
// `new` is `\c`, where `c` is a constant matched by `re`
new.regexpMatch("\\\\\\Q" + getAMatchedString(re) + "\\E")
)
}
/**
* Holds if data flowing into `nd` has no un-escaped backslashes.
*/
predicate allBackslashesEscaped(DataFlow::Node nd) {
// `JSON.stringify` escapes backslashes
nd = DataFlow::globalVarRef("JSON").getAMemberCall("stringify")
or
// check whether `nd` itself escapes backslashes
exists (RegExpLiteral rel | isBackslashEscape(nd.asExpr(), rel) |
// if it's a complex regexp, we conservatively assume that it probably escapes backslashes
not isSimple(rel.getRoot()) or
getAMatchedString(rel) = "\\"
)
or
// flow through string methods
exists (DataFlow::MethodCallNode mc, string m |
m = "replace" or
m = "slice" or m = "substr" or m = "substring" or
m = "toLowerCase" or m = "toUpperCase" or m = "trim" |
mc = nd and m = mc.getMethodName() and allBackslashesEscaped(mc.getReceiver())
)
or
// general data flow
allBackslashesEscaped(nd.getAPredecessor())
}
from MethodCallExpr repl, Expr old, string msg
where repl.getMethodName() = "replace" and
old = repl.getArgument(0) and
(
not old.(RegExpLiteral).isGlobal() and
msg = "This replaces only the first occurrence of " + old + "." and
// only flag if this is likely to be a sanitizer or URL encoder or decoder
exists (string m | m = getAMatchedString(old) |
// sanitizer
m = metachar()
or
exists (string urlEscapePattern | urlEscapePattern = "(%[0-9A-Fa-f]{2})+" |
// URL decoder
m.regexpMatch(urlEscapePattern)
or
// URL encoder
repl.getArgument(1).getStringValue().regexpMatch(urlEscapePattern)
)
) and
// don't flag replace operations in a loop
not DataFlow::valueNode(repl.getReceiver()) = DataFlow::valueNode(repl).getASuccessor+()
or
exists (RegExpLiteral rel |
isBackslashEscape(repl, rel) and
not allBackslashesEscaped(DataFlow::valueNode(repl)) and
msg = "This does not backslash-escape the backslash character."
)
)
select old, msg