recognize more HttpResponseSink by restricting the hasNonHtmlHeader check

This commit is contained in:
Erik Krogh Kristensen
2020-02-28 10:42:08 +01:00
parent b210009eec
commit c14a485ca7
5 changed files with 161 additions and 7 deletions

View File

@@ -271,7 +271,7 @@ module HTTP {
*/
abstract class StandardRouteHandler extends RouteHandler {
override HeaderDefinition getAResponseHeader(string name) {
result.(StandardHeaderDefinition).getRouteHandler() = this and
result.getRouteHandler() = this and
result.getAHeaderName() = name
}

View File

@@ -252,10 +252,11 @@ module NodeJSLib {
private class WriteHead extends HeaderDefinition {
WriteHead() {
astNode.getMethodName() = "writeHead" and
astNode.getNumArgument() > 1
astNode.getNumArgument() >= 1
}
override predicate definesExplicitly(string headerName, Expr headerValue) {
astNode.getNumArgument() > 1 and
exists(DataFlow::SourceNode headers, string header |
headers.flowsToExpr(astNode.getLastArgument()) and
headers.hasPropertyWrite(header, DataFlow::valueNode(headerValue)) and

View File

@@ -305,18 +305,64 @@ module ReflectedXss {
* a content type that does not (case-insensitively) contain the string "html". This
* is to prevent us from flagging plain-text or JSON responses as vulnerable.
*/
private class HttpResponseSink extends Sink, DataFlow::ValueNode {
class HttpResponseSink extends Sink, DataFlow::ValueNode {
override HTTP::ResponseSendArgument astNode;
HttpResponseSink() { not nonHtmlContentType(astNode.getRouteHandler()) }
HttpResponseSink() { not exists(getAnonHtmlHeaderDefinition(astNode)) }
}
/**
* Gets a HeaderDefinition that defines a non-html content-type for `send`.
*/
HTTP::HeaderDefinition getAnonHtmlHeaderDefinition(HTTP::ResponseSendArgument send) {
exists(HTTP::RouteHandler h |
send.getRouteHandler() = h and
result = nonHtmlContentTypeHeader(h)
|
// not the case that the control just exists without potentially going to the worksFor.
not isIrrelevantFor(result, send)
)
}
/**
* Holds if `h` may send a response with a content type other than HTML.
*/
private predicate nonHtmlContentType(HTTP::RouteHandler h) {
exists(HTTP::HeaderDefinition hd | hd = h.getAResponseHeader("content-type") |
not exists(string tp | hd.defines("content-type", tp) | tp.regexpMatch("(?i).*html.*"))
HTTP::HeaderDefinition nonHtmlContentTypeHeader(HTTP::RouteHandler h) {
result = h.getAResponseHeader("content-type") and
not exists(string tp | result.defines("content-type", tp) | tp.regexpMatch("(?i).*html.*"))
}
/**
* Holds if a header set in `header` is unlikely to affect a resonse send in `sender`.
*/
predicate isIrrelevantFor(HTTP::HeaderDefinition header, HTTP::ResponseSendArgument sender) {
not header.getBasicBlock().getASuccessor*() = sender.getBasicBlock() and
not sender.getBasicBlock().getASuccessor*() = header.getBasicBlock() and
(
// If there is another header `otherHeader` next to `sender`, then `header` is probably irrelevant.
exists(HTTP::HeaderDefinition otherHeader | not header = otherHeader |
otherHeader.getBasicBlock().getASuccessor*() = sender.getBasicBlock() and
not otherHeader = nonHtmlContentTypeHeader(sender.getRouteHandler())
)
or
// Tries to recognize variants of:
// ```
// response.writeHead(500, {'Content-Type': 'text/plain'});
// response.end('Some error');
// return;
// ```
exists(ReachableBasicBlock headerBlock | headerBlock = header.getBasicBlock() |
headerBlock.getASuccessor() instanceof ControlFlowExitNode and
strictcount(headerBlock.getASuccessor()) = 1 and
not (
exists(DataFlow::CallNode call |
exists(call.getACallee()) and
call.getBasicBlock() = headerBlock
)
or
exists(Expr e | e.getBasicBlock() = headerBlock and e instanceof Function)
)
)
)
}