Merge pull request #241 from asger-semmle/host-header-forgery

JS: Add HostHeaderPoisoningInEmailGeneration query
This commit is contained in:
Max Schaefer
2018-10-02 08:32:00 +01:00
committed by GitHub
27 changed files with 486 additions and 83 deletions

View File

@@ -0,0 +1,46 @@
<!DOCTYPE qhelp PUBLIC "-//Semmle//qhelp//EN" "qhelp.dtd">
<qhelp>
<overview>
<p>
Using the HTTP Host header to construct a link in an email can facilitate phishing attacks and leak password reset tokens.
A malicious user can send an HTTP request to the targeted web site, but with a Host header that refers to his own web site.
This means the emails will be sent out to potential victims, originating from a server they trust, but with
links leading to a malicious web site.
</p>
<p>
If the email contains a password reset link, and should the victim click the link, the secret reset token will be leaked to the attacker.
Using the leaked token, the attacker can then construct the real reset link and use it to change the victim's password.
</p>
</overview>
<recommendation>
<p>
Obtain the server's host name from a configuration file and avoid relying on the Host header.
</p>
</recommendation>
<example>
<p>
The following example uses the <code>req.host</code> to generate a password reset link.
This value is derived from the Host header, and can thus be set to anything by an attacker:
</p>
<sample src="examples/HostHeaderPoisoningInEmailGeneration.js"/>
<p>
To ensure the link refers to the correct web site, get the host name from a configuration file:
</p>
<sample src="examples/HostHeaderPoisoningInEmailGenerationGood.js"/>
</example>
<references>
<li>
Mitre:
<a href="https://cwe.mitre.org/data/definitions/640.html">CWE-640: Weak Password Recovery Mechanism for Forgotten Password</a>.
</li>
<li>
Ian Muscat:
<a href="https://www.acunetix.com/blog/articles/automated-detection-of-host-header-attacks/">What is a Host Header Attack?</a>.
</li>
</references>
</qhelp>

View File

@@ -0,0 +1,29 @@
/**
* @name Host header poisoning in email generation
* @description Using the HTTP Host header to construct a link in an email can facilitate phishing attacks and leak password reset tokens.
* @kind problem
* @problem.severity error
* @precision high
* @id js/host-header-forgery-in-email-generation
* @tags security
* external/cwe/cwe-640
*/
import javascript
class TaintedHostHeader extends TaintTracking::Configuration {
TaintedHostHeader() { this = "TaintedHostHeader" }
override predicate isSource(DataFlow::Node node) {
exists (HTTP::RequestHeaderAccess input | node = input |
input.getKind() = "header" and
input.getAHeaderName() = "host")
}
override predicate isSink(DataFlow::Node node) {
exists (EmailSender email | node = email.getABody())
}
}
from TaintedHostHeader taint, DataFlow::Node src, DataFlow::Node sink
where taint.hasFlow(src, sink)
select sink, "Links in this email can be hijacked by poisoning the HTTP host header $@.", src, "here"

View File

@@ -0,0 +1,19 @@
let nodemailer = require('nodemailer');
let express = require('express');
let backend = require('./backend');
let app = express();
let config = JSON.parse(fs.readFileSync('config.json', 'utf8'));
app.post('/resetpass', (req, res) => {
let email = req.query.email;
let transport = nodemailer.createTransport(config.smtp);
let token = backend.getUserSecretResetToken(email);
transport.sendMail({
from: 'webmaster@example.com',
to: email,
subject: 'Forgot password',
text: `Click to reset password: https://${req.host}/resettoken/${token}`,
});
});

View File

@@ -0,0 +1,19 @@
let nodemailer = require('nodemailer');
let express = require('express');
let backend = require('./backend');
let app = express();
let config = JSON.parse(fs.readFileSync('config.json', 'utf8'));
app.post('/resetpass', (req, res) => {
let email = req.query.email;
let transport = nodemailer.createTransport(config.smtp);
let token = backend.getUserSecretResetToken(email);
transport.sendMail({
from: 'webmaster@example.com',
to: email,
subject: 'Forgot password',
text: `Click to reset password: https://${config.hostname}/resettoken/${token}`,
});
});

View File

