Merge remote-tracking branch 'upstream/master' into mergeback-2018-10-08

This commit is contained in:
Tom Hvitved
2018-10-08 11:48:56 +02:00
151 changed files with 5884 additions and 546 deletions

View File

@@ -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())
)
}
/**

View File

@@ -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>

View File

@@ -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>&amp;</code>, so the test
is equivalent to <code>x &amp; (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>&amp;</code> or removing whitespace
around <code>==</code> to make it visually apparent that it binds less tightly:
<code>x &amp; y==0</code>.
</p>
<p>
Probably the best approach in this case, though, would be to use the <code>&amp;&amp;</code>
operator instead to clarify the intended interpretation: <code>x &amp;&amp; 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>

View 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."

View File

@@ -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."

View File

@@ -0,0 +1,3 @@
if (x & y == 0) {
// ...
}

View File

@@ -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);

View File

@@ -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);

View 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"

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

@@ -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"

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

@@ -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)
}
}

View File

@@ -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.
*/

View File

@@ -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)

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

@@ -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
)
}
}
/**

View File

@@ -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))
)
}

View File

@@ -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. */

View File

@@ -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())
}

View File

@@ -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
}

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()
)
)
}
}
@@ -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))
}

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();
}
@@ -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.
*

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;
@@ -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"
}

View File

@@ -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)
}
}
}

View File

@@ -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
}
}
}

View File

@@ -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()
)
}
}
}

View File

@@ -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
)
}