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

Skip to content

Commit eb6e886

Browse files
authored
Merge pull request #2247 from max-schaefer/odasa-8149
Approved by asger-semmle, esbena
2 parents 770a470 + 016808b commit eb6e886

5 files changed

Lines changed: 193 additions & 30 deletions

File tree

change-notes/1.23/analysis-javascript.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232

3333
| **Query** | **Expected impact** | **Change** |
3434
|--------------------------------|------------------------------|---------------------------------------------------------------------------|
35+
| Double escaping or unescaping (`js/double-escaping`) | More results | This rule now detects additional escaping and unescaping functions. |
3536
| Incomplete string escaping or encoding (`js/incomplete-sanitization`) | Fewer false-positive results | This rule now recognizes additional ways delimiters can be stripped away. |
3637
| Client-side cross-site scripting (`js/xss`) | More results, fewer false-positive results | More potential vulnerabilities involving functions that manipulate DOM attributes are now recognized, and more sanitizers are detected. |
3738
| Code injection (`js/code-injection`) | More results | More potential vulnerabilities involving functions that manipulate DOM event handler attributes are now recognized. |

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ attacks such as cross-site scripting. One particular example of this is HTML ent
1010
where HTML special characters are replaced by HTML character entities to prevent them from being
1111
interpreted as HTML markup. For example, the less-than character is encoded as <code>&amp;lt;</code>
1212
and the double-quote character as <code>&amp;quot;</code>.
13-
Other examples include backslash-escaping for including untrusted data in string literals and
14-
percent-encoding for URI components.
13+
Other examples include backslash escaping or JSON encoding for including untrusted data in string
14+
literals, and percent-encoding for URI components.
1515
</p>
1616
<p>
1717
The reverse process of replacing escape sequences with the characters they represent is known as

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