@@ -13,6 +13,7 @@ import semmle.javascript.Constants
import semmle.javascript.DataFlow
import semmle.javascript.DefUse
import semmle.javascript.DOM
import semmle.javascript.EmailClients
import semmle.javascript.Errors
import semmle.javascript.ES2015Modules
import semmle.javascript.Expr

View File

@@ -0,0 +1,68 @@
import javascript
/**
* An operation that sends an email.
*/
abstract class EmailSender extends DataFlow::DefaultSourceNode {
/**
* Gets a data flow node holding the plaintext version of the email body.
*/
abstract DataFlow::Node getPlainTextBody();
/**
* Gets a data flow node holding the HTML body of the email.
*/
abstract DataFlow::Node getHtmlBody();
/**
* Gets a data flow node holding the address of the email recipient(s).
*/
abstract DataFlow::Node getTo();
/**
* Gets a data flow node holding the address of the email sender.
*/
abstract DataFlow::Node getFrom();
/**
* Gets a data flow node holding the email subject.
*/
abstract DataFlow::Node getSubject();
/**
* Gets a data flow node that refers to the HTML body or plaintext body of the email.
*/
DataFlow::Node getABody() {
result = getPlainTextBody() or
result = getHtmlBody()
}
}
/**
* An email-sending call based on the `nodemailer` package.
*/
private class NodemailerEmailSender extends EmailSender, DataFlow::MethodCallNode {
NodemailerEmailSender() {
this = DataFlow::moduleMember("nodemailer", "createTransport").getACall().getAMethodCall("sendMail")
}
override DataFlow::Node getPlainTextBody() {
result = getOptionArgument(0, "text")
}
override DataFlow::Node getHtmlBody() {
result = getOptionArgument(0, "html")
}
override DataFlow::Node getTo() {
result = getOptionArgument(0, "to")
}
override DataFlow::Node getFrom() {
result = getOptionArgument(0, "from")
}
override DataFlow::Node getSubject() {
result = getOptionArgument(0, "subject")
}
}

View File

