Skip to content

Commit 3b117f5

Browse files
authored
Merge pull request github#5419 from erik-krogh/forgery
Approved by asgerf
2 parents 98c1aa5 + ed8e0fb commit 3b117f5

File tree

12 files changed

+227
-62
lines changed

12 files changed

+227
-62
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
lgtm,codescanning
2+
* URIs used in the puppeteer library are now recognized as sinks for `js/request-forgery`.
3+
Affected packages are
4+
[puppeteer](https://www.npmjs.com/package/puppeteer)

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

javascript/ql/src/semmle/javascript/ApiGraphs.qll

Lines changed: 18 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -313,10 +313,7 @@ module API {
313313
module Node {
314314
/** Gets a node whose type has the given qualified name. */
315315
Node ofType(string moduleName, string exportedName) {
316-
exists(TypeName tn |
317-
tn.hasQualifiedName(moduleName, exportedName) and
318-
result = Impl::MkCanonicalNameUse(tn).(Node).getInstance()
319-
)
316+
result = Impl::MkHasUnderlyingType(moduleName, exportedName).(Node).getInstance()
320317
}
321318
}
322319

@@ -384,6 +381,8 @@ module API {
384381
imports(_, m)
385382
or
386383
m = any(CanonicalName n | isUsed(n)).getExternalModuleName()
384+
or
385+
any(TypeAnnotation n).hasQualifiedName(m, _)
387386
} or
388387
MkClassInstance(DataFlow::ClassNode cls) { cls = trackDefNode(_) and hasSemantics(cls) } or
389388
MkAsyncFuncResult(DataFlow::FunctionNode f) {
@@ -392,26 +391,13 @@ module API {
392391
MkDef(DataFlow::Node nd) { rhs(_, _, nd) } or
393392
MkUse(DataFlow::Node nd) { use(_, _, nd) } or
394393
/**
395-
* A TypeScript canonical name that is defined somewhere, and that isn't a module root.
396-
* (Module roots are represented by `MkModuleExport` nodes instead.)
397-
*
398-
* For most purposes, you probably want to use the `mkCanonicalNameDef` predicate instead of
399-
* this constructor.
400-
*/
401-
MkCanonicalNameDef(CanonicalName n) {
402-
not n.isRoot() and
403-
isDefined(n)
404-
} or
405-
/**
406-
* A TypeScript canonical name that is used somewhere, and that isn't a module root.
407-
* (Module roots are represented by `MkModuleImport` nodes instead.)
408-
*
409-
* For most purposes, you probably want to use the `mkCanonicalNameUse` predicate instead of
410-
* this constructor.
394+
* A TypeScript type, identified by name of the type-annotation.
395+
* This API node is exclusively used by `API::Node::ofType`.
411396
*/
412-
MkCanonicalNameUse(CanonicalName n) {
413-
not n.isRoot() and
414-
isUsed(n)
397+
MkHasUnderlyingType(string moduleName, string exportName) {
398+
any(TypeAnnotation n).hasQualifiedName(moduleName, exportName)
399+
or
400+
any(Type t).hasUnderlyingType(moduleName, exportName)
415401
} or
416402
MkSyntheticCallbackArg(DataFlow::Node src, int bound, DataFlow::InvokeNode nd) {
417403
trackUseNode(src, true, bound).flowsTo(nd.getCalleeNode())
@@ -420,10 +406,9 @@ module API {
420406
class TDef = MkModuleDef or TNonModuleDef;
421407

422408
class TNonModuleDef =
423-
MkModuleExport or MkClassInstance or MkAsyncFuncResult or MkDef or MkCanonicalNameDef or
424-
MkSyntheticCallbackArg;
409+
MkModuleExport or MkClassInstance or MkAsyncFuncResult or MkDef or MkSyntheticCallbackArg;
425410

426-
class TUse = MkModuleUse or MkModuleImport or MkUse or MkCanonicalNameUse;
411+
class TUse = MkModuleUse or MkModuleImport or MkUse or MkHasUnderlyingType;
427412

428413
private predicate hasSemantics(DataFlow::Node nd) { not nd.getTopLevel().isExterns() }
429414

@@ -460,20 +445,6 @@ module API {
460445
)
461446
}
462447

463-
/** An API-graph node representing definitions of the canonical name `cn`. */
464-
private TApiNode mkCanonicalNameDef(CanonicalName cn) {
465-
if cn.isModuleRoot()
466-
then result = MkModuleExport(cn.getExternalModuleName())
467-
else result = MkCanonicalNameDef(cn)
468-
}
469-
470-
/** An API-graph node representing uses of the canonical name `cn`. */
471-
private TApiNode mkCanonicalNameUse(CanonicalName cn) {
472-
if cn.isModuleRoot()
473-
then result = MkModuleImport(cn.getExternalModuleName())
474-
else result = MkCanonicalNameUse(cn)
475-
}
476-
477448
/**
478449
* Holds if `rhs` is the right-hand side of a definition of a node that should have an
479450
* incoming edge from `base` labeled `lbl` in the API graph.
@@ -577,11 +548,6 @@ module API {
577548
exists(string m | nd = MkModuleExport(m) | exports(m, rhs))
578549
or
579550
nd = MkDef(rhs)
580-
or
581-
exists(CanonicalName n | nd = MkCanonicalNameDef(n) |
582-
rhs = n.(Namespace).getADefinition().flow() or
583-
rhs = n.(CanonicalFunctionName).getADefinition().flow()
584-
)
585551
}
586552

587553
/**
@@ -633,10 +599,10 @@ module API {
633599
ref = cls.getConstructor().getParameter(i)
634600
)
635601
or
636-
exists(TypeName tn |
637-
base = MkCanonicalNameUse(tn) and
602+
exists(string moduleName, string exportName |
603+
base = MkHasUnderlyingType(moduleName, exportName) and
638604
lbl = Label::instance() and
639-
ref = getANodeWithType(tn)
605+
ref.(DataFlow::SourceNode).hasUnderlyingType(moduleName, exportName)
640606
)
641607
or
642608
exists(DataFlow::InvokeNode call |
@@ -676,8 +642,6 @@ module API {
676642
)
677643
or
678644
nd = MkUse(ref)
679-
or
680-
exists(CanonicalName n | nd = MkCanonicalNameUse(n) | ref.asExpr() = n.getAnAccess())
681645
}
682646

683647
/** Holds if module `m` exports `rhs`. */
@@ -832,13 +796,6 @@ module API {
832796
result = awaited(call, DataFlow::TypeTracker::end())
833797
}
834798

835-
private DataFlow::SourceNode getANodeWithType(TypeName tn) {
836-
exists(string moduleName, string typeName |
837-
tn.hasQualifiedName(moduleName, typeName) and
838-
result.hasUnderlyingType(moduleName, typeName)
839-
)
840-
}
841-
842799
/**
843800
* Holds if there is an edge from `pred` to `succ` in the API graph that is labeled with `lbl`.
844801
*/
@@ -879,11 +836,10 @@ module API {
879836
succ = MkClassInstance(trackDefNode(def))
880837
)
881838
or
882-
exists(CanonicalName cn1, string n, CanonicalName cn2 |
883-
pred in [mkCanonicalNameDef(cn1), mkCanonicalNameUse(cn1)] and
884-
cn2 = cn1.getChild(n) and
885-
lbl = Label::member(n) and
886-
succ in [mkCanonicalNameDef(cn2), mkCanonicalNameUse(cn2)]
839+
exists(string moduleName, string exportName |
840+
pred = MkModuleImport(moduleName) and
841+
lbl = Label::member(exportName) and
842+
succ = MkHasUnderlyingType(moduleName, exportName)
887843
)
888844
or
889845
exists(DataFlow::Node nd, DataFlow::FunctionNode f |
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/**
2+
* Provides classes and predicates for reasoning about [puppeteer](https://www.npmjs.com/package/puppeteer).
3+
*/
4+
5+
import javascript
6+
7+
/**
8+
* Classes and predicates modelling the [puppeteer](https://www.npmjs.com/package/puppeteer) library.
9+
*/
10+
module Puppeteer {
11+
/**
12+
* A reference to a module import of puppeteer.
13+
*/
14+
private API::Node puppeteer() { result = API::moduleImport(["puppeteer", "puppeteer-core"]) }
15+
16+
/**
17+
* A reference to a `Browser` from puppeteer.
18+
*/
19+
private API::Node browser() {
20+
result = API::Node::ofType("puppeteer", "Browser")
21+
or
22+
result = puppeteer().getMember(["launch", "connect"]).getReturn().getPromised()
23+
or
24+
result = [page(), context(), target()].getMember("browser").getReturn()
25+
}
26+
27+
/**
28+
* A reference to a `Page` from puppeteer.
29+
*/
30+
API::Node page() {
31+
result = API::Node::ofType("puppeteer", "Page")
32+
or
33+
result = [browser(), context()].getMember("newPage").getReturn().getPromised()
34+
or
35+
result = [browser(), context()].getMember("pages").getReturn().getPromised().getUnknownMember()
36+
or
37+
result = target().getMember("page").getReturn().getPromised()
38+
}
39+
40+
/**
41+
* A reference to a `Target` from puppeteer.
42+
*/
43+
private API::Node target() {
44+
result = API::Node::ofType("puppeteer", "Target")
45+
or
46+
result = [page(), browser()].getMember("target").getReturn()
47+
or
48+
result = context().getMember("targets").getReturn().getUnknownMember()
49+
or
50+
result = target().getMember("opener").getReturn()
51+
}
52+
53+
/**
54+
* A reference to a `BrowserContext` from puppeteer.
55+
*/
56+
private API::Node context() {
57+
result = API::Node::ofType("puppeteer", "BrowserContext")
58+
or
59+
result = [page(), target()].getMember("browserContext").getReturn()
60+
or
61+
result = browser().getMember("browserContexts").getReturn().getUnknownMember()
62+
or
63+
result = browser().getMember("createIncognitoBrowserContext").getReturn().getPromised()
64+
or
65+
result = browser().getMember("defaultBrowserContext").getReturn()
66+
}
67+
68+
/**
69+
* A call requesting a `Page` to navigate to some url, seen as a `ClientRequest`.
70+
*/
71+
private class PuppeteerGotoCall extends ClientRequest::Range, API::InvokeNode {
72+
PuppeteerGotoCall() { this = page().getMember("goto").getACall() }
73+
74+
override DataFlow::Node getUrl() { result = getArgument(0) }
75+
76+
override DataFlow::Node getHost() { none() }
77+
78+
override DataFlow::Node getADataNode() { none() }
79+
}
80+
81+
/**
82+
* A call requesting a `Page` to load a stylesheet or script, seen as a `ClientRequest`.
83+
*/
84+
private class PuppeteerLoadResourceCall extends ClientRequest::Range, API::InvokeNode {
85+
PuppeteerLoadResourceCall() {
86+
this = page().getMember(["addStyleTag", "addScriptTag"]).getACall()
87+
}
88+
89+
override DataFlow::Node getUrl() { result = getParameter(0).getMember("url").getARhs() }
90+
91+
override DataFlow::Node getHost() { none() }
92+
93+
override DataFlow::Node getADataNode() { none() }
94+
}
95+
}

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

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

639+
/**
640+
* A path argument given to a `Page` in puppeteer, specifying where a pdf/screenshot should be saved.
641+
*/
642+
private class PuppeteerPath extends TaintedPath::Sink {
643+
PuppeteerPath() {
644+
this =
645+
Puppeteer::page()
646+
.getMember(["pdf", "screenshot"])
647+
.getParameter(0)
648+
.getMember("path")
649+
.getARhs()
650+
}
651+
}
652+
639653
/**
640654
* Holds if there is a step `src -> dst` mapping `srclabel` to `dstlabel` relevant for path traversal vulnerabilities.
641655
*/
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
| mongodb | Collection | index.ts:14:3:14:17 | getCollection() |
22
| mongoose | Model | index.ts:22:3:22:20 | getMongooseModel() |
33
| mongoose | Query | index.ts:23:3:23:20 | getMongooseQuery() |
4+
| puppeteer | Browser | index.ts:30:22:30:33 | this.browser |

javascript/ql/test/ApiGraphs/typed/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,11 @@ app.post("/find", (req, res) => {
2222
getMongooseModel().find({ id: v }); /* def (parameter 0 (member find (instance (member Model (member exports (module mongoose)))))) */
2323
getMongooseQuery().find({ id: v }); /* def (parameter 0 (member find (instance (member Query (member exports (module mongoose)))))) */
2424
});
25+
26+
import * as puppeteer from 'puppeteer';
27+
class Renderer {
28+
private browser: puppeteer.Browser;
29+
foo(): void {
30+
const page = this.browser.newPage(); /* use (instance (member Browser (member exports (module puppeteer)))) */
31+
}
32+
}

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) |
@@ -138,6 +141,9 @@ test_getUrl
138141
| apollo.js:17:1:17:34 | new Pre ... yurl"}) | apollo.js:17:26:17:32 | "myurl" |
139142
| apollo.js:20:1:20:77 | createN ... phql'}) | apollo.js:20:30:20:75 | 'https: ... raphql' |
140143
| apollo.js:23:1:23:31 | new Web ... wsUri}) | apollo.js:23:25:23:29 | wsUri |
144+
| puppeteer.ts:6:11:6:42 | page.go ... e.com') | puppeteer.ts:6:21:6:41 | 'https: ... le.com' |
145+
| puppeteer.ts:8:5:8:61 | page.ad ... css" }) | puppeteer.ts:8:29:8:58 | "http:/ ... le.css" |
146+
| puppeteer.ts:18:30:18:50 | page.go ... estUrl) | puppeteer.ts:18:40:18:49 | requestUrl |
141147
| tst.js:11:5:11:16 | request(url) | tst.js:11:13:11:15 | url |
142148
| tst.js:13:5:13:20 | request.get(url) | tst.js:13:17:13:19 | url |
143149
| 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+
{}

0 commit comments

Comments
 (0)