Skip to content

Commit 2f3869f

Browse files
committed
add model for puppeteer
1 parent fbbec5d commit 2f3869f

File tree

8 files changed

+226
-0
lines changed

8 files changed

+226
-0
lines changed

javascript/ql/src/javascript.qll

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ import semmle.javascript.frameworks.Next
102102
import semmle.javascript.frameworks.NoSQL
103103
import semmle.javascript.frameworks.PkgCloud
104104
import semmle.javascript.frameworks.PropertyProjection
105+
import semmle.javascript.frameworks.Puppeteer
105106
import semmle.javascript.frameworks.React
106107
import semmle.javascript.frameworks.ReactNative
107108
import semmle.javascript.frameworks.Request
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import javascript
2+
3+
/**
4+
* Classes and predicates modelling the `puppeteer` library.
5+
*/
6+
module Puppeteer {
7+
/**
8+
* A reference to a module import of puppeteer.
9+
*/
10+
private API::Node puppeteer() { result = API::moduleImport(["puppeteer", "puppeteer-core"]) }
11+
12+
private class BrowserTypeEntryPoint extends API::EntryPoint {
13+
BrowserTypeEntryPoint() { this = "PuppeteerBrowserTypeEntryPoint" }
14+
15+
override DataFlow::SourceNode getAUse() { result.hasUnderlyingType("puppeteer", "Browser") }
16+
17+
override DataFlow::Node getARhs() { none() }
18+
}
19+
20+
/**
21+
* A reference to a `Browser` from puppeteer.
22+
*/
23+
private API::Node browser() {
24+
result = API::root().getASuccessor(any(BrowserTypeEntryPoint b))
25+
or
26+
result = puppeteer().getMember(["launch", "connect"]).getReturn().getPromised()
27+
or
28+
result = [page(), context(), target()].getMember("browser").getReturn()
29+
}
30+
31+
private class PageTypeEntryPoint extends API::EntryPoint {
32+
PageTypeEntryPoint() { this = "PuppeteerPageTypeEntryPoint" }
33+
34+
override DataFlow::SourceNode getAUse() { result.hasUnderlyingType("puppeteer", "Page") }
35+
36+
override DataFlow::Node getARhs() { none() }
37+
}
38+
39+
/**
40+
* A reference to a `Page` from puppeteer.
41+
*/
42+
API::Node page() {
43+
result = API::root().getASuccessor(any(PageTypeEntryPoint b))
44+
or
45+
result = [browser(), context()].getMember("newPage").getReturn().getPromised()
46+
or
47+
result = [browser(), context()].getMember("pages").getReturn().getPromised().getUnknownMember()
48+
or
49+
result = target().getMember("page").getReturn().getPromised()
50+
}
51+
52+
private class TargetTypeEntryPoint extends API::EntryPoint {
53+
TargetTypeEntryPoint() { this = "PuppeteerTargetTypeEntryPoint" }
54+
55+
override DataFlow::SourceNode getAUse() { result.hasUnderlyingType("puppeteer", "Target") }
56+
57+
override DataFlow::Node getARhs() { none() }
58+
}
59+
60+
/**
61+
* A reference to a `Target` from puppeteer.
62+
*/
63+
private API::Node target() {
64+
result = API::root().getASuccessor(any(TargetTypeEntryPoint b))
65+
or
66+
result = [page(), browser()].getMember("target").getReturn()
67+
or
68+
result = context().getMember("targets").getReturn().getUnknownMember()
69+
or
70+
result = target().getMember("opener").getReturn()
71+
}
72+
73+
private class ContextTypeEntryPoint extends API::EntryPoint {
74+
ContextTypeEntryPoint() { this = "PuppeteerContextTypeEntryPoint" }
75+
76+
override DataFlow::SourceNode getAUse() {
77+
result.hasUnderlyingType("puppeteer", "BrowserContext")
78+
}
79+
80+
override DataFlow::Node getARhs() { none() }
81+
}
82+
83+
/**
84+
* A reference to a `BrowserContext` from puppeteer.
85+
*/
86+
private API::Node context() {
87+
result = API::root().getASuccessor(any(ContextTypeEntryPoint b))
88+
or
89+
result = [page(), target()].getMember("browserContext").getReturn()
90+
or
91+
result = browser().getMember("browserContexts").getReturn().getUnknownMember()
92+
or
93+
result = browser().getMember("createIncognitoBrowserContext").getReturn().getPromised()
94+
or
95+
result = browser().getMember("defaultBrowserContext").getReturn()
96+
}
97+
98+
/**
99+
* A call requesting a `Page` to navigate to some url, seen as a `ClientRequest`.
100+
*/
101+
private class PuppeteerGotoCall extends ClientRequest::Range, API::InvokeNode {
102+
PuppeteerGotoCall() { this = page().getMember("goto").getACall() }
103+
104+
override DataFlow::Node getUrl() { result = getArgument(0) }
105+
106+
override DataFlow::Node getHost() { none() }
107+
108+
override DataFlow::Node getADataNode() { none() }
109+
}
110+
111+
/**
112+
* A call requesting a `Page` to load a stylesheet or script, seen as a `ClientRequest`.
113+
*/
114+
private class PuppeteerLoadResourceCall extends ClientRequest::Range, API::InvokeNode {
115+
PuppeteerLoadResourceCall() {
116+
this = page().getMember(["addStyleTag", "addScriptTag"]).getACall()
117+
}
118+
119+
override DataFlow::Node getUrl() { result = getParameter(0).getMember("url").getARhs() }
120+
121+
override DataFlow::Node getHost() { none() }
122+
123+
override DataFlow::Node getADataNode() { none() }
124+
}
125+
}