@@ -471,29 +471,14 @@ module Express {
propName = "originalUrl"
)
or
exists (string methodName |
// `req.get(...)` or `req.header(...)`
kind = "header" and
this.(DataFlow::MethodCallNode).calls(request, methodName) |
methodName = "get" or
methodName = "header"
)
or
exists (DataFlow::PropRead headers |
// `req.headers.name`
kind = "header" and
headers.accesses(request, "headers") and
this = headers.getAPropertyRead())
or
exists (string propName | propName = "host" or propName = "hostname" |
// `req.host` and `req.hostname` are derived from headers
kind = "header" and
this.(DataFlow::PropRead).accesses(request, propName))
or
// `req.cookies`
kind = "cookie" and
this.(DataFlow::PropRef).accesses(request, "cookies")
)
or
exists (RequestHeaderAccess access | this = access |
rh = access.getRouteHandler() and
kind = "header")
}
override RouteHandler getRouteHandler() {
@@ -505,6 +490,53 @@ module Express {
}
}
/**
* An access to a header on an Express request.
*/
private class RequestHeaderAccess extends HTTP::RequestHeaderAccess {
RouteHandler rh;
RequestHeaderAccess() {
exists (DataFlow::Node request | request = DataFlow::valueNode(rh.getARequestExpr()) |
exists (string methodName |
// `req.get(...)` or `req.header(...)`
this.(DataFlow::MethodCallNode).calls(request, methodName) |
methodName = "get" or
methodName = "header"
)
or
exists (DataFlow::PropRead headers |
// `req.headers.name`
headers.accesses(request, "headers") and
this = headers.getAPropertyRead())
or
exists (string propName | propName = "host" or propName = "hostname" |
// `req.host` and `req.hostname` are derived from headers
this.(DataFlow::PropRead).accesses(request, propName))
)
}
override string getAHeaderName() {
exists (string name |
name = this.(DataFlow::PropRead).getPropertyName()
or
this.(DataFlow::CallNode).getArgument(0).mayHaveStringValue(name)
|
if name = "hostname" then
result = "host"
else
result = name.toLowerCase())
}
override RouteHandler getRouteHandler() {
result = rh
}
override string getKind() {
result = "header"
}
}
/**
* HTTP headers created by Express calls
*/
@@ -600,9 +632,9 @@ module Express {
astNode.getMethodName() = any(string n | n = "set" or n = "header") and
astNode.getNumArgument() = 1
}
/**
* Gets a reference to the multiple headers object that is to be set.
* Gets a reference to the multiple headers object that is to be set.
*/
private DataFlow::SourceNode getAHeaderSource() {
result.flowsToExpr(astNode.getArgument(0))
@@ -618,12 +650,12 @@ module Express {
override RouteHandler getRouteHandler() {
result = rh
}
override Expr getNameExpr() {
exists (DataFlow::PropWrite write |
exists (DataFlow::PropWrite write |
getAHeaderSource().flowsTo(write.getBase()) and
result = write.getPropertyNameExpr()
)
)
}
}

View File

@@ -72,9 +72,9 @@ module HTTP {
* Holds if the header with (lower-case) name `headerName` is set to the value of `headerValue`.
*/
abstract predicate definesExplicitly(string headerName, Expr headerValue);
/**
* Returns the expression used to compute the header name.
* Returns the expression used to compute the header name.
*/
abstract Expr getNameExpr();
}
@@ -354,9 +354,9 @@ module HTTP {
headerName = getNameExpr().getStringValue().toLowerCase() and
headerValue = astNode.getArgument(1)
}
override Expr getNameExpr() {
result = astNode.getArgument(0)
result = astNode.getArgument(0)
}
}
@@ -400,7 +400,20 @@ module HTTP {
*/
abstract string getKind();
}
/**
* An access to a header on an incoming HTTP request.
*/
abstract class RequestHeaderAccess extends RequestInputAccess {
/**
* Gets the lower-case name of an HTTP header from which this input is derived,
* if this can be determined.
*
* When the name of the header is unknown, this has no result.
*/
abstract string getAHeaderName();
}
/**
* A node that looks like a route setup on a server.
*

View File

@@ -121,13 +121,6 @@ module Hapi {
this.asExpr().(PropAccess).accesses(url, "path")
)
or
exists (PropAccess headers |
// `request.headers.<name>`
kind = "header" and
headers.accesses(request, "headers") and
this.asExpr().(PropAccess).accesses(headers, _)
)
or
exists (PropAccess state |
// `request.state.<name>`
kind = "cookie" and
@@ -135,6 +128,10 @@ module Hapi {
this.asExpr().(PropAccess).accesses(state, _)
)
)
or
exists (RequestHeaderAccess access | this = access |
rh = access.getRouteHandler() and
kind = "header")
}
override RouteHandler getRouteHandler() {
@@ -146,6 +143,35 @@ module Hapi {
}
}
/**
* An access to an HTTP header on a Hapi request.
*/
private class RequestHeaderAccess extends HTTP::RequestHeaderAccess {
RouteHandler rh;
RequestHeaderAccess() {
exists (Expr request | request = rh.getARequestExpr() |
exists (PropAccess headers |
// `request.headers.<name>`
headers.accesses(request, "headers") and
this.asExpr().(PropAccess).accesses(headers, _)
)
)
}
override string getAHeaderName() {
result = this.(DataFlow::PropRead).getPropertyName().toLowerCase()
}
override RouteHandler getRouteHandler() {
result = rh
}
override string getKind() {
result = "header"
}
}
/**
* An HTTP header defined in a Hapi server.
*/

View File

