Skip to content

Commit b4eb75e

Browse files
authored
Merge pull request #22522 from github/jacknojo/enhance_hapi_coverage
Improve hapi coverage
2 parents 03cccfe + b78a78f commit b4eb75e

11 files changed

Lines changed: 291 additions & 40 deletions

File tree

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
category: fix
3+
---
4+
* Improved Hapi route handler and request input tracking through custom route registration helpers and higher-order function wrappers.

‎javascript/ql/lib/semmle/javascript/dataflow/internal/FunctionWrapperSteps.qll‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,3 +145,41 @@ private module Cached {
145145
}
146146

147147
import Cached
148+
149+
/** Gets a value referring to a forwarding-wrapper around `func`. */
150+
private DataFlow::SourceNode trackWrappedFunction(
151+
DataFlow::FunctionNode func, DataFlow::TypeTracker t
152+
) {
153+
t.start() and
154+
functionOneWayForwardingStep(func, result) and
155+
// Exclude functions since ordinary call edges can handle that case.
156+
// This will typically match calls to a "wrapper factory".
157+
not result instanceof DataFlow::FunctionNode
158+
or
159+
functionOneWayForwardingStep(trackWrappedFunction(func, t.continue()), result)
160+
or
161+
exists(DataFlow::TypeTracker t2 | result = trackWrappedFunction(func, t2).track(t2, t))
162+
}
163+
164+
/** Gets a value referring to a forwarding-wrapper around `func`. */
165+
DataFlow::SourceNode trackWrappedFunction(DataFlow::SourceNode func) {
166+
result = trackWrappedFunction(func, DataFlow::TypeTracker::end())
167+
}
168+
169+
/**
170+
* Data flow into a concrete function invoked through a forwarding wrapper.
171+
*
172+
* Only arguments with a statically known position and a corresponding non-rest parameter are
173+
* modeled.
174+
*/
175+
private class FunctionWrapperCallStep extends DataFlow::SharedFlowStep {
176+
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
177+
exists(DataFlow::CallNode call, DataFlow::FunctionNode wrapped, int index |
178+
call = trackWrappedFunction(wrapped).getACall() and
179+
pred = call.getArgument(index) and
180+
succ = wrapped.getParameter(index) and
181+
// A rest parameter receives an array, not the argument at this index.
182+
not succ.(DataFlow::ParameterNode).isRestParameter()
183+
)
184+
}
185+
}

‎javascript/ql/lib/semmle/javascript/frameworks/Hapi.qll‎

Lines changed: 110 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import javascript
66
import semmle.javascript.frameworks.HTTP
7+
private import semmle.javascript.dataflow.internal.CallGraphs
78

