mirror of
https://github.com/github/codeql.git
synced 2026-04-28 10:15:14 +02:00
Merge remote-tracking branch 'upstream/master' into mergeback-2018-10-08
This commit is contained in:
@@ -33,12 +33,15 @@ predicate deadStoreOfLocal(VarDef vd, PurelyLocalVariable v) {
|
||||
* is itself an expression evaluating to `null` or `undefined`.
|
||||
*/
|
||||
predicate isNullOrUndef(Expr e) {
|
||||
// `null` or `undefined`
|
||||
e instanceof NullLiteral or
|
||||
e.(VarAccess).getName() = "undefined" or
|
||||
e instanceof VoidExpr or
|
||||
// recursive case to catch multi-assignments of the form `x = y = null`
|
||||
isNullOrUndef(e.(AssignExpr).getRhs())
|
||||
exists (Expr inner |
|
||||
inner = e.stripParens() |
|
||||
// `null` or `undefined`
|
||||
inner instanceof NullLiteral or
|
||||
inner.(VarAccess).getName() = "undefined" or
|
||||
inner instanceof VoidExpr or
|
||||
// recursive case to catch multi-assignments of the form `x = y = null`
|
||||
isNullOrUndef(inner.(AssignExpr).getRhs())
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -39,7 +39,7 @@ property of the name stored in variable <code>member</code>:
|
||||
|
||||
<p>
|
||||
However, this test is ineffective as written: the operator <code>!</code> binds more
|
||||
tighly than <code>in</code>, so it is applied first. Applying <code>!</code> to a
|
||||
tightly than <code>in</code>, so it is applied first. Applying <code>!</code> to a
|
||||
non-empty string yields <code>false</code>, so the <code>in</code> operator actually
|
||||
ends up checking whether <code>obj</code> contains a property called <code>"false"</code>.
|
||||
</p>
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
<!DOCTYPE qhelp PUBLIC "-//Semmle//qhelp//EN" "qhelp.dtd">
|
||||
<qhelp>
|
||||
|
||||
<overview>
|
||||
<p>
|
||||
Nested expressions that rely on less well-known operator precedence rules can be
|
||||
hard to read and understand. They could even indicate a bug where the author of the
|
||||
code misunderstood the precedence rules.
|
||||
</p>
|
||||
</overview>
|
||||
|
||||
<recommendation>
|
||||
<p>
|
||||
Use parentheses or additional whitespace to clarify grouping.
|
||||
</p>
|
||||
</recommendation>
|
||||
|
||||
<example>
|
||||
<p>
|
||||
Consider the following snippet of code:
|
||||
</p>
|
||||
|
||||
<sample src="examples/UnclearOperatorPrecedence.js" />
|
||||
|
||||
<p>
|
||||
It might look like this tests whether <code>x</code> and <code>y</code> have any bits in
|
||||
common, but in fact <code>==</code> binds more tightly than <code>&</code>, so the test
|
||||
is equivalent to <code>x & (y == 0)</code>.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
If this is the intended interpretation, parentheses should be used to clarify this. You could
|
||||
also consider adding extra whitespace around <code>&</code> or removing whitespace
|
||||
around <code>==</code> to make it visually apparent that it binds less tightly:
|
||||
<code>x & y==0</code>.
|
||||
</p>
|
||||
<p>
|
||||
Probably the best approach in this case, though, would be to use the <code>&&</code>
|
||||
operator instead to clarify the intended interpretation: <code>x && y == 0</code>.
|
||||
</p>
|
||||
</example>
|
||||
|
||||
<references>
|
||||
<li>Mozilla Developer Network, <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence">Operator precedence</a>.</li>
|
||||
</references>
|
||||
</qhelp>
|
||||
29
javascript/ql/src/Expressions/UnclearOperatorPrecedence.ql
Normal file
29
javascript/ql/src/Expressions/UnclearOperatorPrecedence.ql
Normal file
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* @name Unclear precedence of nested operators
|
||||
* @description Nested expressions involving binary bitwise operators and comparisons are easy
|
||||
* to misunderstand without additional disambiguating parentheses or whitespace.
|
||||
* @kind problem
|
||||
* @problem.severity recommendation
|
||||
* @id js/unclear-operator-precedence
|
||||
* @tags maintainability
|
||||
* correctness
|
||||
* statistical
|
||||
* non-attributable
|
||||
* external/cwe/cwe-783
|
||||
* @precision very-high
|
||||
*/
|
||||
|
||||
import javascript
|
||||
|
||||
from BitwiseBinaryExpr bit, Comparison rel, Expr other
|
||||
where bit.hasOperands(rel, other) and
|
||||
// only flag if whitespace doesn't clarify the nesting (note that if `bit` has less
|
||||
// whitespace than `rel`, it will be reported by `js/whitespace-contradicts-precedence`)
|
||||
bit.getWhitespaceAroundOperator() = rel.getWhitespaceAroundOperator() and
|
||||
// don't flag if the other operand is itself a comparison,
|
||||
// since the nesting tends to be visually more obvious in such cases
|
||||
not other instanceof Comparison and
|
||||
// don't flag occurrences in minified code
|
||||
not rel.getTopLevel().isMinified()
|
||||
select rel, "The '" + rel.getOperator() + "' operator binds more tightly than " +
|
||||
"'" + bit.getOperator() + "', which may not be obvious in this case."
|
||||
@@ -54,29 +54,6 @@ class HarmlessNestedExpr extends BinaryExpr {
|
||||
}
|
||||
}
|
||||
|
||||
/** Holds if the right operand of `expr` starts on line `line`, at column `col`. */
|
||||
predicate startOfBinaryRhs(BinaryExpr expr, int line, int col) {
|
||||
exists(Location rloc | rloc = expr.getRightOperand().getLocation() |
|
||||
rloc.getStartLine() = line and rloc.getStartColumn() = col
|
||||
)
|
||||
}
|
||||
|
||||
/** Holds if the left operand of `expr` ends on line `line`, at column `col`. */
|
||||
predicate endOfBinaryLhs(BinaryExpr expr, int line, int col) {
|
||||
exists(Location lloc | lloc = expr.getLeftOperand().getLocation() |
|
||||
lloc.getEndLine() = line and lloc.getEndColumn() = col
|
||||
)
|
||||
}
|
||||
|
||||
/** Gets the number of whitespace characters around the operator of `expr`. */
|
||||
int operatorWS(BinaryExpr expr) {
|
||||
exists(int line, int lcol, int rcol |
|
||||
endOfBinaryLhs(expr, line, lcol) and
|
||||
startOfBinaryRhs(expr, line, rcol) and
|
||||
result = rcol - lcol + 1 - expr.getOperator().length()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `inner` is an operand of `outer`, and the relative precedence
|
||||
* may not be immediately clear, but is important for the semantics of
|
||||
@@ -88,10 +65,8 @@ predicate interestingNesting(BinaryExpr inner, BinaryExpr outer) {
|
||||
not inner instanceof HarmlessNestedExpr
|
||||
}
|
||||
|
||||
from BinaryExpr inner, BinaryExpr outer, int wsouter, int wsinner
|
||||
from BinaryExpr inner, BinaryExpr outer
|
||||
where interestingNesting(inner, outer) and
|
||||
wsinner = operatorWS(inner) and wsouter = operatorWS(outer) and
|
||||
wsinner % 2 = 0 and wsouter % 2 = 0 and
|
||||
wsinner > wsouter and
|
||||
inner.getWhitespaceAroundOperator() > outer.getWhitespaceAroundOperator() and
|
||||
not outer.getTopLevel().isMinified()
|
||||
select outer, "Whitespace around nested operators contradicts precedence."
|
||||
@@ -0,0 +1,3 @@
|
||||
if (x & y == 0) {
|
||||
// ...
|
||||
}
|
||||
@@ -6,7 +6,7 @@ express().get('/list-directory', function(req, res) {
|
||||
var list = '<ul>';
|
||||
fileNames.forEach(fileName => {
|
||||
// BAD: `fileName` can contain HTML elements
|
||||
list += '<li>' + fileName '</li>';
|
||||
list += '<li>' + fileName + '</li>';
|
||||
});
|
||||
list += '</ul>'
|
||||
res.send(list);
|
||||
|
||||
@@ -7,7 +7,7 @@ express().get('/list-directory', function(req, res) {
|
||||
var list = '<ul>';
|
||||
fileNames.forEach(fileName => {
|
||||
// GOOD: escaped `fileName` can not contain HTML elements
|
||||
list += '<li>' + escape(fileName) '</li>';
|
||||
list += '<li>' + escape(fileName) + '</li>';
|
||||
});
|
||||
list += '</ul>'
|
||||
res.send(list);
|
||||
|
||||
16
javascript/ql/src/Security/CWE-200/FileAccessToHttp.ql
Normal file
16
javascript/ql/src/Security/CWE-200/FileAccessToHttp.ql
Normal file
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* @name File Access data flows to Http POST/PUT
|
||||
* @description Writing data from file directly to http body or request header can be an indication to data exfiltration or unauthorized information disclosure.
|
||||
* @kind problem
|
||||
* @problem.severity warning
|
||||
* @id js/file-access-to-http
|
||||
* @tags security
|
||||
* external/cwe/cwe-200
|
||||
*/
|
||||
|
||||
import javascript
|
||||
import semmle.javascript.security.dataflow.FileAccessToHttp
|
||||
|
||||
from FileAccessToHttpDataFlow::Configuration config, DataFlow::Node src, DataFlow::Node sink
|
||||
where config.hasFlow (src, sink)
|
||||
select src, "$@ flows directly to Http request body", sink, "File access"
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
@@ -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}`,
|
||||
});
|
||||
});
|
||||
@@ -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}`,
|
||||
});
|
||||
});
|
||||
16
javascript/ql/src/Security/CWE-912/HttpToFileAccess.ql
Normal file
16
javascript/ql/src/Security/CWE-912/HttpToFileAccess.ql
Normal file
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* @name Http response data flows to File Access
|
||||
* @description Writing data from an HTTP request directly to the file system allows arbitrary file upload and might indicate a backdoor.
|
||||
* @kind problem
|
||||
* @problem.severity warning
|
||||
* @id js/http-to-file-access
|
||||
* @tags security
|
||||
* external/cwe/cwe-912
|
||||
*/
|
||||
|
||||
import javascript
|
||||
import semmle.javascript.security.dataflow.HttpToFileAccess
|
||||
|
||||
from HttpToFileAccessFlow::Configuration configuration, DataFlow::Node src, DataFlow::Node sink
|
||||
where configuration.hasFlow(src, sink)
|
||||
select sink, "$@ flows to file system", src, "Untrusted data received from Http response"
|
||||
@@ -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
|
||||
|
||||
@@ -281,7 +281,7 @@ import javascript
|
||||
*/
|
||||
class ControlFlowNode extends @cfg_node, Locatable {
|
||||
/** Gets a node succeeding this node in the CFG. */
|
||||
ControlFlowNode getASuccessor() {
|
||||
cached ControlFlowNode getASuccessor() {
|
||||
successor(this, result)
|
||||
}
|
||||
|
||||
@@ -457,3 +457,47 @@ class ConcreteControlFlowNode extends ControlFlowNode {
|
||||
not this instanceof SyntheticControlFlowNode
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A CFG node corresponding to a nested (that is, non-toplevel) import declaration.
|
||||
*
|
||||
* This is a non-standard language feature that is not currently supported very well
|
||||
* by the extractor, in particular such imports do not appear in the control flow graph
|
||||
* generated by the extractor. We patch them in by overriding `getASuccessor`; once an
|
||||
* extractor fix becomes available, this class can be removed.
|
||||
*/
|
||||
private class NestedImportDeclaration extends ImportDeclaration {
|
||||
NestedImportDeclaration() {
|
||||
exists (ASTNode p | p = getParent() |
|
||||
not p instanceof TopLevel
|
||||
) and
|
||||
// if there are no specifiers, the default control flow graph is fine
|
||||
exists(getASpecifier())
|
||||
}
|
||||
|
||||
override ControlFlowNode getASuccessor() {
|
||||
result = getSpecifier(0).getFirstControlFlowNode()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A CFG node corresponding to an import specifier in a nested import declaration.
|
||||
*
|
||||
* As for `NestedImportDeclaration` above, this is a temporary workaround that will be
|
||||
* removed once extractor support for this non-standard language feature becomes available.
|
||||
*/
|
||||
private class NestedImportSpecifier extends ImportSpecifier {
|
||||
NestedImportDeclaration decl;
|
||||
int i;
|
||||
|
||||
NestedImportSpecifier() {
|
||||
this = decl.getSpecifier(i)
|
||||
}
|
||||
|
||||
override ControlFlowNode getASuccessor() {
|
||||
result = decl.getSpecifier(i+1).getFirstControlFlowNode()
|
||||
or
|
||||
not exists(decl.getSpecifier(i+1)) and
|
||||
successor(decl, result)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,13 +22,27 @@ abstract class SystemCommandExecution extends DataFlow::Node {
|
||||
}
|
||||
|
||||
/**
|
||||
* A data flow node that performs a file system access.
|
||||
* A data flow node that performs a file system access (read, write, copy, permissions, stats, etc).
|
||||
*/
|
||||
abstract class FileSystemAccess extends DataFlow::Node {
|
||||
|
||||
/** Gets an argument to this file system access that is interpreted as a path. */
|
||||
abstract DataFlow::Node getAPathArgument();
|
||||
|
||||
/** Gets a node that represents file system access data, such as buffer the data is copied to. */
|
||||
abstract DataFlow::Node getDataNode();
|
||||
}
|
||||
|
||||
/**
|
||||
* A data flow node that performs read file system access.
|
||||
*/
|
||||
abstract class FileSystemReadAccess extends FileSystemAccess { }
|
||||
|
||||
/**
|
||||
* A data flow node that performs write file system access.
|
||||
*/
|
||||
abstract class FileSystemWriteAccess extends FileSystemAccess { }
|
||||
|
||||
/**
|
||||
* A data flow node that contains a file name or an array of file names from the local file system.
|
||||
*/
|
||||
|
||||
@@ -27,7 +27,7 @@ class ES2015Module extends Module {
|
||||
|
||||
/** Gets an export declaration in this module. */
|
||||
ExportDeclaration getAnExport() {
|
||||
result.getContainer() = this
|
||||
result.getTopLevel() = this
|
||||
}
|
||||
|
||||
override Module getAnImportedModule() {
|
||||
@@ -55,7 +55,7 @@ class ES2015Module extends Module {
|
||||
/** An import declaration. */
|
||||
class ImportDeclaration extends Stmt, Import, @importdeclaration {
|
||||
override ES2015Module getEnclosingModule() {
|
||||
result = getContainer()
|
||||
result = getTopLevel()
|
||||
}
|
||||
|
||||
override PathExprInModule getImportedPath() {
|
||||
@@ -254,7 +254,7 @@ class BulkReExportDeclaration extends ReExportDeclaration, @exportalldeclaration
|
||||
* but we ignore this subtlety.
|
||||
*/
|
||||
private predicate isShadowedFromBulkExport(BulkReExportDeclaration reExport, string name) {
|
||||
exists (ExportNamedDeclaration other | other.getContainer() = reExport.getEnclosingModule() |
|
||||
exists (ExportNamedDeclaration other | other.getTopLevel() = reExport.getEnclosingModule() |
|
||||
other.getAnExportedDecl().getName() = name
|
||||
or
|
||||
other.getASpecifier().getExportedName() = name)
|
||||
|
||||
68
javascript/ql/src/semmle/javascript/EmailClients.qll
Normal file
68
javascript/ql/src/semmle/javascript/EmailClients.qll
Normal 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")
|
||||
}
|
||||
}
|
||||
@@ -1008,6 +1008,25 @@ class BinaryExpr extends @binaryexpr, Expr {
|
||||
override ControlFlowNode getFirstControlFlowNode() {
|
||||
result = getLeftOperand().getFirstControlFlowNode()
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number of whitespace characters around the operator of this expression.
|
||||
*
|
||||
* This predicate is only defined if both operands are on the same line, and if the
|
||||
* amount of whitespace before and after the operator are the same.
|
||||
*/
|
||||
int getWhitespaceAroundOperator() {
|
||||
exists (Token lastLeft, Token operator, Token firstRight, int l, int c1, int c2, int c3, int c4 |
|
||||
lastLeft = getLeftOperand().getLastToken() and
|
||||
operator = lastLeft.getNextToken() and
|
||||
firstRight = operator.getNextToken() and
|
||||
lastLeft.getLocation().hasLocationInfo(_, _, _, l, c1) and
|
||||
operator.getLocation().hasLocationInfo(_, l, c2, l, c3) and
|
||||
firstRight.getLocation().hasLocationInfo(_, l, c4, _, _) and
|
||||
result = c2-c1-1 and
|
||||
result = c4-c3-1
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -283,6 +283,18 @@ abstract class AdditionalSink extends DataFlow::Node {
|
||||
abstract predicate isSinkFor(Configuration cfg);
|
||||
}
|
||||
|
||||
/**
|
||||
* An invocation that is modeled as a partial function application.
|
||||
*
|
||||
* This contributes additional argument-passing flow edges that should be added to all data flow configurations.
|
||||
*/
|
||||
cached abstract class AdditionalPartialInvokeNode extends DataFlow::InvokeNode {
|
||||
/**
|
||||
* Holds if `argument` is passed as argument `index` to the function in `callback`.
|
||||
*/
|
||||
cached abstract predicate isPartialArgument(DataFlow::Node callback, DataFlow::Node argument, int index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Additional flow step to model flow from import specifiers into the SSA variable
|
||||
* corresponding to the imported variable.
|
||||
@@ -299,6 +311,37 @@ private class FlowStepThroughImport extends AdditionalFlowStep, DataFlow::ValueN
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A partial call through the built-in `Function.prototype.bind`.
|
||||
*/
|
||||
private class BindPartialCall extends AdditionalPartialInvokeNode, DataFlow::MethodCallNode {
|
||||
BindPartialCall() {
|
||||
getMethodName() = "bind"
|
||||
}
|
||||
|
||||
override predicate isPartialArgument(DataFlow::Node callback, DataFlow::Node argument, int index) {
|
||||
callback = getReceiver() and
|
||||
argument = getArgument(index + 1)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A partial call through `_.partial` or a function with a similar interface.
|
||||
*/
|
||||
private class LibraryPartialCall extends AdditionalPartialInvokeNode {
|
||||
LibraryPartialCall() {
|
||||
this = LodashUnderscore::member("partial").getACall() or
|
||||
this = DataFlow::moduleMember("ramda", "partial").getACall()
|
||||
}
|
||||
|
||||
override predicate isPartialArgument(DataFlow::Node callback, DataFlow::Node argument, int index) {
|
||||
callback = getArgument(0) and
|
||||
exists (DataFlow::ArrayLiteralNode array |
|
||||
array.flowsTo(getArgument(1)) and
|
||||
argument = array.getElement(index))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if there is a flow step from `pred` to `succ` described by `summary`
|
||||
* under configuration `cfg`.
|
||||
@@ -395,16 +438,18 @@ private predicate isRelevant(DataFlow::Node nd, DataFlow::Configuration cfg) {
|
||||
* either `pred` is an argument of `f` and `succ` the corresponding parameter, or
|
||||
* `pred` is a variable definition whose value is captured by `f` at `succ`.
|
||||
*/
|
||||
pragma[noopt]
|
||||
private predicate callInputStep(Function f, DataFlow::Node invk,
|
||||
DataFlow::Node pred, DataFlow::Node succ,
|
||||
DataFlow::Configuration cfg) {
|
||||
isRelevant(pred, cfg) and
|
||||
(
|
||||
isRelevant(pred, cfg) and
|
||||
exists (Parameter parm |
|
||||
argumentPassing(invk, pred, f, parm) and
|
||||
succ = DataFlow::parameterNode(parm)
|
||||
)
|
||||
or
|
||||
isRelevant(pred, cfg) and
|
||||
exists (SsaDefinition prevDef, SsaDefinition def |
|
||||
pred = DataFlow::ssaDefinitionNode(prevDef) and
|
||||
calls(invk, f) and captures(f, prevDef, def) and
|
||||
@@ -445,6 +490,7 @@ private predicate flowThroughCall(DataFlow::Node input, DataFlow::Node invk,
|
||||
DataFlow::Configuration cfg, boolean valuePreserving) {
|
||||
exists (Function f, DataFlow::ValueNode ret |
|
||||
ret.asExpr() = f.getAReturnedExpr() and
|
||||
calls(invk, f) and // Do not consider partial calls
|
||||
reachableFromInput(f, invk, input, ret, cfg, PathSummary::level(valuePreserving))
|
||||
)
|
||||
}
|
||||
|
||||
@@ -307,7 +307,7 @@ class ArrayLiteralNode extends DataFlow::ValueNode, DataFlow::DefaultSourceNode
|
||||
/** A data flow node corresponding to a `new Array()` or `Array()` invocation. */
|
||||
class ArrayConstructorInvokeNode extends DataFlow::InvokeNode {
|
||||
ArrayConstructorInvokeNode() {
|
||||
getCallee() = DataFlow::globalVarRef("Array")
|
||||
getCalleeNode() = DataFlow::globalVarRef("Array")
|
||||
}
|
||||
|
||||
/** Gets the `i`th initial element of this array, if one is provided. */
|
||||
|
||||
@@ -27,6 +27,21 @@ predicate calls(DataFlow::InvokeNode invk, Function f) {
|
||||
f = invk.getACallee()
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `invk` may invoke `f` indirectly through the given `callback` argument.
|
||||
*
|
||||
* This only holds for explicitly modeled partial calls.
|
||||
*/
|
||||
private predicate partiallyCalls(DataFlow::AdditionalPartialInvokeNode invk, DataFlow::AnalyzedNode callback, Function f) {
|
||||
invk.isPartialArgument(callback, _, _) and
|
||||
exists (AbstractFunction callee | callee = callback.getAValue() |
|
||||
if callback.getAValue().isIndefinite("global") then
|
||||
(f = callee.getFunction() and f.getFile() = invk.getFile())
|
||||
else
|
||||
f = callee.getFunction()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `f` captures the variable defined by `def` in `cap`.
|
||||
*/
|
||||
@@ -69,6 +84,11 @@ predicate argumentPassing(DataFlow::InvokeNode invk, DataFlow::ValueNode arg, Fu
|
||||
f.getParameter(i) = parm and not parm.isRestParameter() and
|
||||
arg = invk.getArgument(i)
|
||||
)
|
||||
or
|
||||
exists (DataFlow::Node callback, int i |
|
||||
invk.(DataFlow::AdditionalPartialInvokeNode).isPartialArgument(callback, arg, i) and
|
||||
partiallyCalls(invk, callback, f) and
|
||||
parm = f.getParameter(i) and not parm.isRestParameter())
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -301,7 +301,7 @@ private class AnalyzedExportAssign extends AnalyzedPropertyWrite, DataFlow::Valu
|
||||
}
|
||||
|
||||
override predicate writes(AbstractValue baseVal, string propName, DataFlow::AnalyzedNode source) {
|
||||
baseVal = TAbstractModuleObject(exportAssign.getContainer()) and
|
||||
baseVal = TAbstractModuleObject(exportAssign.getTopLevel()) and
|
||||
propName = "exports" and
|
||||
source = this
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -800,6 +832,10 @@ module Express {
|
||||
asExpr().(MethodCallExpr).calls(any(ResponseExpr res), name))
|
||||
}
|
||||
|
||||
override DataFlow::Node getDataNode() {
|
||||
result = DataFlow::valueNode(astNode)
|
||||
}
|
||||
|
||||
override DataFlow::Node getAPathArgument() {
|
||||
result = DataFlow::valueNode(astNode.getArgument(0))
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -132,6 +132,11 @@ module HTTP {
|
||||
result = "http" or result = "https"
|
||||
}
|
||||
|
||||
/**
|
||||
* An expression whose value is sent as (part of) the body of an HTTP request (POST, PUT).
|
||||
*/
|
||||
abstract class RequestBody extends DataFlow::Node {}
|
||||
|
||||
/**
|
||||
* An expression whose value is sent as (part of) the body of an HTTP response.
|
||||
*/
|
||||
@@ -354,9 +359,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 +405,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.
|
||||
*
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
@@ -336,6 +372,23 @@ module NodeJSLib {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if the `i`th parameter of method `methodName` of the Node.js
|
||||
* `fs` module might represent a data parameter or buffer or a callback
|
||||
* that receives the data.
|
||||
*
|
||||
* We determine this by looking for an externs declaration for
|
||||
* `fs.methodName` where the `i`th parameter's name is `data` or
|
||||
* `buffer` or a 'callback'.
|
||||
*/
|
||||
private predicate fsDataParam(string methodName, int i, string n) {
|
||||
exists (ExternalMemberDecl decl, Function f, JSDocParamTag p |
|
||||
decl.hasQualifiedName("fs", methodName) and f = decl.getInit() and
|
||||
p.getDocumentedParameter() = f.getParameter(i).getAVariable() and
|
||||
n = p.getName().toLowerCase() |
|
||||
n = "data" or n = "buffer" or n = "callback"
|
||||
)
|
||||
}
|
||||
/**
|
||||
* A member `member` from module `fs` or its drop-in replacements `graceful-fs` or `fs-extra`.
|
||||
*/
|
||||
@@ -348,21 +401,161 @@ module NodeJSLib {
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A call to a method from module `fs`, `graceful-fs` or `fs-extra`.
|
||||
*/
|
||||
private class NodeJSFileSystemAccess extends FileSystemAccess, DataFlow::CallNode {
|
||||
private class NodeJSFileSystemAccessCall extends FileSystemAccess, DataFlow::CallNode {
|
||||
string methodName;
|
||||
|
||||
NodeJSFileSystemAccess() {
|
||||
NodeJSFileSystemAccessCall() {
|
||||
this = fsModuleMember(methodName).getACall()
|
||||
}
|
||||
|
||||
string getMethodName() {
|
||||
result = methodName
|
||||
}
|
||||
|
||||
override DataFlow::Node getDataNode() {
|
||||
(
|
||||
methodName = "readFileSync" and
|
||||
result = this
|
||||
)
|
||||
or
|
||||
exists (int i, string paramName | fsDataParam(methodName, i, paramName) |
|
||||
(
|
||||
paramName = "callback" and
|
||||
exists (DataFlow::ParameterNode p, string n |
|
||||
p = getCallback(i).getAParameter() and
|
||||
n = p.getName().toLowerCase() and
|
||||
result = p |
|
||||
n = "data" or n = "buffer" or n = "string"
|
||||
)
|
||||
)
|
||||
or
|
||||
result = getArgument(i))
|
||||
}
|
||||
|
||||
override DataFlow::Node getAPathArgument() {
|
||||
exists (int i | fsFileParam(methodName, i) |
|
||||
result = getArgument(i)
|
||||
exists (int i | fsFileParam(methodName, i) |
|
||||
result = getArgument(i))
|
||||
}
|
||||
}
|
||||
|
||||
/** Only NodeJSSystemFileAccessCalls that write data to 'fs' */
|
||||
private class NodeJSFileSystemAccessWriteCall extends FileSystemWriteAccess, NodeJSFileSystemAccessCall {
|
||||
NodeJSFileSystemAccessWriteCall () {
|
||||
this.getMethodName() = "appendFile" or
|
||||
this.getMethodName() = "appendFileSync" or
|
||||
this.getMethodName() = "write" or
|
||||
this.getMethodName() = "writeFile" or
|
||||
this.getMethodName() = "writeFileSync" or
|
||||
this.getMethodName() = "writeSync"
|
||||
}
|
||||
}
|
||||
|
||||
/** Only NodeJSSystemFileAccessCalls that read data from 'fs' */
|
||||
private class NodeJSFileSystemAccessReadCall extends FileSystemReadAccess, NodeJSFileSystemAccessCall {
|
||||
NodeJSFileSystemAccessReadCall () {
|
||||
this.getMethodName() = "read" or
|
||||
this.getMethodName() = "readSync" or
|
||||
this.getMethodName() = "readFile" or
|
||||
this.getMethodName() = "readFileSync"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A call to write corresponds to a pattern where file stream is open first with 'createWriteStream', followed by 'write' or 'end' call
|
||||
*/
|
||||
private class NodeJSFileSystemWrite extends FileSystemWriteAccess, DataFlow::CallNode {
|
||||
|
||||
NodeJSFileSystemAccessCall init;
|
||||
|
||||
NodeJSFileSystemWrite() {
|
||||
exists (NodeJSFileSystemAccessCall n |
|
||||
n.getCalleeName() = "createWriteStream" and init = n |
|
||||
this = n.getAMemberCall("write") or
|
||||
this = n.getAMemberCall("end")
|
||||
)
|
||||
}
|
||||
|
||||
override DataFlow::Node getDataNode() {
|
||||
result = this.getArgument(0)
|
||||
}
|
||||
|
||||
override DataFlow::Node getAPathArgument() {
|
||||
result = init.getAPathArgument()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A call to read corresponds to a pattern where file stream is open first with createReadStream, followed by 'read' call
|
||||
*/
|
||||
private class NodeJSFileSystemRead extends FileSystemReadAccess, DataFlow::CallNode {
|
||||
|
||||
NodeJSFileSystemAccessCall init;
|
||||
|
||||
NodeJSFileSystemRead() {
|
||||
exists (NodeJSFileSystemAccessCall n |
|
||||
n.getCalleeName() = "createReadStream" and init = n |
|
||||
this = n.getAMemberCall("read")
|
||||
)
|
||||
}
|
||||
|
||||
override DataFlow::Node getDataNode() {
|
||||
result = this
|
||||
}
|
||||
|
||||
override DataFlow::Node getAPathArgument() {
|
||||
result = init.getAPathArgument()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A call to read corresponds to a pattern where file stream is open first with createReadStream, followed by 'pipe' call
|
||||
*/
|
||||
private class NodeJSFileSystemPipe extends FileSystemReadAccess, DataFlow::CallNode {
|
||||
|
||||
NodeJSFileSystemAccessCall init;
|
||||
|
||||
NodeJSFileSystemPipe() {
|
||||
exists (NodeJSFileSystemAccessCall n |
|
||||
n.getCalleeName() = "createReadStream" and init = n |
|
||||
this = n.getAMemberCall("pipe")
|
||||
)
|
||||
}
|
||||
|
||||
override DataFlow::Node getDataNode() {
|
||||
result = this.getArgument(0)
|
||||
}
|
||||
|
||||
override DataFlow::Node getAPathArgument() {
|
||||
result = init.getAPathArgument()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An 'on' event where data comes in as a parameter (usage: readstream.on('data', chunk))
|
||||
*/
|
||||
private class NodeJSFileSystemReadDataEvent extends FileSystemReadAccess, DataFlow::CallNode {
|
||||
|
||||
NodeJSFileSystemAccessCall init;
|
||||
|
||||
NodeJSFileSystemReadDataEvent() {
|
||||
exists(NodeJSFileSystemAccessCall n |
|
||||
n.getCalleeName() = "createReadStream" and init = n |
|
||||
this = n.getAMethodCall("on") and
|
||||
this.getArgument(0).mayHaveStringValue("data")
|
||||
)
|
||||
}
|
||||
|
||||
override DataFlow::Node getDataNode() {
|
||||
result = this.getCallback(1).getParameter(0)
|
||||
}
|
||||
|
||||
override DataFlow::Node getAPathArgument() {
|
||||
result = init.getAPathArgument()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -380,7 +573,7 @@ module NodeJSLib {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A call to a method from module `child_process`.
|
||||
*/
|
||||
@@ -476,21 +669,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 +736,7 @@ module NodeJSLib {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A model of a URL request in the Node.js `http` library.
|
||||
*/
|
||||
@@ -569,7 +762,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 +772,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 +789,31 @@ module NodeJSLib {
|
||||
this = mcn.getCallback(1).getParameter(0)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
override string getSourceType() {
|
||||
result = "http.request data parameter"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* An argument to client request.write () method, can be used to write body to a HTTP or HTTPS POST/PUT request,
|
||||
* or request option (like headers, cookies, even url)
|
||||
*/
|
||||
class HttpRequestWriteArgument extends HTTP::RequestBody, DataFlow::Node {
|
||||
HttpRequestWriteArgument () {
|
||||
exists(CustomClientRequest req |
|
||||
this = req.getAMethodCall("write").getArgument(0) or
|
||||
this = req.getArgument(0))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 +821,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 +836,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 +847,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 +864,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 +878,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 +888,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 +904,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 +914,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 +929,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 +946,7 @@ module NodeJSLib {
|
||||
handler.getAHandledEvent() = "error"
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
override string getSourceType() {
|
||||
result = "NodeJSClientRequest error event"
|
||||
}
|
||||
|
||||
@@ -44,5 +44,13 @@ module Request {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// using 'request' library to make http 'POST' and 'PUT' requests with message body.
|
||||
private class RequestPostBody extends HTTP::RequestBody {
|
||||
RequestPostBody () {
|
||||
this = DataFlow::moduleMember("request", "post").getACall().getArgument(1) or
|
||||
this = DataFlow::moduleImport("request").getAnInvocation().getArgument(0)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Provides Taint tracking configuration for reasoning about file access taint flow to http post body
|
||||
*/
|
||||
import javascript
|
||||
import semmle.javascript.frameworks.HTTP
|
||||
|
||||
module FileAccessToHttpDataFlow {
|
||||
/**
|
||||
* A data flow source for reasoning about file access to http post body flow vulnerabilities.
|
||||
*/
|
||||
abstract class Source extends DataFlow::Node { }
|
||||
|
||||
/**
|
||||
* A data flow sink for reasoning about file access to http post body flow vulnerabilities.
|
||||
*/
|
||||
abstract class Sink extends DataFlow::Node { }
|
||||
|
||||
/**
|
||||
* A sanitizer for reasoning about file access to http post body flow vulnerabilities.
|
||||
*/
|
||||
abstract class Sanitizer extends DataFlow::Node { }
|
||||
|
||||
/**
|
||||
* A taint-tracking configuration for reasoning about file access to http post body flow vulnerabilities.
|
||||
*/
|
||||
class Configuration extends TaintTracking::Configuration {
|
||||
Configuration() { this = "FileAccessToHttpDataFlow" }
|
||||
|
||||
override predicate isSource(DataFlow::Node source) {
|
||||
source instanceof Source
|
||||
}
|
||||
|
||||
override predicate isSink(DataFlow::Node sink) {
|
||||
sink instanceof Sink
|
||||
}
|
||||
|
||||
override predicate isSanitizer(DataFlow::Node node) {
|
||||
super.isSanitizer(node) or
|
||||
node instanceof Sanitizer
|
||||
}
|
||||
|
||||
/** additional taint step that taints an object wrapping a source */
|
||||
override predicate isAdditionalTaintStep(DataFlow::Node pred, DataFlow::Node succ) {
|
||||
(
|
||||
pred = DataFlow::valueNode(_) or
|
||||
pred = DataFlow::parameterNode(_) or
|
||||
pred instanceof DataFlow::PropRead
|
||||
) and
|
||||
exists (DataFlow::PropWrite pwr |
|
||||
succ = pwr.getBase() and
|
||||
pred = pwr.getRhs()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** A source is a file access parameter, as in readFromFile(buffer). */
|
||||
private class FileAccessArgumentAsSource extends Source {
|
||||
FileAccessArgumentAsSource() {
|
||||
exists(FileSystemReadAccess src |
|
||||
this = src.getDataNode().getALocalSource()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Sink is any parameter or argument that evaluates to a parameter ot a function or call that sets Http Body on a request */
|
||||
private class HttpRequestBodyAsSink extends Sink {
|
||||
HttpRequestBodyAsSink () {
|
||||
this instanceof HTTP::RequestBody
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Provides taint tracking configuration for reasoning about files created from untrusted http downloads.
|
||||
*/
|
||||
import javascript
|
||||
import semmle.javascript.security.dataflow.RemoteFlowSources
|
||||
|
||||
module HttpToFileAccessFlow {
|
||||
/**
|
||||
* A data flow source from untrusted http request to file access taint tracking configuration.
|
||||
*/
|
||||
abstract class Source extends DataFlow::Node { }
|
||||
|
||||
/**
|
||||
* A data flow sink for untrusted http request to file access taint tracking configuration.
|
||||
*/
|
||||
abstract class Sink extends DataFlow::Node { }
|
||||
|
||||
/**
|
||||
* A sanitizer for untrusted http request to file access taint tracking configuration.
|
||||
*/
|
||||
abstract class Sanitizer extends DataFlow::Node { }
|
||||
|
||||
/**
|
||||
* A taint-tracking configuration for reasoning about file access from untrusted http response body.
|
||||
*/
|
||||
class Configuration extends TaintTracking::Configuration {
|
||||
Configuration() { this = "HttpToFileAccessFlow" }
|
||||
|
||||
override predicate isSource(DataFlow::Node source) {
|
||||
source instanceof Source
|
||||
}
|
||||
|
||||
override predicate isSink(DataFlow::Node sink) {
|
||||
sink instanceof Sink
|
||||
}
|
||||
|
||||
override predicate isSanitizer(DataFlow::Node node) {
|
||||
super.isSanitizer(node) or
|
||||
node instanceof Sanitizer
|
||||
}
|
||||
}
|
||||
|
||||
/** A source of remote data, considered as a flow source for untrusted http data to file system access. */
|
||||
class RemoteFlowSourceAsSource extends Source {
|
||||
RemoteFlowSourceAsSource() { this instanceof RemoteFlowSource }
|
||||
}
|
||||
|
||||
/** A sink that represents file access method (write, append) argument */
|
||||
class FileAccessAsSink extends Sink {
|
||||
FileAccessAsSink () {
|
||||
exists(FileSystemWriteAccess src |
|
||||
this = src.getDataNode()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,10 +21,10 @@ module ServerSideUrlRedirect {
|
||||
* Holds if this sink may redirect to a non-local URL.
|
||||
*/
|
||||
predicate maybeNonLocal() {
|
||||
exists (Expr prefix | prefix = getAPrefix(this) |
|
||||
not exists(prefix.getStringValue())
|
||||
exists (DataFlow::Node prefix | prefix = getAPrefix(this) |
|
||||
not exists(prefix.asExpr().getStringValue())
|
||||
or
|
||||
exists (string prefixVal | prefixVal = prefix.getStringValue() |
|
||||
exists (string prefixVal | prefixVal = prefix.asExpr().getStringValue() |
|
||||
// local URLs (i.e., URLs that start with `/` not followed by `\` or `/`,
|
||||
// or that start with `~/`) are unproblematic
|
||||
not prefixVal.regexpMatch("/[^\\\\/].*|~/.*") and
|
||||
@@ -47,12 +47,12 @@ module ServerSideUrlRedirect {
|
||||
/**
|
||||
* Gets an expression that may end up being a prefix of the string concatenation `nd`.
|
||||
*/
|
||||
private Expr getAPrefix(Sink sink) {
|
||||
private DataFlow::Node getAPrefix(Sink sink) {
|
||||
exists (DataFlow::Node prefix |
|
||||
prefix = prefixCandidate(sink) and
|
||||
not exists(StringConcatenation::getFirstOperand(prefix)) and
|
||||
not exists(prefix.getAPredecessor()) and
|
||||
result = prefix.asExpr()
|
||||
result = prefix
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user