Skip to content

Commit 0fcdf91

Browse files
committed
JS: Add model of cheerio
1 parent e7adb62 commit 0fcdf91

3 files changed

Lines changed: 159 additions & 24 deletions

File tree

javascript/ql/src/javascript.qll

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ import semmle.javascript.frameworks.AsyncPackage
6060
import semmle.javascript.frameworks.AWS
6161
import semmle.javascript.frameworks.Azure
6262
import semmle.javascript.frameworks.Babel
63+
import semmle.javascript.frameworks.Cheerio
6364
import semmle.javascript.frameworks.ComposedFunctions
6465
import semmle.javascript.frameworks.ClientRequests
6566
import semmle.javascript.frameworks.ClosureLibrary
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/**
2+
* Provides a model of `cheerio`, a server-side DOM manipulation library like with a jQuery-like API.
3+
*/
4+
5+
import javascript
6+
private import semmle.javascript.security.dataflow.Xss as Xss
7+
8+
module Cheerio {
9+
/**
10+
* A reference to the `cheerio` function, possibly with a loaded DOM.
11+
*/
12+
private DataFlow::SourceNode cheerioRef(DataFlow::TypeTracker t) {
13+
t.start() and
14+
(
15+
result = DataFlow::moduleImport("cheerio")
16+
or
17+
exists(string name | result = cheerioRef().getAMemberCall(name) |
18+
name = "load" or
19+
name = "parseHTML"
20+
)
21+
)
22+
or
23+
exists(DataFlow::TypeTracker t2 | result = cheerioRef(t2).track(t2, t))
24+
}
25+
26+
/**
27+
* A reference to the `cheerio` function, possibly with a loaded DOM.
28+
*/
29+
DataFlow::SourceNode cheerioRef() { result = cheerioRef(DataFlow::TypeTracker::end()) }
30+
31+
/**
32+
* Creation of `cheerio` object, a collection of virtual DOM elements
33+
* with an interface similar to jQuery objects.
34+
*/
35+
class CheerioObjectCreation extends DataFlow::SourceNode {
36+
CheerioObjectCreation::Range range;
37+
38+
CheerioObjectCreation() { this = range }
39+
}
40+
41+
module CheerioObjectCreation {
42+
/**
43+
* Creation of a `cheerio` object.
44+
*/
45+
abstract class Range extends DataFlow::SourceNode { }
46+
47+
private class DefaultRange extends Range {
48+
DefaultRange() {
49+
this = cheerioRef().getACall()
50+
or
51+
this = cheerioRef().getAMethodCall()
52+
}
53+
}
54+
}
55+
56+
/**
57+
* Gets a reference to a `cheerio` object, a collection of virtual DOM elements
58+
* with an interface similar to jQuery objects.
59+
*/
60+
private DataFlow::SourceNode cheerioObjectRef(DataFlow::TypeTracker t) {
61+
t.start() and
62+
result instanceof CheerioObjectCreation
63+
or
64+
// Chainable calls.
65+
t.start() and
66+
exists(DataFlow::MethodCallNode call, string name |
67+
call = cheerioObjectRef().getAMethodCall(name) and
68+
result = call
69+
|
70+
if name = "attr" or name = "data" or name = "prop" or name = "css"
71+
then call.getNumArgument() = 2
72+
else
73+
if name = "val" or name = "html" or name = "text"
74+
then call.getNumArgument() = 1
75+
else (
76+
name != "toString" and
77+
name != "toArray" and
78+
name != "hasClass"
79+
)
80+
)
81+
or
82+
exists(DataFlow::TypeTracker t2 | result = cheerioObjectRef(t2).track(t2, t))
83+
}
84+
85+
/**
86+
* Gets a reference to a `cheerio` object, a collection of virtual DOM elements
87+
* with an interface similar to jQuery objects.
88+
*/
89+
DataFlow::SourceNode cheerioObjectRef() {
90+
result = cheerioObjectRef(DataFlow::TypeTracker::end())
91+
}
92+
93+
/**
94+
* Definition of a DOM attribute through `cheerio`.
95+
*/
96+
class AttributeDef extends DOM::AttributeDefinition {
97+
DataFlow::CallNode call;
98+
99+
AttributeDef() {
100+
this = call.asExpr() and
101+
call = cheerioObjectRef().getAMethodCall("attr") and
102+
call.getNumArgument() >= 2
103+
}
104+
105+
override string getName() { call.getArgument(0).mayHaveStringValue(result) }
106+
107+
override DataFlow::Node getValueNode() { result = call.getArgument(1) }
108+
}
109+
110+
/**
111+
* XSS sink through `cheerio`.
112+
*/
113+
class XssSink extends Xss::DomBasedXss::Sink {
114+
XssSink() {
115+
exists(string name | this = cheerioObjectRef().getAMethodCall(name).getAnArgument() |
116+
JQuery::isMethodArgumentInterpretedAsHtml(name)
117+
)
118+
}
119+
}
120+
}

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