javascript/ql/src/semmle/javascript/security/dataflow/TaintedPathCustomizations.qll

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -631,6 +631,20 @@ module TaintedPath {
631631
SendPathSink() { this = DataFlow::moduleImport("send").getACall().getArgument(1) }
632632
}
633633

634+
/**
635+
* A path argument given to a `Page` in puppeteer, specifying where a pdf/screenshot should be saved.
636+
*/
637+
private class PuppeteerPath extends TaintedPath::Sink {
638+
PuppeteerPath() {
639+
this =
640+
Puppeteer::page()
641+
.getMember(["pdf", "screenshot"])
642+
.getParameter(0)
643+
.getMember("path")
644+
.getARhs()
645+
}
646+
}
647+
634648
/**
635649
* Holds if there is a step `src -> dst` mapping `srclabel` to `dstlabel` relevant for path traversal vulnerabilities.
636650
*/

javascript/ql/test/library-tests/frameworks/ClientRequests/ClientRequests.expected

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ test_ClientRequest
55
| apollo.js:17:1:17:34 | new Pre ... yurl"}) |
66
| apollo.js:20:1:20:77 | createN ... phql'}) |
77
| apollo.js:23:1:23:31 | new Web ... wsUri}) |
8+
| puppeteer.ts:6:11:6:42 | page.go ... e.com') |
9+
| puppeteer.ts:8:5:8:61 | page.ad ... css" }) |
10+
| puppeteer.ts:18:30:18:50 | page.go ... estUrl) |
811
| tst.js:11:5:11:16 | request(url) |
912
| tst.js:13:5:13:20 | request.get(url) |
1013
| tst.js:15:5:15:23 | request.delete(url) |
@@ -136,6 +139,9 @@ test_getUrl
136139
| apollo.js:17:1:17:34 | new Pre ... yurl"}) | apollo.js:17:26:17:32 | "myurl" |
137140
| apollo.js:20:1:20:77 | createN ... phql'}) | apollo.js:20:30:20:75 | 'https: ... raphql' |
138141
| apollo.js:23:1:23:31 | new Web ... wsUri}) | apollo.js:23:25:23:29 | wsUri |
142+
| puppeteer.ts:6:11:6:42 | page.go ... e.com') | puppeteer.ts:6:21:6:41 | 'https: ... le.com' |
143+
| puppeteer.ts:8:5:8:61 | page.ad ... css" }) | puppeteer.ts:8:29:8:58 | "http:/ ... le.css" |
144+
| puppeteer.ts:18:30:18:50 | page.go ... estUrl) | puppeteer.ts:18:40:18:49 | requestUrl |
139145
| tst.js:11:5:11:16 | request(url) | tst.js:11:13:11:15 | url |
140146
| tst.js:13:5:13:20 | request.get(url) | tst.js:13:17:13:19 | url |
141147
| tst.js:15:5:15:23 | request.delete(url) | tst.js:15:20:15:22 | url |
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import * as puppeteer from 'puppeteer';
2+
3+
(async () => {
4+
const browser = await puppeteer.launch();
5+
const page = await browser.newPage();
6+
await page.goto('https://example.com');
7+
8+
page.addStyleTag({ url: "http://example.org/style.css" })
9+
})();
10+
11+
class Renderer {
12+
private browser: puppeteer.Browser;
13+
constructor(browser: puppeteer.Browser) {
14+
this.browser = browser;
15+
}
16+
async foo(requestUrl: string): Promise<void> {
17+
const page = await this.browser.newPage();
18+
let response = await page.goto(requestUrl);
19+
}
20+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}

