Skip to content

Commit bc13204

Browse files
committed
refactor header checks to be based on dominance
1 parent 9016f43 commit bc13204

File tree

2 files changed

+44
-29
lines changed

2 files changed

+44
-29
lines changed

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

Lines changed: 35 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -319,8 +319,8 @@ module ReflectedXss {
319319
send.getRouteHandler() = h and
320320
result = nonHtmlContentTypeHeader(h)
321321
|
322-
// The HeaderDefinition affects a response sent at `send`.
323-
not isIrrelevantFor(result, send)
322+
// The HeaderDefinition affects a response sent at `send`.
323+
headerAffects(result, send)
324324
)
325325
}
326326

@@ -333,40 +333,47 @@ module ReflectedXss {
333333
}
334334

335335
/**
336-
* Holds if a header set in `header` is unlikely to affect a response sent at `sender`.
336+
* Holds if a header set in `header` is likely to affect a response sent at `sender`.
337337
*/
338-
predicate isIrrelevantFor(HTTP::HeaderDefinition header, HTTP::ResponseSendArgument sender) {
338+
predicate headerAffects(HTTP::HeaderDefinition header, HTTP::ResponseSendArgument sender) {
339339
sender.getRouteHandler() = header.getRouteHandler() and
340-
not header.getBasicBlock().getASuccessor*() = sender.getBasicBlock() and
341-
not sender.getBasicBlock().getASuccessor*() = header.getBasicBlock() and
342340
(
343-
// If there is another header `otherHeader` next to `sender`, then `header` is probably irrelevant.
344-
exists(HTTP::HeaderDefinition otherHeader | not header = otherHeader |
345-
otherHeader.getBasicBlock().getASuccessor*() = sender.getBasicBlock() and
346-
not otherHeader = nonHtmlContentTypeHeader(sender.getRouteHandler())
347-
)
341+
// `sender` is affected by a dominating `header`.
342+
header.getBasicBlock().(ReachableBasicBlock).dominates(sender.getBasicBlock())
348343
or
349-
// Tries to recognize variants of:
350-
// ```
351-
// response.writeHead(500, {'Content-Type': 'text/plain'});
352-
// response.end('Some error');
353-
// return;
354-
// ```
355-
exists(ReachableBasicBlock headerBlock | headerBlock = header.getBasicBlock() |
356-
headerBlock.getASuccessor() instanceof ControlFlowExitNode and
357-
strictcount(headerBlock.getASuccessor()) = 1 and
358-
not (
359-
exists(DataFlow::CallNode call |
360-
exists(call.getACallee()) and
361-
call.getBasicBlock() = headerBlock
362-
)
363-
or
364-
exists(Expr e | e.getBasicBlock() = headerBlock and e instanceof Function)
365-
)
344+
// There is no dominating header, and `header` is non-local.
345+
not isLocalHeaderDefinition(header) and
346+
not exists(HTTP::HeaderDefinition dominatingHeader |
347+
dominatingHeader.getBasicBlock().(ReachableBasicBlock).dominates(sender.getBasicBlock())
366348
)
367349
)
368350
}
369351

352+
/**
353+
* Holds if the HeaderDefinition `header` seems to be local.
354+
* A HeaderDefinition is local if it dominates exactly one `ResponseSendArgument`.
355+
*
356+
* Recognizes variants of:
357+
* ```
358+
* response.writeHead(500, ...);
359+
* response.end('Some error');
360+
* return;
361+
* ```
362+
*/
363+
predicate isLocalHeaderDefinition(HTTP::HeaderDefinition header) {
364+
exists(ReachableBasicBlock headerBlock |
365+
headerBlock = header.getBasicBlock().(ReachableBasicBlock)
366+
|
367+
1 =
368+
strictcount(HTTP::ResponseSendArgument sender |
369+
sender.getRouteHandler() = header.getRouteHandler() and
370+
header.getBasicBlock().(ReachableBasicBlock).dominates(sender.getBasicBlock())
371+
) and
372+
// doesn't dominate something that looks like a callback.
373+
not exists(Expr e | e instanceof Function | headerBlock.dominates(e.getBasicBlock()))
374+
)
375+
}
376+
370377
/**
371378
* A regexp replacement involving an HTML meta-character, viewed as a sanitizer for
372379
* XSS vulnerabilities.

javascript/ql/test/query-tests/Security/CWE-079/ReflectedXssContentTypes.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,16 @@ app.get('/user/:id', function (req, res) {
6464
return;
6565
}
6666
doSomething();
67-
somethingMOre();
67+
somethingMore();
6868
while(Math.random()) {};
6969
res.writeHead(404);
7070
res.send("FOO: " + req.params.id); // NOT OK - content type is not set.
71+
});
72+
73+
app.get('/user/:id', function (req, res) {
74+
res.header({'Content-Type': textContentType()});
75+
myFancyFunction(() => {
76+
res.send("FOO: " + req.params.id); // OK
77+
});
78+
res.end("FOO: " + req.params.id); // OK
7179
});

0 commit comments

Comments
 (0)