Lines changed: 120 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -46,46 +46,44 @@ string getStringValue(RegExpLiteral rl) {
4646
*/
4747
DataFlow::Node getASimplePredecessor(DataFlow::Node nd) {
4848
result = nd.getAPredecessor() and
49-
not nd.(DataFlow::SsaDefinitionNode).getSsaVariable().getDefinition() instanceof SsaPhiNode
49+
not exists(SsaDefinition ssa |
50+
ssa = nd.(DataFlow::SsaDefinitionNode).getSsaVariable().getDefinition()
51+
|
52+
ssa instanceof SsaPhiNode or
53+
ssa instanceof SsaVariableCapture
54+
)
5055
}
5156

5257
/**
5358
* Holds if `metachar` is a meta-character that is used to escape special characters
5459
* into a form described by regular expression `regex`.
5560
*/
5661
predicate escapingScheme(string metachar, string regex) {
57-
metachar = "&" and regex = "&.*;"
62+
metachar = "&" and regex = "&.+;"
5863
or
59-
metachar = "%" and regex = "%.*"
64+
metachar = "%" and regex = "%.+"
6065
or
61-
metachar = "\\" and regex = "\\\\.*"
66+
metachar = "\\" and regex = "\\\\.+"
6267
}
6368

6469
/**
65-
* A call to `String.prototype.replace` that replaces all instances of a pattern.
70+
* A method call that performs string replacement.
6671
*/
67-
class Replacement extends DataFlow::Node {
68-
RegExpLiteral pattern;
72+
abstract class Replacement extends DataFlow::Node {
73+
/**
74+
* Holds if this replacement replaces the string `input` with `output`.
75+
*/
76+
abstract predicate replaces(string input, string output);
6977

70-
Replacement() {
71-
exists(DataFlow::MethodCallNode mcn | this = mcn |
72-
mcn.getMethodName() = "replace" and
73-
pattern.flow().(DataFlow::SourceNode).flowsTo(mcn.getArgument(0)) and
74-
mcn.getNumArgument() = 2 and
75-
pattern.isGlobal()
76-
)
77-
}
78+
/**
79+
* Gets the input of this replacement.
80+
*/
81+
abstract DataFlow::Node getInput();
7882

7983
/**
80-
* Holds if this replacement replaces the string `input` with `output`.
84+
* Gets the output of this replacement.
8185
*/
82-
predicate replaces(string input, string output) {
83-
exists(DataFlow::MethodCallNode mcn |
84-
mcn = this and
85-
input = getStringValue(pattern) and
86-
output = mcn.getArgument(1).getStringValue()
87-
)
88-
}
86+
abstract DataFlow::SourceNode getOutput();
8987

9088
/**
9189
* Holds if this replacement escapes `char` using `metachar`.
@@ -118,9 +116,12 @@ class Replacement extends DataFlow::Node {
118116
/**
119117
* Gets the previous replacement in this chain of replacements.
120118
*/
121-
Replacement getPreviousReplacement() {
122-
result = getASimplePredecessor*(this.(DataFlow::MethodCallNode).getReceiver())
123-
}
119+
Replacement getPreviousReplacement() { result.getOutput() = getASimplePredecessor*(getInput()) }
120+
121+
/**
122+
* Gets the next replacement in this chain of replacements.
123+
*/
124+
Replacement getNextReplacement() { this = result.getPreviousReplacement() }
124125

125126
/**
126127
* Gets an earlier replacement in this chain of replacements that
@@ -130,7 +131,9 @@ class Replacement extends DataFlow::Node {
130131
exists(Replacement pred | pred = this.getPreviousReplacement() |
131132
if pred.escapes(_, metachar)
132133
then result = pred
133-
else result = pred.getAnEarlierEscaping(metachar)
134+
else (
135+
not pred.unescapes(metachar, _) and result = pred.getAnEarlierEscaping(metachar)
136+
)
134137
)
135138
}
136139

@@ -142,9 +145,98 @@ class Replacement extends DataFlow::Node {
142145
exists(Replacement succ | this = succ.getPreviousReplacement() |
143146
if succ.unescapes(metachar, _)
144147
then result = succ
145-
else result = succ.getALaterUnescaping(metachar)
148+
else (
149+
not succ.escapes(_, metachar) and result = succ.getALaterUnescaping(metachar)
150+
)
151+
)
152+
}
153+
}
154+
155+
/**
156+
* A call to `String.prototype.replace` that replaces all instances of a pattern.
157+
*/
158+
class GlobalStringReplacement extends Replacement, DataFlow::MethodCallNode {
159+
RegExpLiteral pattern;
160+
161+
GlobalStringReplacement() {
162+
this.getMethodName() = "replace" and
163+
pattern.flow().(DataFlow::SourceNode).flowsTo(this.getArgument(0)) and
164+
this.getNumArgument() = 2 and
165+
pattern.isGlobal()
166+
}
167+
168+
override predicate replaces(string input, string output) {
169+
input = getStringValue(pattern) and
170+
output = this.getArgument(1).getStringValue()
171+
or
172+
exists(DataFlow::FunctionNode replacer, DataFlow::PropRead pr, DataFlow::ObjectLiteralNode map |
173+
replacer = getCallback(1) and
174+
replacer.getParameter(0).flowsToExpr(pr.getPropertyNameExpr()) and
175+
pr = map.getAPropertyRead() and
176+
pr.flowsTo(replacer.getAReturn()) and
177+
map.asExpr().(ObjectExpr).getPropertyByName(input).getInit().getStringValue() = output
146178
)
147179
}
180+
181+
override DataFlow::Node getInput() { result = this.getReceiver() }
182+
183+
override DataFlow::SourceNode getOutput() { result = this }
184+
}
185+
186+
/**
187+
* A call to `JSON.stringify`, viewed as a string replacement.
188+
*/
189+
class JsonStringifyReplacement extends Replacement, DataFlow::CallNode {
190+
JsonStringifyReplacement() { this = DataFlow::globalVarRef("JSON").getAMemberCall("stringify") }
191+
192+
override predicate replaces(string input, string output) {
193+
input = "\\" and output = "\\\\"
194+
// the other replacements are not relevant for this query
195+
}
196+
197+
override DataFlow::Node getInput() { result = this.getArgument(0) }
198+
199+
override DataFlow::SourceNode getOutput() { result = this }
200+
}
201+
202+
/**
203+
* A call to `JSON.parse`, viewed as a string replacement.
204+
*/
205+
class JsonParseReplacement extends Replacement {
206+
JsonParserCall self;
207+
208+
JsonParseReplacement() { this = self }
209+
210+
override predicate replaces(string input, string output) {
211+
input = "\\\\" and output = "\\"
212+
// the other replacements are not relevant for this query
213+
}
214+
215+
override DataFlow::Node getInput() { result = self.getInput() }
216+
217+
override DataFlow::SourceNode getOutput() { result = self.getOutput() }
218+
}
219+
220+
/**
221+
* A string replacement wrapped in a utility function.
222+
*/
223+
class WrappedReplacement extends Replacement, DataFlow::CallNode {
224+
int i;
225+
226+
Replacement inner;
227+
228+
WrappedReplacement() {
229+
exists(DataFlow::FunctionNode wrapped | wrapped.getFunction() = getACallee() |
230+
wrapped.getParameter(i).flowsTo(inner.getPreviousReplacement*().getInput()) and
231+
inner.getNextReplacement*().getOutput().flowsTo(wrapped.getAReturn())
232+
)
233+
}
234+
235+
override predicate replaces(string input, string output) { inner.replaces(input, output) }
236+
237+
override DataFlow::Node getInput() { result = getArgument(i) }
238+
239+
override DataFlow::SourceNode getOutput() { result = this }
148240
}
149241

150242
from Replacement primary, Replacement supplementary, string message, string metachar

javascript/ql/test/query-tests/Security/CWE-116/DoubleEscaping/DoubleEscaping.expected

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,8 @@
55
| tst.js:53:10:53:33 | s.repla ... , '\\\\') | This replacement may produce '\\' characters that are double-unescaped $@. | tst.js:53:10:54:33 | s.repla ... , '\\'') | here |
66
| tst.js:60:7:60:28 | s.repla ... '%25') | This replacement may double-escape '%' characters from $@. | tst.js:59:7:59:28 | s.repla ... '%26') | here |
77
| tst.js:68:10:70:38 | s.repla ... &amp;") | This replacement may double-escape '&' characters from $@. | tst.js:68:10:69:39 | s.repla ... apos;") | here |
8+
| tst.js:74:10:77:10 | JSON.st ... ) | This replacement may double-escape '\\' characters from $@. | tst.js:75:12:76:37 | s.repla ... u003E") | here |
9+
| tst.js:86:10:86:22 | JSON.parse(s) | This replacement may produce '\\' characters that are double-unescaped $@. | tst.js:86:10:86:47 | JSON.pa ... g, "<") | here |
10+
| tst.js:99:10:99:66 | s.repla ... &amp;") | This replacement may double-escape '&' characters from $@. | tst.js:99:10:99:43 | s.repla ... epl[c]) | here |
11+
| tst.js:107:10:107:53 | encodeD ... &amp;") | This replacement may double-escape '&' characters from $@. | tst.js:107:10:107:30 | encodeD ... otes(s) | here |
12+
| tst.js:115:10:115:47 | encodeQ ... &amp;") | This replacement may double-escape '&' characters from $@. | tst.js:115:10:115:24 | encodeQuotes(s) | here |

