Skip to content

Commit ccbb7ce

Browse files
authored
Merge pull request #1224 from asger-semmle/cheerio
Approved by esben-semmle
2 parents a1cc2fb + e704139 commit ccbb7ce

File tree

7 files changed

+199
-24
lines changed

7 files changed

+199
-24
lines changed

change-notes/1.21/analysis-javascript.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
- [Firebase](https://firebase.google.com/)
1010
- [Express](https://expressjs.com/)
1111
- [shelljs](https://www.npmjs.com/package/shelljs)
12+
- [cheerio](https://www.npmjs.com/package/cheerio)
1213

1314
* The security queries now track data flow through Base64 decoders such as the Node.js `Buffer` class, the DOM function `atob`, and a number of npm packages intcluding [`abab`](https://www.npmjs.com/package/abab), [`atob`](https://www.npmjs.com/package/atob), [`btoa`](https://www.npmjs.com/package/btoa), [`base-64`](https://www.npmjs.com/package/base-64), [`js-base64`](https://www.npmjs.com/package/js-base64), [`Base64.js`](https://www.npmjs.com/package/Base64) and [`base64-js`](https://www.npmjs.com/package/base64-js).
1415

javascript/ql/src/javascript.qll

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ import semmle.javascript.frameworks.AsyncPackage
6262
import semmle.javascript.frameworks.AWS
6363
import semmle.javascript.frameworks.Azure
6464
import semmle.javascript.frameworks.Babel
65+
import semmle.javascript.frameworks.Cheerio
6566
import semmle.javascript.frameworks.ComposedFunctions
6667
import semmle.javascript.frameworks.ClientRequests
6768
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 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 that of a jQuery object.
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 method `name` 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 method `name` 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+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
test_CheerioRef
2+
| tst.js:1:1:1:30 | import ... eerio"; |
3+
| tst.js:1:8:1:14 | cheerio |
4+
| tst.js:4:11:4:23 | getTemplate() |
5+
| tst.js:9:10:9:36 | cheerio ... t</b>') |
6+
test_CheerioObjectCreation
7+
| tst.js:5:3:5:18 | $.attr('foo', 5) |
8+
| tst.js:9:10:9:36 | cheerio ... t</b>') |
9+
test_AttributeDefinition
10+
| tst.js:5:3:5:18 | $.attr('foo', 5) |
11+
test_CheerioObjectRef
12+
| tst.js:4:11:4:23 | getTemplate() |
13+
| tst.js:5:3:5:18 | $.attr('foo', 5) |
14+
| tst.js:5:3:5:33 | $.attr( ... ar', 6) |
15+
| tst.js:5:3:5:41 | $.attr( ... html(x) |
16+
| tst.js:9:10:9:36 | cheerio ... t</b>') |
17+
test_XssSink
18+
| tst.js:5:40:5:40 | x |
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import javascript
2+
3+
query DataFlow::Node test_CheerioRef() { result = Cheerio::cheerioRef() }
4+
5+
query Cheerio::CheerioObjectCreation test_CheerioObjectCreation() { any() }
6+
7+
query DOM::AttributeDefinition test_AttributeDefinition() { any() }
8+
9+
query DataFlow::Node test_CheerioObjectRef() { result = Cheerio::cheerioObjectRef() }
10+
11+
query Cheerio::XssSink test_XssSink() { any() }
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import cheerio from "cheerio";
2+
3+
function test(x) {
4+
let $ = getTemplate();
5+
$.attr('foo', 5).data('bar', 6).html(x);
6+
}
7+
8+
function getTemplate() {
9+
return cheerio.load('<b>text</b>');
10+
}

0 commit comments

Comments
 (0)