Skip to content

Commit 37ea3f2

Browse files
author
Alvaro Muñoz
committed
Refactored ReplySource to ReplyCall. Got rid of unnecessary ref()
1 parent 742e4aa commit 37ea3f2

File tree

4 files changed

+237
-117
lines changed

4 files changed

+237
-117
lines changed

javascript/ql/lib/semmle/javascript/frameworks/Spife.qll

Lines changed: 94 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ module Spife {
186186
string kind;
187187

188188
ContextInputAccess() {
189-
this = request.ref().getAMethodCall("get")
189+
this = request.ref().getAMethodCall("get") and
190190
kind = "path"
191191
}
192192

@@ -217,29 +217,25 @@ module Spife {
217217
* A Spife response source, that is, the response variable used by a
218218
* route handler.
219219
*/
220-
private class ReplySource extends Http::Servers::ResponseSource instanceof DataFlow::CallNode {
221-
ReplySource() {
222-
// const reply = require("@npm/spife/reply")
220+
private class ReplyCall extends API::CallNode {
221+
ReplyCall() {
223222
// reply(resp)
223+
this = API::moduleImport(["@npm/spife/reply", "spife/reply"]).getACall()
224+
or
224225
// reply.header(resp, 'foo', 'bar')
225-
this = API::moduleImport(["@npm/spife/reply", "spife/reply"]).getACall() or
226226
this = API::moduleImport(["@npm/spife/reply", "spife/reply"]).getAMember().getACall()
227227
}
228228

229-
private DataFlow::SourceNode reachesHandlerReturn(DataFlow::TypeTracker t) {
230-
result = this and
231-
t.start()
232-
or
233-
exists(DataFlow::TypeTracker t2 | result = this.reachesHandlerReturn(t2).track(t2, t))
229+
predicate isDirectReplyCall() {
230+
this = API::moduleImport(["@npm/spife/reply", "spife/reply"]).getACall()
234231
}
235232

236233
/**
237234
* Gets the route handler that provides this response.
238235
*/
239-
override RouteHandler getRouteHandler() {
236+
RouteHandler getRouteHandler() {
240237
exists(RouteHandler handler |
241-
handler.(DataFlow::FunctionNode).getAReturn().getALocalSource() =
242-
this.reachesHandlerReturn(DataFlow::TypeTracker::end()) and
238+
handler.getAReturn() = this.getReturn().getAValueReachableFromSource() and
243239
result = handler
244240
)
245241
}
@@ -248,47 +244,46 @@ module Spife {
248244
/**
249245
* An HTTP header defined in a Spife response.
250246
*/
251-
private class HeaderDefinition extends Http::ExplicitHeaderDefinition, DataFlow::MethodCallNode instanceof ReplySource {
252-
HeaderDefinition() {
247+
private class SingleHeaderDefinition extends Http::ExplicitHeaderDefinition instanceof ReplyCall {
248+
SingleHeaderDefinition() {
253249
// reply.header(RESPONSE, 'Cache-Control', 'no-cache')
254-
exists(DataFlow::MethodCallNode call |
255-
this.ref() = call and
256-
call.getMethodName() = "header" and
257-
call.getNumArgument() = 3
258-
)
250+
this.getCalleeName() = "header" and
251+
this.getNumArgument() = 3
259252
}
260253

261254
override predicate definesHeaderValue(string headerName, DataFlow::Node headerValue) {
262255
// reply.header(RESPONSE, 'Cache-Control', 'no-cache')
263256
this.getNameNode().mayHaveStringValue(headerName) and
264-
headerValue = this.getArgument(2)
257+
headerValue = this.(DataFlow::MethodCallNode).getArgument(2)
265258
}
266259

267-
override DataFlow::Node getNameNode() { result = this.getArgument(1) }
260+
override DataFlow::Node getNameNode() {
261+
result = this.(DataFlow::MethodCallNode).getArgument(1)
262+
}
268263

269-
override RouteHandler getRouteHandler() { result = this.getRouteHandler() }
264+
override RouteHandler getRouteHandler() { result = this.(ReplyCall).getRouteHandler() }
270265
}
271266

272267
/**
273268
* An invocation that sets any number of headers of the HTTP response.
274269
*/
275-
private class MultipleHeaderDefinitions extends Http::ExplicitHeaderDefinition, DataFlow::CallNode {
276-
ReplySource reply;
277-
270+
private class MultipleHeaderDefinitions extends Http::ExplicitHeaderDefinition instanceof ReplyCall {
278271
MultipleHeaderDefinitions() {
279-
// reply.header(RESPONSE, {'Cache-Control': 'no-cache'})
280-
// reply(RESPONSE, {'Cache-Control': 'no-cache'})
281-
exists(DataFlow::CallNode call | call = [reply.ref(), reply.ref().getAMethodCall("header")] |
282-
call.getAnArgument().getALocalSource() instanceof DataFlow::ObjectLiteralNode and
283-
this = call
284-
)
272+
(
273+
// reply.header(RESPONSE, {'Cache-Control': 'no-cache'})
274+
this.getCalleeName() = "header"
275+
or
276+
// reply(RESPONSE, {'Cache-Control': 'no-cache'})
277+
this.isDirectReplyCall()
278+
) and
279+
this.getAnArgument().getALocalSource() instanceof DataFlow::ObjectLiteralNode
285280
}
286281

287282
/**
288283
* Gets a reference to the multiple headers object that is to be set.
289284
*/
290-
private DataFlow::ObjectLiteralNode getAHeaderSource() {
291-
result = this.getAnArgument().getALocalSource()
285+
DataFlow::ObjectLiteralNode getAHeaderSource() {
286+
result = this.(DataFlow::CallNode).getAnArgument().getALocalSource()
292287
}
293288

294289
override predicate definesHeaderValue(string headerName, DataFlow::Node headerValue) {
@@ -302,114 +297,128 @@ module Spife {
302297
result = this.getAHeaderSource().getAPropertyWrite().getPropertyNameExpr().flow()
303298
}
304299

305-
override RouteHandler getRouteHandler() { result = reply.getRouteHandler() }
300+
override RouteHandler getRouteHandler() { result = this.(ReplyCall).getRouteHandler() }
306301
}
307302

308303
/**
309304
* A header produced by a route handler with no explicit declaration of a Content-Type.
310305
*/
311-
private class ContentTypeRouteHandlerHeader extends Http::ImplicitHeaderDefinition,
312-
DataFlow::FunctionNode instanceof RouteHandler {
306+
private class ContentTypeRouteHandlerHeader extends Http::ImplicitHeaderDefinition instanceof RouteHandler {
313307
override predicate defines(string headerName, string headerValue) {
314308
headerName = "content-type" and headerValue = "application/json"
315309
}
316310

317-
override Http::RouteHandler getRouteHandler() { result = this }
311+
override RouteHandler getRouteHandler() { result = this }
318312
}
319313

320314
/**
321315
* An HTTP cookie defined in a Spife HTTP response.
322316
*/
323-
private class CookieDefinition extends Http::CookieDefinition, DataFlow::MethodCallNode {
317+
private class CookieDefinition extends Http::CookieDefinition instanceof ReplyCall {
324318
CookieDefinition() {
325319
// reply.cookie(RESPONSE, 'TEST', 'FOO', {"maxAge": 1000, "httpOnly": true, "secure": true})
326-
this = any(ReplySource r).ref().getAMethodCall("cookie")
320+
this.getCalleeName() = "cookie"
327321
}
328322

329-
override DataFlow::Node getNameArgument() { result = this.getArgument(1) }
323+
// this = any(ReplyCall r).ref().getAMethodCall("cookie")
324+
override DataFlow::Node getNameArgument() { result = this.(ReplyCall).getArgument(1) }
330325

331-
override DataFlow::Node getValueArgument() { result = this.getArgument(2) }
326+
override DataFlow::Node getValueArgument() { result = this.(ReplyCall).getArgument(2) }
332327

333-
override RouteHandler getRouteHandler() { result = this.getRouteHandler() }
328+
override RouteHandler getRouteHandler() { result = this.(ReplyCall).getRouteHandler() }
334329
}
335330

336331
/**
337-
* A response argument passed to the `reply` method.
332+
* A response sent using a method on the `reply` object.
338333
*/
339-
private class ReplyArgument extends Http::ResponseSendArgument, DataFlow::Node {
334+
private class ReplyMethodCallArgument extends Http::ResponseSendArgument {
335+
ReplyCall reply;
336+
337+
ReplyMethodCallArgument() {
338+
// reply.header(RESPONSE, {'Cache-Control': 'no-cache'})
339+
reply.getCalleeName() =
340+
["cookie", "link", "header", "headers", "raw", "status", "toStream", "vary"] and
341+
reply.getArgument(0) = this
342+
}
343+
344+
override RouteHandler getRouteHandler() { result = reply.getRouteHandler() }
345+
}
346+
347+
/**
348+
* A response sent using the `reply()` method.
349+
*/
350+
private class ReplyCallArgument extends Http::ResponseSendArgument {
351+
ReplyCall reply;
352+
353+
ReplyCallArgument() {
354+
// reply(RESPONSE, {'Cache-Control': 'no-cache'})
355+
reply.isDirectReplyCall() and
356+
reply.getArgument(0) = this
357+
}
358+
359+
override RouteHandler getRouteHandler() { result = reply.getRouteHandler() }
360+
}
361+
362+
/**
363+
* The return statement for a route handler.
364+
*/
365+
private class RouteHandlerReturn extends Http::ResponseSendArgument {
340366
RouteHandler rh;
341367

342-
ReplyArgument() {
343-
exists(ReplySource reply, DataFlow::CallNode call |
344-
reply.ref() = call and
345-
call.getCalleeName() =
346-
["reply", "cookie", "link", "header", "headers", "raw", "status", "toStream", "vary"] and
347-
this = call.getArgument(0) and
348-
rh = reply.getRouteHandler()
349-
)
350-
or
351-
this = rh.getAReturn()
368+
RouteHandlerReturn() {
369+
this = rh.getAReturn() and not this.getALocalSource() instanceof ReplyCall
352370
}
353371

354372
override RouteHandler getRouteHandler() { result = rh }
355373
}
356374

357375
/**
358-
* An expression passed to the `template` method of the reply object
359-
* as the value of a template variable.
376+
* A call to `reply.template('template', { ... })`, seen as a template instantiation.
360377
*/
361-
private class TemplateInput extends Http::ResponseBody {
362-
TemplateObjectInput obj;
378+
private class TemplateCall extends Templating::TemplateInstantiation::Range instanceof ReplyCall {
379+
TemplateCall() { this.getCalleeName() = "template" }
363380

364-
TemplateInput() {
365-
obj.getALocalSource().(DataFlow::ObjectLiteralNode).hasPropertyWrite(_, this)
366-
}
381+
override DataFlow::SourceNode getOutput() { result = this }
367382

368-
override RouteHandler getRouteHandler() { result = obj.getRouteHandler() }
383+
override DataFlow::Node getTemplateFileNode() { result = this.(ReplyCall).getArgument(0) }
384+
385+
override DataFlow::Node getTemplateParamsNode() { result = this.(ReplyCall).getArgument(1) }
369386
}
370387

371388
/**
372389
* An object passed to the `template` method of the reply object.
373390
*/
374391
private class TemplateObjectInput extends DataFlow::Node {
375-
ReplySource reply;
392+
TemplateCall call;
376393

377-
TemplateObjectInput() {
378-
exists(DataFlow::MethodCallNode call |
379-
reply.ref() = call and
380-
call.getMethodName() = "template" and
381-
this = call.getArgument(1)
382-
)
383-
}
394+
TemplateObjectInput() { this = call.(ReplyCall).getArgument(1) }
384395

385396
/**
386397
* Gets the route handler that uses this object.
387398
*/
388-
RouteHandler getRouteHandler() { result = reply.getRouteHandler() }
399+
RouteHandler getRouteHandler() { result = call.(ReplyCall).getRouteHandler() }
389400
}
390401

391402
/**
392-
* An invocation of the `redirect` method of an HTTP response object.
403+
* An expression passed to the `template` method of the reply object
404+
* as the value of a template variable.
393405
*/
394-
private class RedirectInvocation extends Http::RedirectInvocation, DataFlow::MethodCallNode instanceof ReplySource {
395-
RedirectInvocation() { this.ref().(DataFlow::MethodCallNode).getMethodName() = "redirect" }
406+
private class TemplateInput extends Http::ResponseBody {
407+
TemplateObjectInput obj;
396408

397-
override DataFlow::Node getUrlArgument() { result = this.getAnArgument() }
409+
TemplateInput() { obj.getALocalSource().hasPropertyWrite(_, this) }
398410

399-
override RouteHandler getRouteHandler() { result = this.getRouteHandler() }
411+
override RouteHandler getRouteHandler() { result = obj.getRouteHandler() }
400412
}
401413

402414
/**
403-
* A call to `reply.template('template', { ... })`, seen as a template instantiation.
415+
* An invocation of the `redirect` method of an HTTP response object.
404416
*/
405-
private class TemplateCall extends Templating::TemplateInstantiation::Range,
406-
DataFlow::MethodCallNode instanceof ReplySource {
407-
TemplateCall() { this.getMethodName() = "template" }
417+
private class RedirectInvocation extends Http::RedirectInvocation instanceof ReplyCall {
418+
RedirectInvocation() { this.getCalleeName() = "redirect" }
408419

409-
override DataFlow::SourceNode getOutput() { result = this }
410-
411-
override DataFlow::Node getTemplateFileNode() { result = this.getArgument(0) }
420+
override DataFlow::Node getUrlArgument() { result = this.getAnArgument() }
412421

413-
override DataFlow::Node getTemplateParamsNode() { result = this.getArgument(1) }
422+
override RouteHandler getRouteHandler() { result = this.getRouteHandler() }
414423
}
415424
}

javascript/ql/test/library-tests/frameworks/Spife/lib/views/index.js

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -35,23 +35,23 @@ function homepage(req, context) { // test: handler
3535
sink(req.urlObject.pathname) // test: source
3636
sink(context.get('package')) // test: source
3737
sink(context)
38-
return reply.template('home', { target: req.query.name }) // test: source, templateInstantiation, stackTraceExposureSink
38+
return reply.template('home', { target: req.query.name }) // test: source, templateInstantiation, stackTraceExposureSink, responseBody
3939
}
4040

4141
function raw1(req, context) { // test: handler
4242
sink(req.query.name) // test: source
4343
return reply(req.query.name, 200, { // test: source, xssSink, stackTraceExposureSink, xss
44-
"content-type": "text/html",
45-
"access-control-allow-origin": "*", // test: corsMiconfigurationSink
46-
"access-control-allow-headers": "Content-Type, Authorization, Content-Length, X-Requested-With",
47-
"access-control-allow-methods": "GET, POST, PUT, DELETE, OPTIONS",
48-
"access-control-allow-credentials": "true"
44+
"content-type": "text/html", // test: headerDefinition
45+
"access-control-allow-origin": "*", // test: corsMiconfigurationSink, headerDefinition
46+
"access-control-allow-headers": "Content-Type, Authorization, Content-Length, X-Requested-With", // test: headerDefinition
47+
"access-control-allow-methods": "GET, POST, PUT, DELETE, OPTIONS", // test: headerDefinition
48+
"access-control-allow-credentials": "true" //test: headerDefinition
4949

5050
})
5151
}
5252

5353
function redirect(req, context) { // test: handler
54-
return reply.redirect(context.get('redirect_url')) // test: redirectSink, source, stackTraceExposureSink
54+
return reply.redirect(context.get('redirect_url')) // test: redirectSink, source
5555
}
5656
function raw2(req, context) { // test: handler
5757
return reply.cookie({ "test": req.query.name }, "test", req.query.name, { "httpOnly": false, "secure": false }) // test: source, cleartextStorageSink, stackTraceExposureSink, cookieDefinition
@@ -64,7 +64,7 @@ function test1(req, context) { // test: handler
6464
case 'html':
6565
return reply.header('<p>' + req.query.name + '</p>', 'content-type', 'text/html') // test: source, xssSink, stackTraceExposureSink, xss, headerDefinition
6666
case 'plain':
67-
return reply.header('<p>' + req.query.name + '</p>', { 'content-type': 'text/plain' }) // test: source, stackTraceExposureSink, !xssSink, !xss, headerDefinition
67+
return reply.header('<p>' + req.query.name + '</p>', { 'content-type': 'text/plain' }) // test: source, stackTraceExposureSink, !xssSink, !xss, headerDefinition, headerDefinition
6868
}
6969
return 'well, I guess you just want plaintext.'
7070
}
@@ -87,7 +87,7 @@ function test4(req, context) { // test: handler
8787
const body = req.body // test: source
8888
const newPackument = body['package-json']
8989
const message = `INFO: User invited to package ${newPackument._id} successfully.`
90-
return reply(message, 200, { 'npm-notice': message }) // test: stackTraceExposureSink, !xssSink, !xss
90+
return reply(message, 200, { 'npm-notice': message }) // test: stackTraceExposureSink, !xssSink, !xss, headerDefinition
9191
}
9292

9393
function test5(req, context) { // test: handler
@@ -102,7 +102,7 @@ function test6(req, context) { // test: handler
102102
const newPackument = body['package-json']
103103
const message = `INFO: User invited to package ${newPackument._id} successfully.`
104104
if (message.contains('foo')) {
105-
return reply(message, 200, { 'npm-notice': message }) // test: stackTraceExposureSink, !xssSink, !xss
105+
return reply(message, 200, { 'npm-notice': message }) // test: stackTraceExposureSink, !xssSink, !xss, headerDefinition
106106
} else {
107107
return reply(message, 200, { 'npm-notice': message, 'content-type': 'text/html' }) // test: stackTraceExposureSink, xssSink, xss, headerDefinition
108108
}

0 commit comments

Comments
 (0)