Skip to content

Commit 57c8dd8

Browse files
authored
Merge pull request github#2801 from esbena/js/bulky-route-handler-registration
Approved by asgerf
2 parents 0d1fb0f + 872ee13 commit 57c8dd8

File tree

12 files changed

+1621
-2
lines changed

12 files changed

+1621
-2
lines changed

javascript/ql/src/semmle/javascript/Collections.qll

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ private class PseudoProperty extends TypeTrackingPseudoProperty {
3131
* `load`/`store`/`loadStore` can be used in the `CollectionsTypeTracking` module.
3232
* (Thereby avoiding naming conflicts with a "cousin" `AdditionalFlowStep` implementation.)
3333
*/
34-
abstract private class CollectionFlowStep extends DataFlow::AdditionalFlowStep {
34+
abstract class CollectionFlowStep extends DataFlow::AdditionalFlowStep {
3535
final override predicate step(DataFlow::Node pred, DataFlow::Node succ) { none() }
3636

3737
final override predicate step(

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,14 @@ module Express {
119119
t.start() and
120120
result = getARouteHandlerExpr().flow().getALocalSource()
121121
or
122-
exists(DataFlow::TypeBackTracker t2 | result = getARouteHandler(t2).backtrack(t2, t))
122+
exists(DataFlow::TypeBackTracker t2, DataFlow::SourceNode succ | succ = getARouteHandler(t2) |
123+
result = succ.backtrack(t2, t)
124+
or
125+
exists(HTTP::RouteHandlerCandidateContainer container |
126+
result = container.getRouteHandler(succ)
127+
) and
128+
t = t2
129+
)
123130
}
124131

125132
override Expr getServer() { result.(Application).getARouteHandler() = getARouteHandler() }

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

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
*/
44

55
import javascript
6+
private import semmle.javascript.DynamicPropertyAccess
7+
private import semmle.javascript.dataflow.internal.StepSummary
68

79
module HTTP {
810
/**
@@ -496,4 +498,111 @@ module HTTP {
496498
class CookieCryptographicKey extends CryptographicKey {
497499
CookieCryptographicKey() { this = any(CookieMiddlewareInstance instance).getASecretKey() }
498500
}
501+
502+
/**
503+
* An object that contains one or more potential route handlers.
504+
*/
505+
class RouteHandlerCandidateContainer extends DataFlow::Node {
506+
RouteHandlerCandidateContainer::Range self;
507+
508+
RouteHandlerCandidateContainer() { this = self }
509+
510+
/**
511+
* Gets the route handler in this container that is accessed at `access`.
512+
*/
513+
DataFlow::SourceNode getRouteHandler(DataFlow::SourceNode access) {
514+
result = self.getRouteHandler(access)
515+
}
516+
}
517+
518+
/**
519+
* Provides classes for working with objects that may contain one or more route handlers.
520+
*/
521+
module RouteHandlerCandidateContainer {
522+
private DataFlow::SourceNode ref(DataFlow::TypeTracker t, RouteHandlerCandidateContainer c) {
523+
t.start() and result = c
524+
or
525+
exists(DataFlow::TypeTracker t2 | result = ref(t2, c).track(t2, t))
526+
}
527+
528+
private DataFlow::SourceNode ref(RouteHandlerCandidateContainer c) {
529+
result = ref(DataFlow::TypeTracker::end(), c)
530+
}
531+
532+
/**
533+
* A container for one or more potential route handlers.
534+
*
535+
* Extend this class and implement its abstract member predicates to model additional
536+
* containers.
537+
*/
538+
abstract class Range extends DataFlow::SourceNode {
539+
/**
540+
* Gets the route handler in this container that is accessed at `access`.
541+
*/
542+
abstract DataFlow::SourceNode getRouteHandler(DataFlow::SourceNode access);
543+
}
544+
545+
/**
546+
* An object that contains one or more potential route handlers.
547+
*/
548+
private class ContainerObject extends Range {
549+
ContainerObject() {
550+
(
551+
this instanceof DataFlow::ObjectLiteralNode
552+
or
553+
exists(DataFlow::CallNode create | this = create |
554+
create = DataFlow::globalVarRef("Object").getAMemberCall("create") and
555+
create.getArgument(0).asExpr() instanceof NullLiteral
556+
)
557+
) and
558+
exists(RouteHandlerCandidate candidate | candidate.flowsTo(getAPropertyWrite().getRhs()))
559+
}
560+
561+
override DataFlow::SourceNode getRouteHandler(DataFlow::SourceNode access) {
562+
result instanceof RouteHandlerCandidate and
563+
exists(DataFlow::PropWrite write, DataFlow::PropRead read |
564+
access = read and
565+
ref(this).getAPropertyRead() = read and
566+
result.flowsTo(write.getRhs()) and
567+
write = this.getAPropertyWrite()
568+
|
569+
write.getPropertyName() = read.getPropertyName()
570+
or
571+
exists(EnumeratedPropName prop | access = prop.getASourceProp())
572+
or
573+
read = DataFlow::lvalueNode(any(ForOfStmt stmt).getLValue())
574+
)
575+
}
576+
}
577+
578+
/**
579+
* A collection that contains one or more route potential handlers.
580+
*/
581+
private class ContainerCollection extends HTTP::RouteHandlerCandidateContainer::Range {
582+
ContainerCollection() {
583+
this = DataFlow::globalVarRef("Map").getAnInstantiation() and // restrict to Map for now
584+
exists(
585+
CollectionFlowStep store, DataFlow::Node storeTo, DataFlow::Node input,
586+
RouteHandlerCandidate candidate
587+
|
588+
this.flowsTo(storeTo) and
589+
store.store(input, storeTo, _) and
590+
candidate.flowsTo(input)
591+
)
592+
}
593+
594+
override DataFlow::SourceNode getRouteHandler(DataFlow::SourceNode access) {
595+
exists(
596+
DataFlow::Node input, TypeTrackingPseudoProperty key, CollectionFlowStep store,
597+
CollectionFlowStep load, DataFlow::Node storeTo, DataFlow::Node loadFrom
598+
|
599+
this.flowsTo(storeTo) and
600+
store.store(input, storeTo, key) and
601+
result.(RouteHandlerCandidate).flowsTo(input) and
602+
ref(this).flowsTo(loadFrom) and
603+
load.load(loadFrom, access, key)
604+
)
605+
}
606+
}
607+
}
499608
}

javascript/ql/test/library-tests/frameworks/Express/RouteHandlerContainer.expected

Whitespace-only changes.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import javascript
2+
3+
query predicate getRouteHandlerContainerStep(
4+
HTTP::RouteHandlerCandidateContainer container, DataFlow::SourceNode handler,
5+
DataFlow::SourceNode access
6+
) {
7+
handler = container.getRouteHandler(access)
8+
}
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
var express = require("express");
2+
var app = express();
3+
4+
// registration of route handlers in bulk
5+
let routes0 = {
6+
a: (req, res) => console.log(req),
7+
b: (req, res) => console.log(req)
8+
};
9+
for (const p in routes0) {
10+
app.get(p, routes0[p]);
11+
}
12+
13+
// registration of route handlers in bulk
14+
let routes1 = {
15+
a: (req, res) => console.log(req),
16+
b: (req, res) => console.log(req)
17+
};
18+
for (const handler of routes1) {
19+
app.use(handler);
20+
}
21+
22+
// registration of route handlers in bulk, with indirection
23+
let routes2 = {
24+
a: (req, res) => console.log(req),
25+
b: (req, res) => console.log(req)
26+
};
27+
for (const p of Object.keys(routes2)) {
28+
app.get(p, routes2[p]);
29+
}
30+
31+
// registration of route handlers in bulk, with indirection
32+
let routes3 = {
33+
a: (req, res) => console.log(req),
34+
b: (req, res) => console.log(req)
35+
};
36+
for (const h of Object.values(routes3)) {
37+
app.use(h);
38+
}
39+
40+
// custom router indirection for all requests
41+
let myRouter1 = {
42+
handlers: {},
43+
add: function(n, h) {
44+
this.handlers[n] = h;
45+
},
46+
handle: function(req, res, target) {
47+
this.handlers[target](req, res);
48+
}
49+
};
50+
myRouter1.add("whatever", (req, res) => console.log(req));
51+
app.use((req, res) => myRouter1.handle(req, res, "whatever"));
52+
53+
// simpler custom router indirection for all requests
54+
let mySimpleRouter = {
55+
handler: undefined,
56+
add: function(h) {
57+
this.handler = h;
58+
},
59+
handle: function(req, res) {
60+
this.handler(req, res);
61+
}
62+
};
63+
mySimpleRouter.add((req, res) => console.log(req));
64+
app.use((req, res) => mySimpleRouter.handle(req, res));
65+
66+
// simplest custom router indirection for all requests
67+
let mySimplestRouter = {
68+
handler: (req, res) => console.log(req),
69+
handle: function(req, res) {
70+
this.handler(req, res);
71+
}
72+
};
73+
app.use((req, res) => mySimplestRouter.handle(req, res));
74+
75+
// a combination of bulk registration and indirection through a custom router
76+
let myRouter3 = {
77+
handlers: {},
78+
add: function(n, h) {
79+
this.handlers[n] = h;
80+
},
81+
handle: function(req, res, target) {
82+
this.handlers[target](req, res);
83+
}
84+
};
85+
let routes3 = {
86+
a: (req, res) => console.log(req),
87+
b: (req, res) => console.log(req)
88+
};
89+
for (const p of Object.keys(routes3)) {
90+
myRouter3.add(p, routes3[p]);
91+
}
92+
app.use((req, res) => myRouter3.handle(req, res, "whatever"));
93+
94+
// a combination of bulk registration and indirection through a custom router. Using a map instead of an object.
95+
let myRouter4 = {
96+
handlers: new Map(),
97+
add: function(n, h) {
98+
this.handlers.set(n, h);
99+
},
100+
handle: function(req, res, target) {
101+
this.handlers.get(target)(req, res);
102+
}
103+
};
104+
let routes4 = {
105+
a: (req, res) => console.log(req),
106+
b: (req, res) => console.log(req)
107+
};
108+
for (const p of Object.keys(routes4)) {
109+
myRouter4.add(p, routes4[p]);
110+
}
111+
app.use((req, res) => myRouter4.handle(req, res, "whatever"));
112+
113+
// registration of imported route handlers in bulk
114+
let importedRoutes = require("./route-collection").routes;
115+
for (const p in importedRoutes) {
116+
app.get(p, importedRoutes[p]);
117+
}
118+
app.get("a", importedRoutes.a);
119+
app.get("b", importedRoutes.b);
120+
121+
// registration of imported route handlers in a map
122+
let routesMap = new Map();
123+
routesMap.set("a", (req, res) => console.log(req));
124+
routesMap.set("b", (req, res) => console.log(req));
125+
routesMap.forEach((v, k) => app.get(k, v));
126+
app.get("a", routesMap.get("a"));
127+
app.get("b", routesMap.get("b"));
128+
129+
let method = "GET";
130+
app[method.toLowerCase()](path, (req, res) => undefined);
131+
132+
let names = ["handler-in-dynamic-require"];
133+
names.forEach(name => {
134+
let dynamicRequire = require("./controllers/" + name);
135+
app.get(dynamicRequire.path, dynamicRequire.handler);
136+
});
137+
138+
let bulkRequire = require("./controllers");
139+
app.get(bulkRequire.bulky.path, bulkRequire.bulky.handler);
140+
141+
let options = { app: app };
142+
let args = [];
143+
args.push((req, res) => undefined);
144+
app.use.apply(options.app, args);
145+
146+
let handlers = { handlerA: (req, res) => undefined};
147+
app.use(handlers.handlerA.bind(data));
148+
149+
for ([k, v] of routesMap) {
150+
app.get(k, v) // not supported - requires one too many heap steps
151+
}
152+
153+
app.get("b", routesMap.get("NOT_A_KEY!")); // unknown route handler
154+
155+
let routesMap2 = new Map();
156+
routesMap2.set("c", (req, res) => console.log(req));
157+
routesMap2.set(unknown(), (req, res) => console.log(req));
158+
routesMap2.set("e", (req, res) => console.log(req));
159+
160+
app.get("c", routesMap2.get("c"));
161+
app.get("d", routesMap2.get(unknown()));
162+
app.get("e", unknown());
163+
app.get("d", routesMap2.get("f"));
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = { path: "bulky", handler: (req, res) => undefined };
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = { path: "/A", handler: (req, res) => undefined };
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
let bulky = require("./handler-in-bulk-require");
2+
module.exports = {
3+
bulky: bulky
4+
};
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
exports.routes = {
2+
a: (req, res) => console.log(req),
3+
b: (req, res) => console.log(req)
4+
};

0 commit comments

Comments
 (0)