Lines changed: 38 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -66,21 +66,7 @@ class JQueryMethodCall extends CallExpr {
6666
*/
6767
predicate interpretsArgumentAsHtml(Expr e) {
6868
// some methods interpret all their arguments as (potential) HTML
69-
(
70-
name = "after" or
71-
name = "append" or
72-
name = "appendTo" or
73-
name = "before" or
74-
name = "html" or
75-
name = "insertAfter" or
76-
name = "insertBefore" or
77-
name = "prepend" or
78-
name = "prependTo" or
79-
name = "replaceWith" or
80-
name = "wrap" or
81-
name = "wrapAll" or
82-
name = "wrapInner"
83-
) and
69+
JQuery::isMethodArgumentInterpretedAsHtml(name) and
8470
e = getAnArgument()
8571
or
8672
// for `$, it's only the first one
@@ -97,15 +83,7 @@ class JQueryMethodCall extends CallExpr {
9783
*/
9884
predicate interpretsArgumentAsSelector(Expr e) {
9985
// some methods interpret all their arguments as (potential) selectors
100-
(
101-
name = "appendTo" or
102-
name = "insertAfter" or
103-
name = "insertBefore" or
104-
name = "prependTo" or
105-
name = "wrap" or
106-
name = "wrapAll" or
107-
name = "wrapInner"
108-
) and
86+
JQuery::isMethodArgumentInterpretedAsSelector(name) and
10987
e = getAnArgument()
11088
or
11189
// for `$, it's only the first one
@@ -308,3 +286,39 @@ private class JQueryClientRequest extends CustomClientRequest {
308286

309287
override DataFlow::Node getADataNode() { result = getOptionArgument([0 .. 1], "data") }
310288
}
289+
290+
module JQuery {
291+
/**
292+
* Holds if the given method on a jQuery object may interpret any of its
293+
* arguments as HTML.
294+
*/
295+
predicate isMethodArgumentInterpretedAsHtml(string name) {
296+
name = "after" or
297+
name = "append" or
298+
name = "appendTo" or
299+
name = "before" or
300+
name = "html" or
301+
name = "insertAfter" or
302+
name = "insertBefore" or
303+
name = "prepend" or
304+
name = "prependTo" or
305+
name = "replaceWith" or
306+
name = "wrap" or
307+
name = "wrapAll" or
308+
name = "wrapInner"
309+
}
310+
311+
/**
312+
* Holds if the given method on a jQuery object may interpret any of its
313+
* arguments as a selector.
314+
*/
315+
predicate isMethodArgumentInterpretedAsSelector(string name) {
316+
name = "appendTo" or
317+
name = "insertAfter" or
318+
name = "insertBefore" or
319+
name = "prependTo" or
320+
name = "wrap" or
321+
name = "wrapAll" or
322+
name = "wrapInner"
323+
}
324+
}

0 commit comments

Comments
 (0)