Files
codeql/javascript/ql/lib/semmle/javascript/security/dataflow/TaintedPathCustomizations.qll
2022-03-23 13:35:49 +00:00

882 lines
27 KiB
Plaintext

/**
* Provides default sources, sinks and sanitizers for reasoning about
* tainted-path vulnerabilities, as well as extension points for
* adding your own.
*/
import javascript
module TaintedPath {
/**
* A data flow source for tainted-path vulnerabilities.
*/
abstract class Source extends DataFlow::Node {
/** Gets a flow label denoting the type of value for which this is a source. */
DataFlow::FlowLabel getAFlowLabel() { result instanceof Label::PosixPath }
}
/**
* A data flow sink for tainted-path vulnerabilities.
*/
abstract class Sink extends DataFlow::Node {
/** Gets a flow label denoting the type of value for which this is a sink. */
DataFlow::FlowLabel getAFlowLabel() { result instanceof Label::PosixPath }
}
/**
* A sanitizer for tainted-path vulnerabilities.
*/
abstract class Sanitizer extends DataFlow::Node { }
/**
* A barrier guard for tainted-path vulnerabilities.
*/
abstract class BarrierGuardNode extends DataFlow::LabeledBarrierGuardNode { }
module Label {
/**
* A string indicating if a path is normalized, that is, whether internal `../` components
* have been removed.
*/
class Normalization extends string {
Normalization() { this = "normalized" or this = "raw" }
}
/**
* A string indicating if a path is relative or absolute.
*/
class Relativeness extends string {
Relativeness() { this = "relative" or this = "absolute" }
}
/**
* A flow label representing a Posix path.
*
* There are currently four flow labels, representing the different combinations of
* normalization and absoluteness.
*/
class PosixPath extends DataFlow::FlowLabel {
Normalization normalization;
Relativeness relativeness;
PosixPath() { this = normalization + "-" + relativeness + "-posix-path" }
/** Gets a string indicating whether this path is normalized. */
Normalization getNormalization() { result = normalization }
/** Gets a string indicating whether this path is relative. */
Relativeness getRelativeness() { result = relativeness }
/** Holds if this path is normalized. */
predicate isNormalized() { normalization = "normalized" }
/** Holds if this path is not normalized. */
predicate isNonNormalized() { normalization = "raw" }
/** Holds if this path is relative. */
predicate isRelative() { relativeness = "relative" }
/** Holds if this path is relative. */
predicate isAbsolute() { relativeness = "absolute" }
/** Gets the path label with normalized flag set to true. */
PosixPath toNormalized() {
result.isNormalized() and
result.getRelativeness() = this.getRelativeness()
}
/** Gets the path label with normalized flag set to true. */
PosixPath toNonNormalized() {
result.isNonNormalized() and
result.getRelativeness() = this.getRelativeness()
}
/** Gets the path label with absolute flag set to true. */
PosixPath toAbsolute() {
result.isAbsolute() and
result.getNormalization() = this.getNormalization()
}
/** Gets the path label with absolute flag set to true. */
PosixPath toRelative() {
result.isRelative() and
result.getNormalization() = this.getNormalization()
}
/** Holds if this path may contain `../` components. */
predicate canContainDotDotSlash() {
// Absolute normalized path is the only combination that cannot contain `../`.
not (this.isNormalized() and this.isAbsolute())
}
}
/**
* A flow label representing an array of path elements that may include "..".
*/
class SplitPath extends DataFlow::FlowLabel {
SplitPath() { this = "splitPath" }
}
}
/**
* Holds if `s` is a relative path.
*/
bindingset[s]
predicate isRelative(string s) { not s.charAt(0) = "/" }
/**
* A call that normalizes a path.
*/
class NormalizingPathCall extends DataFlow::CallNode {
DataFlow::Node input;
DataFlow::Node output;
NormalizingPathCall() {
this = NodeJSLib::Path::moduleMember("normalize").getACall() and
input = this.getArgument(0) and
output = this
}
/**
* Gets the input path to be normalized.
*/
DataFlow::Node getInput() { result = input }
/**
* Gets the normalized path.
*/
DataFlow::Node getOutput() { result = output }
}
/**
* A call that converts a path to an absolute normalized path.
*/
class ResolvingPathCall extends DataFlow::CallNode {
DataFlow::Node input;
DataFlow::Node output;
ResolvingPathCall() {
this = NodeJSLib::Path::moduleMember("resolve").getACall() and
input = this.getAnArgument() and
output = this
or
this = NodeJSLib::FS::moduleMember("realpathSync").getACall() and
input = this.getArgument(0) and
output = this
or
this = NodeJSLib::FS::moduleMember("realpath").getACall() and
input = this.getArgument(0) and
output = this.getCallback(1).getParameter(1)
}
/**
* Gets the input path to be normalized.
*/
DataFlow::Node getInput() { result = input }
/**
* Gets the normalized path.
*/
DataFlow::Node getOutput() { result = output }
}
/**
* A call that normalizes a path and converts it to a relative path.
*/
class NormalizingRelativePathCall extends DataFlow::CallNode {
DataFlow::Node input;
DataFlow::Node output;
NormalizingRelativePathCall() {
this = NodeJSLib::Path::moduleMember("relative").getACall() and
input = this.getAnArgument() and
output = this
}
/**
* Gets the input path to be normalized.
*/
DataFlow::Node getInput() { result = input }
/**
* Gets the normalized path.
*/
DataFlow::Node getOutput() { result = output }
}
/**
* A call that preserves taint without changing the flow label.
*/
class PreservingPathCall extends DataFlow::CallNode {
DataFlow::Node input;
DataFlow::Node output;
PreservingPathCall() {
this =
NodeJSLib::Path::moduleMember(["dirname", "toNamespacedPath", "parse", "format"]).getACall() and
input = this.getAnArgument() and
output = this
or
// non-global replace or replace of something other than /\.\./g, /[/]/g, or /[\.]/g.
this.getCalleeName() = "replace" and
input = getReceiver() and
output = this and
not exists(RegExpLiteral literal, RegExpTerm term |
getArgument(0).getALocalSource().asExpr() = literal and
literal.isGlobal() and
literal.getRoot() = term
|
term.getAMatchedString() = "/" or
term.getAMatchedString() = "." or
term.getAMatchedString() = ".."
) and
not this instanceof DotDotSlashPrefixRemovingReplace
}
/**
* Gets the input path to be normalized.
*/
DataFlow::Node getInput() { result = input }
/**
* Gets the normalized path.
*/
DataFlow::Node getOutput() { result = output }
}
/**
* A call that removes all instances of "../" in the prefix of the string.
*/
class DotDotSlashPrefixRemovingReplace extends DataFlow::CallNode {
DataFlow::Node input;
DataFlow::Node output;
DotDotSlashPrefixRemovingReplace() {
this.getCalleeName() = "replace" and
input = getReceiver() and
output = this and
exists(RegExpLiteral literal, RegExpTerm term |
getArgument(0).getALocalSource().asExpr() = literal and
(term instanceof RegExpStar or term instanceof RegExpPlus) and
term.getChild(0) = getADotDotSlashMatcher()
|
literal.getRoot() = term
or
exists(RegExpSequence seq | seq.getNumChild() = 2 and literal.getRoot() = seq |
seq.getChild(0) instanceof RegExpCaret and
seq.getChild(1) = term
)
)
}
/**
* Gets the input path to be sanitized.
*/
DataFlow::Node getInput() { result = input }
/**
* Gets the path where prefix "../" has been removed.
*/
DataFlow::Node getOutput() { result = output }
}
/**
* Gets a RegExpTerm that matches a variation of "../".
*/
private RegExpTerm getADotDotSlashMatcher() {
result.getAMatchedString() = "../"
or
exists(RegExpSequence seq | seq = result |
seq.getChild(0).getConstantValue() = "." and
seq.getChild(1).getConstantValue() = "." and
seq.getChild(2).getAMatchedString() = "/"
)
or
exists(RegExpGroup group | result = group | group.getChild(0) = getADotDotSlashMatcher())
}
/**
* A call that removes all "." or ".." from a path, without also removing all forward slashes.
*/
class DotRemovingReplaceCall extends DataFlow::CallNode {
DataFlow::Node input;
DataFlow::Node output;
DotRemovingReplaceCall() {
this.getCalleeName() = "replace" and
input = getReceiver() and
output = this and
exists(RegExpLiteral literal, RegExpTerm term |
getArgument(0).getALocalSource().asExpr() = literal and
literal.isGlobal() and
literal.getRoot() = term and
not term.getAMatchedString() = "/"
|
term.getAMatchedString() = "." or
term.getAMatchedString() = ".."
)
}
/**
* Gets the input path to be normalized.
*/
DataFlow::Node getInput() { result = input }
/**
* Gets the normalized path.
*/
DataFlow::Node getOutput() { result = output }
}
/**
* Holds if `node` is a prefix of the string `../`.
*/
private predicate isDotDotSlashPrefix(DataFlow::Node node) {
node.getStringValue() + any(string s) = "../"
or
// ".." + path.sep
exists(StringOps::Concatenation conc | node = conc |
conc.getOperand(0).getStringValue() = ".." and
conc.getOperand(1).getALocalSource() = NodeJSLib::Path::moduleMember("sep") and
conc.getNumOperand() = 2
)
}
/**
* A check of form `x.startsWith("../")` or similar.
*
* This is relevant for paths that are known to be normalized.
*/
class StartsWithDotDotSanitizer extends BarrierGuardNode {
StringOps::StartsWith startsWith;
StartsWithDotDotSanitizer() {
this = startsWith and
isDotDotSlashPrefix(startsWith.getSubstring())
}
override predicate blocks(boolean outcome, Expr e, DataFlow::FlowLabel label) {
// Sanitize in the false case for:
// .startsWith(".")
// .startsWith("..")
// .startsWith("../")
outcome = startsWith.getPolarity().booleanNot() and
e = startsWith.getBaseString().asExpr() and
exists(Label::PosixPath posixPath | posixPath = label |
posixPath.isNormalized() and
posixPath.isRelative()
)
}
}
/**
* A check of the form `whitelist.includes(x)` or equivalent, which sanitizes `x` in its "then" branch.
*/
class MembershipTestBarrierGuard extends BarrierGuardNode {
MembershipCandidate candidate;
MembershipTestBarrierGuard() { this = candidate.getTest() }
override predicate blocks(boolean outcome, Expr e) {
candidate = e.flow() and
candidate.getTestPolarity() = outcome
}
}
/**
* A check of form `x.startsWith(dir)` that sanitizes normalized absolute paths, since it is then
* known to be in a subdirectory of `dir`.
*/
class StartsWithDirSanitizer extends BarrierGuardNode {
StringOps::StartsWith startsWith;
StartsWithDirSanitizer() {
this = startsWith and
not isDotDotSlashPrefix(startsWith.getSubstring()) and
// do not confuse this with a simple isAbsolute() check
not startsWith.getSubstring().getStringValue() = "/"
}
override predicate blocks(boolean outcome, Expr e, DataFlow::FlowLabel label) {
outcome = startsWith.getPolarity() and
e = startsWith.getBaseString().asExpr() and
exists(Label::PosixPath posixPath | posixPath = label |
posixPath.isAbsolute() and
posixPath.isNormalized()
)
}
}
/**
* A call to `path.isAbsolute` as a sanitizer for relative paths in true branch,
* and a sanitizer for absolute paths in the false branch.
*/
class IsAbsoluteSanitizer extends BarrierGuardNode {
DataFlow::Node operand;
boolean polarity;
boolean negatable;
IsAbsoluteSanitizer() {
exists(DataFlow::CallNode call | this = call |
call = NodeJSLib::Path::moduleMember("isAbsolute").getACall() and
operand = call.getArgument(0) and
polarity = true and
negatable = true
)
or
exists(StringOps::StartsWith startsWith, string substring | this = startsWith |
startsWith.getSubstring().getStringValue() = "/" + substring and
operand = startsWith.getBaseString() and
polarity = startsWith.getPolarity() and
if substring = "" then negatable = true else negatable = false
) // !x.startsWith("/home") does not guarantee that x is not absolute
}
override predicate blocks(boolean outcome, Expr e, DataFlow::FlowLabel label) {
e = operand.asExpr() and
exists(Label::PosixPath posixPath | posixPath = label |
outcome = polarity and posixPath.isRelative()
or
negatable = true and
outcome = polarity.booleanNot() and
posixPath.isAbsolute()
)
}
}
/**
* An expression of form `x.includes("..")` or similar.
*/
class ContainsDotDotSanitizer extends BarrierGuardNode instanceof StringOps::Includes {
ContainsDotDotSanitizer() { isDotDotSlashPrefix(super.getSubstring()) }
override predicate blocks(boolean outcome, Expr e, DataFlow::FlowLabel label) {
e = super.getBaseString().asExpr() and
outcome = super.getPolarity().booleanNot() and
label.(Label::PosixPath).canContainDotDotSlash() // can still be bypassed by normalized absolute path
}
}
/**
* An expression of form `x.matches(/\.\./)` or similar.
*/
class ContainsDotDotRegExpSanitizer extends BarrierGuardNode instanceof StringOps::RegExpTest {
ContainsDotDotRegExpSanitizer() { super.getRegExp().getAMatchedString() = [".", "..", "../"] }
override predicate blocks(boolean outcome, Expr e, DataFlow::FlowLabel label) {
e = super.getStringOperand().asExpr() and
outcome = super.getPolarity().booleanNot() and
label.(Label::PosixPath).canContainDotDotSlash() // can still be bypassed by normalized absolute path
}
}
/**
* A sanitizer that recognizes the following pattern:
* ```
* var relative = path.relative(webroot, pathname);
* if(relative.startsWith(".." + path.sep) || relative == "..") {
* // pathname is unsafe
* } else {
* // pathname is safe
* }
* ```
*
* or
* ```
* var relative = path.resolve(pathname); // or path.normalize
* if(relative.startsWith(webroot) {
* // pathname is safe
* } else {
* // pathname is unsafe
* }
* ```
*/
class RelativePathStartsWithSanitizer extends BarrierGuardNode {
StringOps::StartsWith startsWith;
DataFlow::CallNode pathCall;
string member;
RelativePathStartsWithSanitizer() {
(member = "relative" or member = "resolve" or member = "normalize") and
this = startsWith and
pathCall = NodeJSLib::Path::moduleMember(member).getACall() and
(
startsWith.getBaseString().getALocalSource() = pathCall
or
startsWith
.getBaseString()
.getALocalSource()
.(NormalizingPathCall)
.getInput()
.getALocalSource() = pathCall
) and
(not member = "relative" or isDotDotSlashPrefix(startsWith.getSubstring()))
}
override predicate blocks(boolean outcome, Expr e) {
member = "relative" and
e = this.maybeGetPathSuffix(pathCall.getArgument(1)).asExpr() and
outcome = startsWith.getPolarity().booleanNot()
or
not member = "relative" and
e = this.maybeGetPathSuffix(pathCall.getArgument(0)).asExpr() and
outcome = startsWith.getPolarity()
}
/**
* Gets the last argument to the given `path.join()` call,
* or the node itself if it is not a join call.
* Is used to get the suffix of the path.
*/
bindingset[e]
private DataFlow::Node maybeGetPathSuffix(DataFlow::Node e) {
exists(DataFlow::CallNode call |
call = NodeJSLib::Path::moduleMember("join").getACall() and e = call
|
result = call.getLastArgument()
)
or
result = e
}
}
/**
* A guard node for a variable in a negative condition, such as `x` in `if(!x)`.
*/
private class VarAccessBarrier extends Sanitizer, DataFlow::VarAccessBarrier { }
/**
* An expression of form `isInside(x, y)` or similar, where `isInside` is
* a library check for the relation between `x` and `y`.
*/
class IsInsideCheckSanitizer extends BarrierGuardNode {
DataFlow::Node checked;
boolean onlyNormalizedAbsolutePaths;
IsInsideCheckSanitizer() {
exists(string name, DataFlow::CallNode check |
name = "path-is-inside" and onlyNormalizedAbsolutePaths = true
or
name = "is-path-inside" and onlyNormalizedAbsolutePaths = false
|
check = DataFlow::moduleImport(name).getACall() and
checked = check.getArgument(0) and
check = this
)
}
override predicate blocks(boolean outcome, Expr e, DataFlow::FlowLabel label) {
(
onlyNormalizedAbsolutePaths = true and
label.(Label::PosixPath).isNormalized() and
label.(Label::PosixPath).isAbsolute()
or
onlyNormalizedAbsolutePaths = false
) and
e = checked.asExpr() and
outcome = true
}
}
/**
* A source of remote user input, considered as a flow source for
* tainted-path vulnerabilities.
*/
class RemoteFlowSourceAsSource extends Source {
RemoteFlowSourceAsSource() {
exists(RemoteFlowSource src |
this = src and
not src instanceof ClientSideRemoteFlowSource
)
}
}
/**
* An expression whose value is interpreted as a path to a module, making it
* a data flow sink for tainted-path vulnerabilities.
*/
class ModulePathSink extends Sink, DataFlow::ValueNode {
ModulePathSink() {
astNode = any(Require rq).getArgument(0) or
astNode = any(ExternalModuleReference rq).getExpression() or
astNode = any(AmdModuleDefinition amd).getDependencies()
}
}
/**
* An expression whose value is resolved to a module using the [resolve](http://npmjs.com/package/resolve) library.
*/
class ResolveModuleSink extends Sink {
ResolveModuleSink() {
this = API::moduleImport("resolve").getACall().getArgument(0)
or
this = API::moduleImport("resolve").getMember("sync").getACall().getArgument(0)
}
}
/**
* A path argument to a file system access.
*/
class FsPathSink extends Sink, DataFlow::ValueNode {
FileSystemAccess fileSystemAccess;
FsPathSink() {
(
this = fileSystemAccess.getAPathArgument() and
not exists(fileSystemAccess.getRootPathArgument())
) and
not this = any(ResolvingPathCall call).getInput()
}
}
/**
* A path argument to a file system access, which disallows upward navigation.
*/
private class FsPathSinkWithoutUpwardNavigation extends FsPathSink {
FsPathSinkWithoutUpwardNavigation() { fileSystemAccess.isUpwardNavigationRejected(this) }
override DataFlow::FlowLabel getAFlowLabel() {
// The protection is ineffective if the ../ segments have already
// cancelled out against the intended root dir using path.join or similar.
// Only flag normalized paths, as this corresponds to the output
// of a normalizing call that had a malicious input.
result.(Label::PosixPath).isNormalized()
}
}
/**
* A path argument to the Express `res.render` method.
*/
class ExpressRenderSink extends Sink, DataFlow::ValueNode {
ExpressRenderSink() {
exists(MethodCallExpr mce |
Express::isResponse(mce.getReceiver()) and
mce.getMethodName() = "render" and
astNode = mce.getArgument(0)
)
}
}
/**
* A `templateUrl` member of an AngularJS directive.
*/
class AngularJSTemplateUrlSink extends Sink, DataFlow::ValueNode {
AngularJSTemplateUrlSink() { this = any(AngularJS::CustomDirective d).getMember("templateUrl") }
}
/**
* Holds if there is a step `src -> dst` mapping `srclabel` to `dstlabel` relevant for path traversal vulnerabilities.
*/
predicate isAdditionalTaintedPathFlowStep(
DataFlow::Node src, DataFlow::Node dst, DataFlow::FlowLabel srclabel,
DataFlow::FlowLabel dstlabel
) {
isPosixPathStep(src, dst, srclabel, dstlabel)
or
// Ignore all preliminary sanitization after decoding URI components
srclabel instanceof Label::PosixPath and
dstlabel instanceof Label::PosixPath and
(
TaintTracking::uriStep(src, dst)
or
exists(DataFlow::CallNode decode |
decode.getCalleeName() = "decodeURIComponent" or decode.getCalleeName() = "decodeURI"
|
src = decode.getArgument(0) and
dst = decode
)
)
or
TaintTracking::promiseStep(src, dst) and srclabel = dstlabel
or
TaintTracking::persistentStorageStep(src, dst) and srclabel = dstlabel
or
exists(DataFlow::PropRead read | read = dst |
src = read.getBase() and
read.getPropertyName() != "length" and
srclabel = dstlabel and
not AccessPath::DominatingPaths::hasDominatingWrite(read)
)
or
// string method calls of interest
exists(DataFlow::MethodCallNode mcn, string name |
srclabel = dstlabel and dst = mcn and mcn.calls(src, name)
|
name = StringOps::substringMethodName() and
// to avoid very dynamic transformations, require at least one fixed index
exists(mcn.getAnArgument().asExpr().getIntValue())
or
exists(string argumentlessMethodName |
argumentlessMethodName =
[
"toLocaleLowerCase", "toLocaleUpperCase", "toLowerCase", "toUpperCase", "trim",
"trimLeft", "trimRight"
]
|
name = argumentlessMethodName
)
)
or
// A `str.split()` call can either split into path elements (`str.split("/")`) or split by some other string.
exists(StringSplitCall mcn | dst = mcn and mcn.getBaseString() = src |
if mcn.getSeparator() = "/"
then
srclabel.(Label::PosixPath).canContainDotDotSlash() and
dstlabel instanceof Label::SplitPath
else srclabel = dstlabel
)
or
// array method calls of interest
exists(DataFlow::MethodCallNode mcn, string name | dst = mcn and mcn.calls(src, name) |
(
name = "pop" or
name = "shift"
) and
srclabel instanceof Label::SplitPath and
dstlabel.(Label::PosixPath).canContainDotDotSlash()
or
(
name = "slice" or
name = "splice" or
name = "concat"
) and
dstlabel instanceof Label::SplitPath and
srclabel instanceof Label::SplitPath
or
name = "join" and
mcn.getArgument(0).mayHaveStringValue("/") and
srclabel instanceof Label::SplitPath and
dstlabel.(Label::PosixPath).canContainDotDotSlash()
)
or
// prefix.concat(path)
exists(DataFlow::MethodCallNode mcn |
mcn.getMethodName() = "concat" and mcn.getAnArgument() = src
|
dst = mcn and
dstlabel instanceof Label::SplitPath and
srclabel instanceof Label::SplitPath
)
or
// reading unknown property of split path
exists(DataFlow::PropRead read | read = dst |
src = read.getBase() and
not read.getPropertyName() = "length" and
not exists(read.getPropertyNameExpr().getIntValue()) and
// split[split.length - 1]
not exists(BinaryExpr binop |
read.getPropertyNameExpr() = binop and
binop.getAnOperand().getIntValue() = 1 and
binop.getAnOperand().(PropAccess).getPropertyName() = "length"
) and
srclabel instanceof Label::SplitPath and
dstlabel.(Label::PosixPath).canContainDotDotSlash()
)
or
exists(API::CallNode call | call = API::moduleImport("slash").getACall() |
src = call.getArgument(0) and
dst = call and
srclabel = dstlabel
)
}
/**
* Holds if we should include a step from `src -> dst` with labels `srclabel -> dstlabel`, and the
* standard taint step `src -> dst` should be suppresesd.
*/
private predicate isPosixPathStep(
DataFlow::Node src, DataFlow::Node dst, Label::PosixPath srclabel, Label::PosixPath dstlabel
) {
// path.normalize() and similar
exists(NormalizingPathCall call |
src = call.getInput() and
dst = call.getOutput() and
dstlabel = srclabel.toNormalized()
)
or
// path.resolve() and similar
exists(ResolvingPathCall call |
src = call.getInput() and
dst = call.getOutput() and
dstlabel.isAbsolute() and
dstlabel.isNormalized()
)
or
// path.relative() and similar
exists(NormalizingRelativePathCall call |
src = call.getInput() and
dst = call.getOutput() and
dstlabel.isRelative() and
dstlabel.isNormalized()
)
or
// path.dirname() and similar
exists(PreservingPathCall call |
src = call.getInput() and
dst = call.getOutput() and
srclabel = dstlabel
)
or
// foo.replace(/\./, "") and similar
exists(DotRemovingReplaceCall call |
src = call.getInput() and
dst = call.getOutput() and
srclabel.isAbsolute() and
dstlabel.isAbsolute() and
dstlabel.isNormalized()
)
or
// foo.replace(/(\.\.\/)*/, "") and similar
exists(DotDotSlashPrefixRemovingReplace call |
src = call.getInput() and
dst = call.getOutput()
|
// the 4 possible combinations of normalized + relative for `srclabel`, and the possible values for `dstlabel` in each case.
srclabel.isNonNormalized() and srclabel.isRelative() // raw + relative -> any()
or
srclabel.isNormalized() and srclabel.isAbsolute() and srclabel = dstlabel // normalized + absolute -> normalized + absolute
or
srclabel.isNonNormalized() and srclabel.isAbsolute() and dstlabel.isAbsolute() // raw + absolute -> raw/normalized + absolute
// normalized + relative -> none()
)
or
// path.join()
exists(DataFlow::CallNode join, int n |
join = NodeJSLib::Path::moduleMember("join").getACall()
|
src = join.getArgument(n) and
dst = join and
(
// If the initial argument is tainted, just normalize it. It can be relative or absolute.
n = 0 and
dstlabel = srclabel.toNormalized()
or
// For later arguments, the flow label depends on whether the first argument is absolute or relative.
// If in doubt, we assume it is absolute.
n > 0 and
srclabel.canContainDotDotSlash() and
dstlabel.isNormalized() and
if isRelative(join.getArgument(0).getStringValue())
then dstlabel.isRelative()
else dstlabel.isAbsolute()
)
)
or
// String concatenation - behaves like path.join() except without normalization
exists(DataFlow::Node operator, int n | StringConcatenation::taintStep(src, dst, operator, n) |
// use ordinary taint flow for the first operand
n = 0 and
srclabel = dstlabel
or
n > 0 and
srclabel.canContainDotDotSlash() and
dstlabel.isNonNormalized() and // The ../ is no longer at the beginning of the string.
(
if isRelative(StringConcatenation::getOperand(operator, 0).getStringValue())
then dstlabel.isRelative()
else dstlabel.isAbsolute()
)
)
}
}