-
Notifications
You must be signed in to change notification settings - Fork 2k
Expand file tree
/
Copy pathIncompleteSanitization.ql
More file actions
169 lines (158 loc) · 4.99 KB
/
IncompleteSanitization.ql
File metadata and controls
169 lines (158 loc) · 4.99 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
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
/**
* @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(DataFlow::Node e) {
result = e.(DataFlow::RegExpLiteralNode).getRoot().getAMatchedString()
or
result = e.getStringValue()
}
/** 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(StringReplaceCall mce, DataFlow::RegExpLiteralNode re) {
mce.isGlobal() and
re = mce.getRegExp() and
(
// replacement with `\$&`, `\$1` or similar
mce.getRawReplacement().getStringValue().regexpMatch("\\\\\\$(&|\\d)")
or
// replacement of `c` with `\c`
exists(string c | mce.replaces(c, "\\" + c))
)
}
/**
* 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(DataFlow::RegExpLiteralNode rel | isBackslashEscape(nd, 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())
}
/**
* Holds if `repl` looks like a call to "String.prototype.replace" that deliberately removes the first occurrence of `str`.
*/
predicate removesFirstOccurence(StringReplaceCall repl, string str) {
not exists(repl.getRegExp()) and repl.replaces(str, "")
}
/**
* Holds if `leftUnwrap` and `rightUnwrap` unwraps a string from a pair of surrounding delimiters.
*/
predicate isDelimiterUnwrapper(
DataFlow::MethodCallNode leftUnwrap, DataFlow::MethodCallNode rightUnwrap
) {
exists(string left, string right |
left = "[" and right = "]"
or
left = "{" and right = "}"
or
left = "(" and right = ")"
or
left = "\"" and right = "\""
or
left = "'" and right = "'"
|
removesFirstOccurence(leftUnwrap, left) and
removesFirstOccurence(rightUnwrap, right) and
leftUnwrap.getAMethodCall() = rightUnwrap
)
}
/*
* Holds if `repl` is a standalone use of `String.prototype.replace` to remove a single newline.
*/
predicate removesTrailingNewLine(StringReplaceCall repl) {
not repl.isGlobal() and
repl.replaces("\n", "") and
not exists(StringReplaceCall other |
repl.getAMethodCall() = other or
other.getAMethodCall() = repl
)
}
from StringReplaceCall repl, DataFlow::Node old, string msg
where
(old = repl.getArgument(0) or old = repl.getRegExp()) and
(
not repl.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)
)
or
// path sanitizer
(m = ".." or m = "/.." or m = "../" or m = "/../")
) and
// don't flag replace operations in a loop
not repl.getReceiver() = repl.getASuccessor+() and
// dont' flag unwrapper
not isDelimiterUnwrapper(repl, _) and
not isDelimiterUnwrapper(_, repl) and
// dont' flag the removal of trailing newlines
not removesTrailingNewLine(repl)
or
exists(DataFlow::RegExpLiteralNode rel |
isBackslashEscape(repl, rel) and
not allBackslashesEscaped(repl) and
msg = "This does not escape backslash characters in the input."
)
)
select repl.getCalleeNode(), msg