javascript/ql/test/query-tests/Security/CWE-022/TaintedPath/TaintedPath.expected

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2168,6 +2168,24 @@ nodes
21682168
| other-fs-libraries.js:42:53:42:56 | path |
21692169
| other-fs-libraries.js:42:53:42:56 | path |
21702170
| other-fs-libraries.js:42:53:42:56 | path |
2171+
| pupeteer.js:5:9:5:71 | tainted |
2172+
| pupeteer.js:5:9:5:71 | tainted |
2173+
| pupeteer.js:5:9:5:71 | tainted |
2174+
| pupeteer.js:5:19:5:71 | "dir/" ... t.data" |
2175+
| pupeteer.js:5:19:5:71 | "dir/" ... t.data" |
2176+
| pupeteer.js:5:19:5:71 | "dir/" ... t.data" |
2177+
| pupeteer.js:5:28:5:53 | parseTo ... t).name |
2178+
| pupeteer.js:5:28:5:53 | parseTo ... t).name |
2179+
| pupeteer.js:5:28:5:53 | parseTo ... t).name |
2180+
| pupeteer.js:5:28:5:53 | parseTo ... t).name |
2181+
| pupeteer.js:9:28:9:34 | tainted |
2182+
| pupeteer.js:9:28:9:34 | tainted |
2183+
| pupeteer.js:9:28:9:34 | tainted |
2184+
| pupeteer.js:9:28:9:34 | tainted |
2185+
| pupeteer.js:13:37:13:43 | tainted |
2186+
| pupeteer.js:13:37:13:43 | tainted |
2187+
| pupeteer.js:13:37:13:43 | tainted |
2188+
| pupeteer.js:13:37:13:43 | tainted |
21712189
| tainted-access-paths.js:6:7:6:48 | path |
21722190
| tainted-access-paths.js:6:7:6:48 | path |
21732191
| tainted-access-paths.js:6:7:6:48 | path |
@@ -6403,6 +6421,27 @@ edges
64036421
| other-fs-libraries.js:38:24:38:30 | req.url | other-fs-libraries.js:38:14:38:37 | url.par ... , true) |
64046422
| other-fs-libraries.js:38:24:38:30 | req.url | other-fs-libraries.js:38:14:38:37 | url.par ... , true) |
64056423
| other-fs-libraries.js:38:24:38:30 | req.url | other-fs-libraries.js:38:14:38:37 | url.par ... , true) |
6424+
| pupeteer.js:5:9:5:71 | tainted | pupeteer.js:9:28:9:34 | tainted |
6425+
| pupeteer.js:5:9:5:71 | tainted | pupeteer.js:9:28:9:34 | tainted |
6426+
| pupeteer.js:5:9:5:71 | tainted | pupeteer.js:9:28:9:34 | tainted |
6427+
| pupeteer.js:5:9:5:71 | tainted | pupeteer.js:9:28:9:34 | tainted |
6428+
| pupeteer.js:5:9:5:71 | tainted | pupeteer.js:9:28:9:34 | tainted |
6429+
| pupeteer.js:5:9:5:71 | tainted | pupeteer.js:9:28:9:34 | tainted |
6430+
| pupeteer.js:5:9:5:71 | tainted | pupeteer.js:13:37:13:43 | tainted |
6431+
| pupeteer.js:5:9:5:71 | tainted | pupeteer.js:13:37:13:43 | tainted |
6432+
| pupeteer.js:5:9:5:71 | tainted | pupeteer.js:13:37:13:43 | tainted |
6433+
| pupeteer.js:5:9:5:71 | tainted | pupeteer.js:13:37:13:43 | tainted |
6434+
| pupeteer.js:5:9:5:71 | tainted | pupeteer.js:13:37:13:43 | tainted |
6435+
| pupeteer.js:5:9:5:71 | tainted | pupeteer.js:13:37:13:43 | tainted |
6436+
| pupeteer.js:5:19:5:71 | "dir/" ... t.data" | pupeteer.js:5:9:5:71 | tainted |
6437+
| pupeteer.js:5:19:5:71 | "dir/" ... t.data" | pupeteer.js:5:9:5:71 | tainted |
6438+
| pupeteer.js:5:19:5:71 | "dir/" ... t.data" | pupeteer.js:5:9:5:71 | tainted |
6439+
| pupeteer.js:5:28:5:53 | parseTo ... t).name | pupeteer.js:5:19:5:71 | "dir/" ... t.data" |
6440+
| pupeteer.js:5:28:5:53 | parseTo ... t).name | pupeteer.js:5:19:5:71 | "dir/" ... t.data" |
6441+
| pupeteer.js:5:28:5:53 | parseTo ... t).name | pupeteer.js:5:19:5:71 | "dir/" ... t.data" |
6442+
| pupeteer.js:5:28:5:53 | parseTo ... t).name | pupeteer.js:5:19:5:71 | "dir/" ... t.data" |
6443+
| pupeteer.js:5:28:5:53 | parseTo ... t).name | pupeteer.js:5:19:5:71 | "dir/" ... t.data" |
6444+
| pupeteer.js:5:28:5:53 | parseTo ... t).name | pupeteer.js:5:19:5:71 | "dir/" ... t.data" |
64066445
| tainted-access-paths.js:6:7:6:48 | path | tainted-access-paths.js:8:19:8:22 | path |
64076446
| tainted-access-paths.js:6:7:6:48 | path | tainted-access-paths.js:8:19:8:22 | path |
64086447
| tainted-access-paths.js:6:7:6:48 | path | tainted-access-paths.js:8:19:8:22 | path |
@@ -8007,6 +8046,8 @@ edges
80078046
| other-fs-libraries.js:40:35:40:38 | path | other-fs-libraries.js:38:24:38:30 | req.url | other-fs-libraries.js:40:35:40:38 | path | This path depends on $@. | other-fs-libraries.js:38:24:38:30 | req.url | a user-provided value |
80088047
| other-fs-libraries.js:41:50:41:53 | path | other-fs-libraries.js:38:24:38:30 | req.url | other-fs-libraries.js:41:50:41:53 | path | This path depends on $@. | other-fs-libraries.js:38:24:38:30 | req.url | a user-provided value |
80098048
| other-fs-libraries.js:42:53:42:56 | path | other-fs-libraries.js:38:24:38:30 | req.url | other-fs-libraries.js:42:53:42:56 | path | This path depends on $@. | other-fs-libraries.js:38:24:38:30 | req.url | a user-provided value |
8049+
| pupeteer.js:9:28:9:34 | tainted | pupeteer.js:5:28:5:53 | parseTo ... t).name | pupeteer.js:9:28:9:34 | tainted | This path depends on $@. | pupeteer.js:5:28:5:53 | parseTo ... t).name | a user-provided value |
8050+
| pupeteer.js:13:37:13:43 | tainted | pupeteer.js:5:28:5:53 | parseTo ... t).name | pupeteer.js:13:37:13:43 | tainted | This path depends on $@. | pupeteer.js:5:28:5:53 | parseTo ... t).name | a user-provided value |
80108051
| tainted-access-paths.js:8:19:8:22 | path | tainted-access-paths.js:6:24:6:30 | req.url | tainted-access-paths.js:8:19:8:22 | path | This path depends on $@. | tainted-access-paths.js:6:24:6:30 | req.url | a user-provided value |
80118052
| tainted-access-paths.js:12:19:12:25 | obj.sub | tainted-access-paths.js:6:24:6:30 | req.url | tainted-access-paths.js:12:19:12:25 | obj.sub | This path depends on $@. | tainted-access-paths.js:6:24:6:30 | req.url | a user-provided value |
80128053
| tainted-access-paths.js:26:19:26:26 | obj.sub3 | tainted-access-paths.js:6:24:6:30 | req.url | tainted-access-paths.js:26:19:26:26 | obj.sub3 | This path depends on $@. | tainted-access-paths.js:6:24:6:30 | req.url | a user-provided value |
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
const puppeteer = require('puppeteer');
2+
const parseTorrent = require('parse-torrent');
3+
4+
(async () => {
5+
let tainted = "dir/" + parseTorrent(torrent).name + ".torrent.data";
6+
7+
const browser = await puppeteer.launch();
8+
const page = await browser.newPage();
9+
await page.pdf({ path: tainted, format: 'a4' });
10+
11+
const pages = await browser.pages();
12+
for (let i = 0; i < something(); i++) {
13+
pages[i].screenshot({ path: tainted });
14+
}
15+
16+
await browser.close();
17+
})();
18+

0 commit comments

Comments
 (0)