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

Skip to content

Commit db3eaa2

Browse files
author
Max Schaefer
committed
JavaScript: Introduce modelling of String.prototype.replace and use it in two queries.
1 parent f43e843 commit db3eaa2

3 files changed

Lines changed: 89 additions & 49 deletions

File tree

javascript/ql/src/Security/CWE-116/DoubleEscaping.ql

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -126,27 +126,13 @@ abstract class Replacement extends DataFlow::Node {
126126
/**
127127
* A call to `String.prototype.replace` that replaces all instances of a pattern.
128128
*/
129-
class GlobalStringReplacement extends Replacement, DataFlow::MethodCallNode {
130-
RegExpLiteral pattern;
131-
129+
class GlobalStringReplacement extends Replacement, StringReplaceCall {
132130
GlobalStringReplacement() {
133-
this.getMethodName() = "replace" and
134-
pattern.flow().(DataFlow::SourceNode).flowsTo(this.getArgument(0)) and
135-
this.getNumArgument() = 2 and
136-
pattern.isGlobal()
131+
isGlobal()
137132
}
138133

139134
override predicate replaces(string input, string output) {
140-
input = pattern.getRoot().getConstantValue() and
141-
output = this.getArgument(1).getStringValue()
142-
or
143-
exists(DataFlow::FunctionNode replacer, DataFlow::PropRead pr, DataFlow::ObjectLiteralNode map |
144-
replacer = getCallback(1) and
145-
replacer.getParameter(0).flowsToExpr(pr.getPropertyNameExpr()) and
146-
pr = map.getAPropertyRead() and
147-
pr.flowsTo(replacer.getAReturn()) and
148-
map.asExpr().(ObjectExpr).getPropertyByName(input).getInit().getStringValue() = output
149-
)
135+
StringReplaceCall.super.replaces(input, output)
150136
}
151137

152138
override DataFlow::Node getInput() {

javascript/ql/src/Security/CWE-116/IncompleteSanitization.ql

Lines changed: 27 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ import javascript
2020
string metachar() { result = "'\"\\&<>\n\r\t*|{}[]%$".charAt(_) }
2121

2222
/** Gets a string matched by `e` in a `replace` call. */
23-
string getAMatchedString(Expr e) {
24-
result = e.(RegExpLiteral).getRoot().getAMatchedString()
23+
string getAMatchedString(DataFlow::Node e) {
24+
result = e.(DataFlow::RegExpLiteralNode).getRoot().getAMatchedString()
2525
or
2626
result = e.getStringValue()
2727
}
@@ -44,16 +44,15 @@ predicate isSimple(RegExpTerm t) {
4444
* Holds if `mce` is of the form `x.replace(re, new)`, where `re` is a global
4545
* regular expression and `new` prefixes the matched string with a backslash.
4646
*/
47-
predicate isBackslashEscape(MethodCallExpr mce, RegExpLiteral re) {
48-
mce.getMethodName() = "replace" and
49-
re.flow().(DataFlow::SourceNode).flowsToExpr(mce.getArgument(0)) and
50-
re.isGlobal() and
51-
exists(string new | new = mce.getArgument(1).getStringValue() |
52-
// `new` is `\$&`, `\$1` or similar
53-
new.regexpMatch("\\\\\\$(&|\\d)")
47+
predicate isBackslashEscape(StringReplaceCall mce, DataFlow::RegExpLiteralNode re) {
48+
mce.isGlobal() and
49+
re = mce.getRegExp() and
50+
(
51+
// replacement with `\$&`, `\$1` or similar
52+
mce.getRawReplacement().getStringValue().regexpMatch("\\\\\\$(&|\\d)")
5453
or
55-
// `new` is `\c`, where `c` is a constant matched by `re`
56-
new.regexpMatch("\\\\\\Q" + getAMatchedString(re) + "\\E")
54+
// replacement of `c` with `\c`
55+
exists(string c | mce.replaces(c, "\\" + c))
5756
)
5857
}
5958

@@ -65,7 +64,7 @@ predicate allBackslashesEscaped(DataFlow::Node nd) {
6564
nd = DataFlow::globalVarRef("JSON").getAMemberCall("stringify")
6665
or
6766
// check whether `nd` itself escapes backslashes
68-
exists(RegExpLiteral rel | isBackslashEscape(nd.asExpr(), rel) |
67+
exists(DataFlow::RegExpLiteralNode rel | isBackslashEscape(nd, rel) |
6968
// if it's a complex regexp, we conservatively assume that it probably escapes backslashes
7069
not isSimple(rel.getRoot()) or
7170
getAMatchedString(rel) = "\\"
@@ -91,10 +90,8 @@ predicate allBackslashesEscaped(DataFlow::Node nd) {
9190
/**
9291
* Holds if `repl` looks like a call to "String.prototype.replace" that deliberately removes the first occurrence of `str`.
9392
*/
94-
predicate removesFirstOccurence(DataFlow::MethodCallNode repl, string str) {
95-
repl.getMethodName() = "replace" and
96-
repl.getArgument(0).getStringValue() = str and
97-
repl.getArgument(1).getStringValue() = ""
93+
predicate removesFirstOccurence(StringReplaceCall repl, string str) {
94+
not exists(repl.getRegExp()) and repl.replaces(str, "")
9895
}
9996

10097
/**
@@ -124,22 +121,20 @@ predicate isDelimiterUnwrapper(
124121
* Holds if `repl` is a standalone use of `String.prototype.replace` to remove a single newline.
125122
*/
126123

127-
predicate removesTrailingNewLine(DataFlow::MethodCallNode repl) {
128-
repl.getMethodName() = "replace" and
129-
repl.getArgument(0).mayHaveStringValue("\n") and
130-
repl.getArgument(1).mayHaveStringValue("") and
131-
not exists(DataFlow::MethodCallNode other | other.getMethodName() = "replace" |
124+
predicate removesTrailingNewLine(StringReplaceCall repl) {
125+
not repl.isGlobal() and
126+
repl.replaces("\n", "") and
127+
not exists(StringReplaceCall other |
132128
repl.getAMethodCall() = other or
133129
other.getAMethodCall() = repl
134130
)
135131
}
136132

137-
from MethodCallExpr repl, Expr old, string msg
133+
from StringReplaceCall repl, DataFlow::Node old, string msg
138134
where
139-
repl.getMethodName() = "replace" and
140-
(old = repl.getArgument(0) or old.flow().(DataFlow::SourceNode).flowsToExpr(repl.getArgument(0))) and
135+
(old = repl.getArgument(0) or old = repl.getRegExp()) and
141136
(
142-
not old.(RegExpLiteral).isGlobal() and
137+
not repl.isGlobal() and
143138
msg = "This replaces only the first occurrence of " + old + "." and
144139
// only flag if this is likely to be a sanitizer or URL encoder or decoder
145140
exists(string m | m = getAMatchedString(old) |
@@ -158,17 +153,17 @@ where
158153
(m = ".." or m = "/.." or m = "../" or m = "/../")
159154
) and
160155
// don't flag replace operations in a loop
161-
not DataFlow::valueNode(repl.getReceiver()) = DataFlow::valueNode(repl).getASuccessor+() and
156+
not repl.getReceiver() = repl.getASuccessor+() and
162157
// dont' flag unwrapper
163-
not isDelimiterUnwrapper(repl.flow(), _) and
164-
not isDelimiterUnwrapper(_, repl.flow()) and
158+
not isDelimiterUnwrapper(repl, _) and
159+
not isDelimiterUnwrapper(_, repl) and
165160
// dont' flag the removal of trailing newlines
166-
not removesTrailingNewLine(repl.flow())
161+
not removesTrailingNewLine(repl)
167162
or
168-
exists(RegExpLiteral rel |
163+
exists(DataFlow::RegExpLiteralNode rel |
169164
isBackslashEscape(repl, rel) and
170-
not allBackslashesEscaped(DataFlow::valueNode(repl)) and
165+
not allBackslashesEscaped(repl) and
171166
msg = "This does not escape backslash characters in the input."
172167
)
173168
)
174-
select repl.getCallee(), msg
169+
select repl.getCalleeNode(), msg

javascript/ql/src/semmle/javascript/StandardLibrary.qll

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,3 +248,62 @@ private class IteratorExceptionStep extends DataFlow::MethodCallNode, DataFlow::
248248
succ = this.getExceptionalReturn()
249249
}
250250
}
251+
252+
/**
253+
* A call to `String.prototype.replace`.
254+
*
255+
* We heuristically include any call to a method called `replace`, provided it either
256+
* has exactly two arguments, or local data flow suggests that the receiver may be a string.
257+
*/
258+
class StringReplaceCall extends DataFlow::MethodCallNode {
259+
StringReplaceCall() {
260+
getMethodName() = "replace" and
261+
(getNumArgument() = 2 or getReceiver().mayHaveStringValue(_))
262+
}
263+
264+
/** Gets the regular expression passed as the first argument to `replace`, if any. */
265+
DataFlow::RegExpLiteralNode getRegExp() {
266+
result.flowsTo(getArgument(0))
267+
}
268+
269+
/** Gets a string that is being replaced by this call. */
270+
string getAReplacedString() {
271+
result = getRegExp().getRoot().getAMatchedString() or
272+
getArgument(0).mayHaveStringValue(result)
273+
}
274+
275+
/**
276+
* Gets the second argument of this call to `replace`, which is either a string
277+
* or a callback.
278+
*/
279+
DataFlow::Node getRawReplacement() {
280+
result = getArgument(1)
281+
}
282+
283+
/**
284+
* Holds if this is a global replacement, that is, the first argument is a regulare expression
285+
* with the `g` flag.
286+
*/
287+
predicate isGlobal() {
288+
getRegExp().isGlobal()
289+
}
290+
291+
/**
292+
* Holds if this call to `replace` replaces `old` with `new`.
293+
*/
294+
predicate replaces(string old, string new) {
295+
exists(string rawNew |
296+
old = getAReplacedString() and
297+
getRawReplacement().mayHaveStringValue(rawNew) and
298+
new = rawNew.replaceAll("$&", old)
299+
)
300+
or
301+
exists(DataFlow::FunctionNode replacer, DataFlow::PropRead pr, DataFlow::ObjectLiteralNode map |
302+
replacer = getCallback(1) and
303+
replacer.getParameter(0).flowsToExpr(pr.getPropertyNameExpr()) and
304+
pr = map.getAPropertyRead() and
305+
pr.flowsTo(replacer.getAReturn()) and
306+
map.asExpr().(ObjectExpr).getPropertyByName(old).getInit().getStringValue() = new
307+
)
308+
}
309+
}

0 commit comments

Comments
 (0)