@@ -182,19 +182,6 @@ module Koa {
propName = "originalUrl" or
propName = "href"
)
or
exists (string propName, PropAccess headers |
// `ctx.request.header.<name>`, `ctx.request.headers.<name>`
kind = "header" and
headers.accesses(request, propName) and
this.asExpr().(PropAccess).accesses(headers, _) |
propName = "header" or
propName = "headers"
)
or
// `ctx.request.get(<name>)`
kind = "header" and
this.asExpr().(MethodCallExpr).calls(request, "get")
)
or
exists (PropAccess cookies |
@@ -203,6 +190,10 @@ module Koa {
cookies.accesses(rh.getAContextExpr(), "cookies") and
this.asExpr().(MethodCallExpr).calls(cookies, "get")
)
or
exists (RequestHeaderAccess access | access = this |
rh = access.getRouteHandler() and
kind = "header")
}
override RouteHandler getRouteHandler() {
@@ -214,6 +205,44 @@ module Koa {
}
}
/**
* An access to an HTTP header on a Koa request.
*/
private class RequestHeaderAccess extends HTTP::RequestHeaderAccess {
RouteHandler rh;
RequestHeaderAccess() {
exists (Expr request | request = rh.getARequestExpr() |
exists (string propName, PropAccess headers |
// `ctx.request.header.<name>`, `ctx.request.headers.<name>`
headers.accesses(request, propName) and
this.asExpr().(PropAccess).accesses(headers, _) |
propName = "header" or
propName = "headers"
)
or
// `ctx.request.get(<name>)`
this.asExpr().(MethodCallExpr).calls(request, "get")
)
}
override string getAHeaderName() {
result = this.(DataFlow::PropRead).getPropertyName().toLowerCase()
or
exists (string name |
this.(DataFlow::CallNode).getArgument(0).mayHaveStringValue(name) and
result = name.toLowerCase())
}
override RouteHandler getRouteHandler() {
result = rh
}
override string getKind() {
result = "header"
}
}
/**
* A call to a Koa method that sets up a route.
*/

View File