89
module Hapi {
910
/**
@@ -101,6 +102,19 @@ module Hapi {
101102
override RequestSource src;
102103
}
103104

105+
private DataFlow::SourceNode requestInputRef(
106+
RouteHandler rh, string property, DataFlow::TypeTracker t
107+
) {
108+
t.start() and
109+
result = rh.getARequestSource().ref().getAPropertyRead(property)
110+
or
111+
exists(DataFlow::TypeTracker t2 | result = requestInputRef(rh, property, t2).track(t2, t))
112+
}
113+
114+
private DataFlow::SourceNode requestInputRef(RouteHandler rh, string property) {
115+
result = requestInputRef(rh, property, DataFlow::TypeTracker::end())
116+
}
117+
104118
/**
105119
* An access to a user-controlled Hapi request input.
106120
*/
@@ -115,18 +129,18 @@ module Hapi {
115129
// `request.rawPayload`
116130
this.(DataFlow::PropRead).accesses(request, "rawPayload")
117131
or
118-
exists(DataFlow::PropRead payload |
119-
// `request.payload.name`
120-
payload.accesses(request, "payload") and
121-
this.(DataFlow::PropRead).accesses(payload, _)
122-
)
132+
// `request.payload` is an object, so prefer a property read if possible.
133+
if exists(requestInputRef(rh, "payload").getAPropertyRead())
134+
then this = requestInputRef(rh, "payload").getAPropertyRead()
135+
else this = rh.getARequestSource().ref().getAPropertyRead("payload")
123136
)
124137
or
125138
kind = "parameter" and
126-
exists(DataFlow::PropRead query |
127-
// `request.query.name`
128-
query.accesses(request, ["query", "params"]) and
129-
this.(DataFlow::PropRead).accesses(query, _)
139+
exists(string property | property = ["query", "params"] |
140+
// These are objects, so prefer a property read if possible.
141+
if exists(requestInputRef(rh, property).getAPropertyRead())
142+
then this = requestInputRef(rh, property).getAPropertyRead()
143+
else this = rh.getARequestSource().ref().getAPropertyRead(property)
130144
)
131145
or
132146
exists(DataFlow::PropRead url |
@@ -199,30 +213,10 @@ module Hapi {
199213
*/
200214
class RouteSetup extends DataFlow::MethodCallNode, Http::Servers::StandardRouteSetup {
201215
ServerDefinition server;
202-
DataFlow::Node handler;
203216

204217
RouteSetup() {
205218
server.ref().getAMethodCall() = this and
206-
(
207-
// server.route({ handler: fun })
208-
this.getMethodName() = "route" and
209-
this.getOptionArgument(0, "handler") = handler
210-
or
211-
// server.ext('/', fun)
212-
this.getMethodName() = "ext" and
213-
handler = this.getArgument(1)
214-
or
215-
// server.route([{ handler(request){}])
216-
this.getMethodName() = "route" and
217-
handler =
218-
this.getArgument(0)
219-
.getALocalSource()
220-
.(DataFlow::ArrayCreationNode)
221-
.getAnElement()
222-
.getALocalSource()
223-
.getAPropertySource("handler")
224-
.getAFunctionValue()
225-
)
219+
this.getMethodName() = ["route", "ext"]
226220
}
227221

228222
override DataFlow::SourceNode getARouteHandler() {
@@ -233,11 +227,45 @@ module Hapi {
233227
t.start() and
234228
result = this.getRouteHandler().getALocalSource()
235229
or
236-
exists(DataFlow::TypeBackTracker t2 | result = this.getARouteHandler(t2).backtrack(t2, t))
230+
this.getMethodName() = "route" and
231+
t.isInProp("handler") and
232+
result = this.getArgument(0).getALocalSource()
233+
or
234+
exists(DataFlow::TypeBackTracker t2, DataFlow::SourceNode succ |
235+
succ = this.getARouteHandler(t2)
236+
|
237+
result = succ.backtrack(t2, t)
238+
or
239+
Http::routeHandlerStep(result, succ) and
240+
t = t2
241+
or
242+
DataFlow::SharedFlowStep::storeStep(result.getALocalUse(), succ,
243+
DataFlow::PseudoProperties::arrayElement()) and
244+
t = t2.continue()
245+
)
237246
}
238247

239248
pragma[noinline]
240-
private DataFlow::Node getRouteHandler() { result = handler }
249+
private DataFlow::Node getRouteHandler() {
250+
// server.route({ handler: fun })
251+
this.getMethodName() = "route" and
252+
this.getOptionArgument(0, "handler") = result
253+
or
254+
// server.ext('/', fun)
255+
this.getMethodName() = "ext" and
256+
result = this.getArgument(1)
257+
or
258+
// server.route([{ handler(request){}])
259+
this.getMethodName() = "route" and
260+
result =
261+
this.getArgument(0)
262+
.getALocalSource()
263+
.(DataFlow::ArrayCreationNode)
264+
.getAnElement()
265+
.getALocalSource()
266+
.getAPropertySource("handler")
267+
.getAFunctionValue()
268+
}
241269

242270
override DataFlow::Node getServer() { result = server }
243271
}
@@ -263,6 +291,56 @@ module Hapi {
263291
}
264292
}
265293

294+
private DataFlow::SourceNode routeDefinitionRef(
295+
DataFlow::ObjectLiteralNode definition, DataFlow::TypeTracker t
296+
) {
297+
t.start() and
298+
result = definition
299+
or
300+
exists(DataFlow::TypeTracker t2 | result = routeDefinitionRef(definition, t2).track(t2, t))
301+
}
302+
303+
private predicate handlerRegistration(
304+
DataFlow::FunctionNode handler, DataFlow::ObjectLiteralNode definition
305+
) {
306+
exists(
307+
DataFlow::CallNode registration, DataFlow::FunctionNode registrar,
308+
DataFlow::ParameterNode handlerParameter, DataFlow::SourceNode handlerRef, int index
309+
|
310+
registration.getACallee() = registrar.getFunction() and
311+
handlerParameter = registrar.getParameter(index) and
312+
handlerParameter.flowsTo(definition.getAPropertyWrite("handler").getRhs()) and
313+
(
314+
handlerRef = handler
315+
or
316+
handlerRef = CallGraph::callgraphStep(handler, DataFlow::TypeTracker::end())
317+
) and
318+
handlerRef.flowsTo(registration.getArgument(index))
319+
)
320+
}
321+
322+
/** Data flow through handlers stored in route definitions by registration helpers. */
323+
private class RegisteredHandlerCallStep extends DataFlow::SharedFlowStep {
324+
DataFlow::CallNode call;
325+
DataFlow::FunctionNode handler;
326+
327+
RegisteredHandlerCallStep() {
328+
exists(DataFlow::ObjectLiteralNode definition, DataFlow::PropRead handlerRead |
329+
handlerRegistration(handler, definition) and
330+
handlerRead.getPropertyName() = "handler" and
331+
routeDefinitionRef(definition, DataFlow::TypeTracker::end()).flowsTo(handlerRead.getBase()) and
332+
call.getCalleeNode() = handlerRead
333+
)
334+
}
335+
336+
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
337+
exists(int index |
338+
pred = call.getArgument(index) and
339+
succ = handler.getParameter(index)
340+
)
341+
}
342+
}
343+
266344
/**
267345
* A function that looks like a Hapi route handler and flows to a route setup.
268346
*/
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import javascript
2+
3+
module WrappedRouteConfig implements DataFlow::ConfigSig {
4+
predicate isSource(DataFlow::Node source) { source instanceof Http::RequestInputAccess }
5+
6+
predicate isSink(DataFlow::Node sink) {
7+
sink = DataFlow::globalVarRef("sink").getACall().getArgument(0)
8+
}
9+
}
10+
11+
module WrappedRouteTaint = TaintTracking::Global<WrappedRouteConfig>;
12+
13+
query predicate test_WrappedRouteFlow(DataFlow::Node source, DataFlow::Node sink) {
14+
WrappedRouteTaint::flow(source, sink)
15+
}

‎javascript/ql/test/library-tests/frameworks/hapi/src/hapi.js‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ server2.ext('onPreResponse', function handler3(request, reply) { // HTTP::RouteH
1919

2020
function handler4(request, reply){
2121
request.rawPayload;
22-
request.payload.foo;
23-
request.query.bar;
22+
getRequest(request).payload.foo;
23+
getRequest(request).query.bar; getRequest(request).params.bar;
2424
request.url.path;
2525
request.headers.baz;
2626
request.state.token;
@@ -34,3 +34,7 @@ function getHandler() {
3434
return function (req, h){}
3535
}
3636
server2.route({handler: getHandler()});
37+
38+
function getRequest(request) {
39+
return request;
40+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
function createCached(fn) {
2+
return async function (...args) {
3+
return fn(...args);
4+
};
5+
}
6+
7+
module.exports = { createCached };
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
const endpoints = [];
2+
3+
function apiEndpoint(method, path, handler) {
4+
endpoints.push({ method, path, handler });
5+
}
6+
7+
function createRouteConfig(method, path, handler) {
8+
const hapiHandler = async function (request, h) {
9+
return handler(request.params, request.query, request.payload);
10+
};
11+
12+
return {
13+
method,
14+
path,
15+
handler: hapiHandler,
16+
};
17+
}
18+
19+
function registerEndpoints(server, serviceInstance) {
20+
for (const definition of endpoints) {
21+
const wrapped = async (params, query, payload) => {
22+
return definition.handler.call(serviceInstance, params, query, payload);
23+
};
24+
25+
const config = createRouteConfig(definition.method, definition.path, wrapped);
26+
server.route(config);
27+
}
28+
}
29+
30+
module.exports = { apiEndpoint, registerEndpoints };
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
const { createCached } = require("./wrapped-route-cache");
2+
3+
class ReportService {
4+
async runQuery(filter) {
5+
sink(filter);
6+
}
7+
}
8+
9+
const service = new ReportService();
10+
11+
const getReportCached = createCached(async function (filter) {
12+
return service.runQuery(filter);
13+
});
14+
15+
module.exports = { getReportCached };
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
const Hapi = require("@hapi/hapi");
2+
const {
3+
apiEndpoint,
4+
registerEndpoints,
5+
} = require("./wrapped-route-configurator");
6+
const { getReportCached } = require("./wrapped-route-service");
7+
8+
class ReportRoutes {
9+
async getReport(params, query) {
10+
return getReportCached(query.filter);
11+
}
12+
}
13+
14+
apiEndpoint("GET", "/reports", ReportRoutes.prototype.getReport);
15+
16+
async function main() {
17+
const server = Hapi.server();
18+
registerEndpoints(server, new ReportRoutes());
19+
return server;
20+
}
21+
22+
main();

0 commit comments

Comments
 (0)