Skip to content

Commit 179a7a8

Browse files
authored
Merge pull request github#5098 from erik-krogh/xml2js
Approved by asgerf
2 parents 402f20c + 3ee0029 commit 179a7a8

File tree

4 files changed

+179
-3
lines changed

4 files changed

+179
-3
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
lgtm,codescanning
2+
* The security queries now track taint through XML parsers.
3+
Affected packages are
4+
[xml2js](https://www.npmjs.com/package/xml2js),
5+
[sax](https://www.npmjs.com/package/sax),
6+
[xml-js](https://www.npmjs.com/package/xml-js),
7+
[htmlparser2](https://www.npmjs.com/package/htmlparser2), and
8+
[node-expat](https://www.npmjs.com/package/node-expat)

javascript/ql/src/semmle/javascript/frameworks/XmlParsers.qll

Lines changed: 129 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ module XML {
2525

2626
/** Holds if this call to the XML parser resolves entities of the given `kind`. */
2727
abstract predicate resolvesEntities(EntityKind kind);
28+
29+
/** Gets a reference to a value resulting from parsing the XML. */
30+
js::DataFlow::Node getAResult() { none() }
2831
}
2932

3033
/**
@@ -98,10 +101,11 @@ module XML {
98101
* An invocation of `expat.Parser.parse` or `expat.Parser.write`.
99102
*/
100103
class ExpatParserInvocation extends ParserInvocation {
104+
js::DataFlow::NewNode parser;
105+
101106
ExpatParserInvocation() {
102-
exists(string m | m = "parse" or m = "write" |
103-
this = moduleMethodCall("node-expat", "Parser", m)
104-
)
107+
parser = js::DataFlow::moduleMember("node-expat", "Parser").getAnInstantiation() and
108+
this = parser.getAMemberCall(["parse", "write"]).asExpr()
105109
}
106110

107111
override js::Expr getSourceArgument() { result = getArgument(0) }
@@ -110,6 +114,10 @@ module XML {
110114
// only internal entities are resolved by default
111115
kind = InternalEntity()
112116
}
117+
118+
override js::DataFlow::Node getAResult() {
119+
result = parser.getAMemberCall("on").getABoundCallbackParameter(1, _)
120+
}
113121
}
114122

115123
/**
@@ -160,4 +168,122 @@ module XML {
160168

161169
override predicate resolvesEntities(XML::EntityKind kind) { kind = InternalEntity() }
162170
}
171+
172+
/**
173+
* An invocation of `xml2js`.
174+
*/
175+
private class Xml2JSInvocation extends XML::ParserInvocation {
176+
js::DataFlow::CallNode call;
177+
178+
Xml2JSInvocation() {
179+
exists(js::API::Node imp | imp = js::API::moduleImport("xml2js") |
180+
call = [imp, imp.getMember("Parser").getInstance()].getMember("parseString").getACall() and
181+
this = call.asExpr()
182+
)
183+
}
184+
185+
override js::Expr getSourceArgument() { result = getArgument(0) }
186+
187+
override predicate resolvesEntities(XML::EntityKind kind) {
188+
// sax-js (the parser used) does not expand entities.
189+
none()
190+
}
191+
192+
override js::DataFlow::Node getAResult() {
193+
result = call.getABoundCallbackParameter(call.getNumArgument() - 1, 1)
194+
}
195+
}
196+
197+
/**
198+
* An invocation of `sax`.
199+
*/
200+
private class SaxInvocation extends XML::ParserInvocation {
201+
js::DataFlow::InvokeNode parser;
202+
203+
SaxInvocation() {
204+
exists(js::API::Node imp | imp = js::API::moduleImport("sax") |
205+
parser = imp.getMember("parser").getACall()
206+
or
207+
parser = imp.getMember("SAXParser").getAnInstantiation()
208+
) and
209+
this = parser.getAMemberCall("write").asExpr()
210+
}
211+
212+
override js::Expr getSourceArgument() { result = getArgument(0) }
213+
214+
override predicate resolvesEntities(XML::EntityKind kind) {
215+
// sax-js does not expand entities.
216+
none()
217+
}
218+
219+
override js::DataFlow::Node getAResult() {
220+
result =
221+
parser
222+
.getAPropertyWrite(any(string s | s.matches("on%")))
223+
.getRhs()
224+
.getAFunctionValue()
225+
.getAParameter()
226+
}
227+
}
228+
229+
/**
230+
* An invocation of `xml-js`.
231+
*/
232+
private class XmlJSInvocation extends XML::ParserInvocation {
233+
XmlJSInvocation() {
234+
this =
235+
js::DataFlow::moduleMember("xml-js", ["xml2json", "xml2js", "json2xml", "js2xml"])
236+
.getACall()
237+
.asExpr()
238+
}
239+
240+
override js::Expr getSourceArgument() { result = getArgument(0) }
241+
242+
override predicate resolvesEntities(XML::EntityKind kind) {
243+
// xml-js does not expand custom entities.
244+
none()
245+
}
246+
247+
override js::DataFlow::Node getAResult() { result.asExpr() = this }
248+
}
249+
250+
/**
251+
* An invocation of `htmlparser2`.
252+
*/
253+
private class HtmlParser2Invocation extends XML::ParserInvocation {
254+
js::DataFlow::NewNode parser;
255+
256+
HtmlParser2Invocation() {
257+
parser = js::DataFlow::moduleMember("htmlparser2", "Parser").getAnInstantiation() and
258+
this = parser.getAMemberCall("write").asExpr()
259+
}
260+
261+
override js::Expr getSourceArgument() { result = getArgument(0) }
262+
263+
override predicate resolvesEntities(XML::EntityKind kind) {
264+
// htmlparser2 does not expand entities.
265+
none()
266+
}
267+
268+
override js::DataFlow::Node getAResult() {
269+
result =
270+
parser
271+
.getArgument(0)
272+
.getALocalSource()
273+
.getAPropertySource()
274+
.getAFunctionValue()
275+
.getAParameter()
276+
}
277+
}
278+
279+
private class XMLParserTaintStep extends js::TaintTracking::AdditionalTaintStep {
280+
XML::ParserInvocation parser;
281+
282+
XMLParserTaintStep() { this.asExpr() = parser }
283+
284+
override predicate step(js::DataFlow::Node pred, js::DataFlow::Node succ) {
285+
pred.asExpr() = parser.getSourceArgument() and
286+
succ = parser.getAResult()
287+
}
288+
}
163289
}

javascript/ql/test/library-tests/TaintTracking/BasicTaintTracking.expected

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,3 +145,8 @@ typeInferenceMismatch
145145
| tst.js:2:13:2:20 | source() | tst.js:45:10:45:24 | x.map(x2 => x2) |
146146
| tst.js:2:13:2:20 | source() | tst.js:47:10:47:30 | Buffer. ... 'hex') |
147147
| tst.js:2:13:2:20 | source() | tst.js:48:10:48:22 | new Buffer(x) |
148+
| xml.js:5:18:5:25 | source() | xml.js:8:14:8:17 | text |
149+
| xml.js:12:17:12:24 | source() | xml.js:13:14:13:19 | result |
150+
| xml.js:23:18:23:25 | source() | xml.js:20:14:20:17 | attr |
151+
| xml.js:26:27:26:34 | source() | xml.js:26:10:26:39 | convert ... (), {}) |
152+
| xml.js:34:18:34:25 | source() | xml.js:31:18:31:21 | name |
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
(function () {
2+
var Parser = require("node-expat").Parser
3+
var parser = new Parser();
4+
5+
parser.write(source());
6+
7+
parser.on("text", text => {
8+
sink(text); // NOT OK
9+
});
10+
11+
var parseString = require('xml2js').parseString;
12+
parseString(source(), function (err, result) {
13+
sink(result); // NOT OK
14+
});
15+
16+
var sax = require("sax");
17+
var parser = sax.parser(strict);
18+
19+
parser.onattribute = function (attr) {
20+
sink(attr); // NOT OK
21+
};
22+
23+
parser.write(source()).close();
24+
25+
var convert = require('xml-js');
26+
sink(convert.xml2json(source(), {})); // NOT OK
27+
28+
const htmlparser2 = require("htmlparser2");
29+
const parser = new htmlparser2.Parser({
30+
onopentag(name, attributes) {
31+
sink(name) // NOT OK
32+
}
33+
});
34+
parser.write(source());
35+
parser.end();
36+
37+
})();

0 commit comments

Comments
 (0)