@@ -146,12 +146,16 @@ module NodeJSLib {
kind = "url" and
this.asExpr().(PropAccess).accesses(request, "url")
or
exists (PropAccess headers, string name |
// `req.headers.<name>`
if name = "cookie" then kind = "cookie" else kind= "header" |
exists (PropAccess headers |
// `req.headers.cookie`
kind = "cookie" and
headers.accesses(request, "headers") and
this.asExpr().(PropAccess).accesses(headers, name)
this.asExpr().(PropAccess).accesses(headers, "cookie")
)
or
exists (RequestHeaderAccess access | this = access |
request = access.getRequest() and
kind = "header")
}
override RouteHandler getRouteHandler() {
@@ -163,6 +167,38 @@ module NodeJSLib {
}
}
/**
* An access to an HTTP header (other than "Cookie") on an incoming Node.js request object.
*/
private class RequestHeaderAccess extends HTTP::RequestHeaderAccess {
RequestExpr request;
RequestHeaderAccess() {
exists (PropAccess headers, string name |
// `req.headers.<name>`
name != "cookie" and
headers.accesses(request, "headers") and
this.asExpr().(PropAccess).accesses(headers, name)
)
}
override string getAHeaderName() {
result = this.(DataFlow::PropRead).getPropertyName().toLowerCase()
}
override RouteHandler getRouteHandler() {
result = request.getRouteHandler()
}
override string getKind() {
result = "header"
}
RequestExpr getRequest() {
result = request
}
}
class RouteSetup extends CallExpr, HTTP::Servers::StandardRouteSetup {
ServerDefinition server;
Expr handler;
@@ -380,7 +416,7 @@ module NodeJSLib {
}
}
/**
* A call to a method from module `child_process`.
*/
@@ -476,21 +512,21 @@ module NodeJSLib {
}
}
/**
* A call to a method from module `vm`
*/
class VmModuleMethodCall extends DataFlow::CallNode {
string methodName;
VmModuleMethodCall() {
this = DataFlow::moduleMember("vm", methodName).getACall()
}
/**
* Gets the code to be executed as part of this call.
*/
DataFlow::Node getACodeArgument() {
DataFlow::Node getACodeArgument() {
(
methodName = "runInContext" or
methodName = "runInNewContext" or
@@ -543,7 +579,7 @@ module NodeJSLib {
}
}
/**
* A model of a URL request in the Node.js `http` library.
*/
@@ -569,7 +605,7 @@ module NodeJSLib {
}
}
/**
* A data flow node that is the parameter of a result callback for an HTTP or HTTPS request made by a Node.js process, for example `res` in `https.request(url, (res) => {})`.
*/
@@ -579,12 +615,12 @@ module NodeJSLib {
this = req.(DataFlow::MethodCallNode).getCallback(1).getParameter(0)
)
}
override string getSourceType() {
result = "NodeJSClientRequest callback parameter"
}
}
/**
* A data flow node that is the parameter of a data callback for an HTTP or HTTPS request made by a Node.js process, for example `body` in `http.request(url, (res) => {res.on('data', (body) => {})})`.
*/
@@ -596,20 +632,20 @@ module NodeJSLib {
this = mcn.getCallback(1).getParameter(0)
)
}
override string getSourceType() {
result = "http.request data parameter"
}
}
/**
* A data flow node that is registered as a callback for an HTTP or HTTPS request made by a Node.js process, for example the function `handler` in `http.request(url).on(message, handler)`.
*/
class ClientRequestHandler extends DataFlow::FunctionNode {
string handledEvent;
NodeJSClientRequest clientRequest;
ClientRequestHandler() {
exists(DataFlow::MethodCallNode mcn |
clientRequest.getAMethodCall("on") = mcn and
@@ -617,14 +653,14 @@ module NodeJSLib {
flowsTo(mcn.getArgument(1))
)
}
/**
* Gets the name of an event this callback is registered for.
*/
string getAHandledEvent() {
result = handledEvent
}
/**
* Gets a request this callback is registered for.
*/
@@ -632,7 +668,7 @@ module NodeJSLib {
result = clientRequest
}
}
/**
* A data flow node that is the parameter of a response callback for an HTTP or HTTPS request made by a Node.js process, for example `res` in `http.request(url).on('response', (res) => {})`.
*/
@@ -643,12 +679,12 @@ module NodeJSLib {
handler.getAHandledEvent() = "response"
)
}
override string getSourceType() {
result = "NodeJSClientRequest response event"
}
}
/**
* A data flow node that is the parameter of a data callback for an HTTP or HTTPS request made by a Node.js process, for example `chunk` in `http.request(url).on('response', (res) => {res.on('data', (chunk) => {})})`.
*/
@@ -660,12 +696,12 @@ module NodeJSLib {
this = mcn.getCallback(1).getParameter(0)
)
}
override string getSourceType() {
result = "NodeJSClientRequest data event"
}
}
/**
* A data flow node that is a login callback for an HTTP or HTTPS request made by a Node.js process.
*/
@@ -674,7 +710,7 @@ module NodeJSLib {
getAHandledEvent() = "login"
}
}
/**
* A data flow node that is a parameter of a login callback for an HTTP or HTTPS request made by a Node.js process, for example `res` in `http.request(url).on('login', (res, callback) => {})`.
*/
@@ -684,12 +720,12 @@ module NodeJSLib {
this = handler.getParameter(0)
)
}
override string getSourceType() {
result = "NodeJSClientRequest login event"
}
}
/**
* A data flow node that is the login callback provided by an HTTP or HTTPS request made by a Node.js process, for example `callback` in `http.request(url).on('login', (res, callback) => {})`.
*/
@@ -700,7 +736,7 @@ module NodeJSLib {
)
}
}
/**
* A data flow node that is the username passed to the login callback provided by an HTTP or HTTPS request made by a Node.js process, for example `username` in `http.request(url).on('login', (res, cb) => {cb(username, password)})`.
*/
@@ -710,14 +746,14 @@ module NodeJSLib {
this = callback.getACall().getArgument(0).asExpr()
)
}
override string getCredentialsKind() {
result = "Node.js http(s) client login username"
}
}
/**
* A data flow node that is the password passed to the login callback provided by an HTTP or HTTPS request made by a Node.js process, for example `password` in `http.request(url).on('login', (res, cb) => {cb(username, password)})`.
* A data flow node that is the password passed to the login callback provided by an HTTP or HTTPS request made by a Node.js process, for example `password` in `http.request(url).on('login', (res, cb) => {cb(username, password)})`.
*/
private class ClientRequestLoginPassword extends CredentialsExpr {
ClientRequestLoginPassword() {
@@ -725,13 +761,13 @@ module NodeJSLib {
this = callback.getACall().getArgument(1).asExpr()
)
}
override string getCredentialsKind() {
result = "Node.js http(s) client login password"
}
}
/**
* A data flow node that is the parameter of an error callback for an HTTP or HTTPS request made by a Node.js process, for example `err` in `http.request(url).on('error', (err) => {})`.
*/
@@ -742,7 +778,7 @@ module NodeJSLib {
handler.getAHandledEvent() = "error"
)
}
override string getSourceType() {
result = "NodeJSClientRequest error event"
}