javascript/ql/test/query-tests/Security/CWE-116/DoubleEscaping/tst.js

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,68 @@ function badEncode(s) {
6969
.replace(indirect2, "&apos;")
7070
.replace(indirect3, "&amp;");
7171
}
72+
73+
function badEscape1(s) {
74+
return JSON.stringify(
75+
s.replace(/</g, "\\u003C")
76+
.replace(/>/g, "\\u003E")
77+
);
78+
}
79+
80+
function goodEscape1(s) {
81+
return JSON.stringify(s)
82+
.replace(/</g, "\\u003C").replace(/>/g, "\\u003E");
83+
}
84+
85+
function badUnescape2(s) {
86+
return JSON.parse(s).replace(/\\u003C/g, "<").replace(/\\u003E/g, ">");
87+
}
88+
89+
function goodUnescape2(s) {
90+
return JSON.parse(s.replace(/\\u003C/g, "<").replace(/\\u003E/g, ">"));
91+
}
92+
93+
function badEncodeWithReplacer(s) {
94+
var repl = {
95+
'"': "&quot;",
96+
"'": "&apos;",
97+
"&": "&amp;"
98+
};
99+
return s.replace(/["']/g, (c) => repl[c]).replace(/&/g, "&amp;");
100+
}
101+
102+
function encodeDoubleQuotes(s) {
103+
return s.replace(/"/g, "&quot;");
104+
}
105+
106+
function badWrappedEncode(s) {
107+
return encodeDoubleQuotes(s).replace(/&/g, "&amp;");
108+
}
109+
110+
function encodeQuotes(s) {
111+
return s.replace(/"/g, "&quot;").replace(/'/g, "&apos;");
112+
}
113+
114+
function badWrappedEncode2(s) {
115+
return encodeQuotes(s).replace(/&/g, "&amp;");
116+
}
117+
118+
function roundtrip(s) {
119+
return JSON.parse(JSON.stringify(s));
120+
}
121+
122+
// dubious, but out of scope for this query
123+
function badRoundtrip(s) {
124+
return s.replace(/\\\\/g, "\\").replace(/\\/g, "\\\\");
125+
}
126+
127+
function testWithCapturedVar(x) {
128+
var captured = x;
129+
(function() {
130+
captured = captured.replace(/\\/g, "\\\\");
131+
})();
132+
}
133+
134+
function cloneAndStringify(s) {
135+
return JSON.stringify(JSON.parse(JSON.stringify(s)));
136+
}

0 commit comments

Comments
 (0)