diff --git a/constraintsolving/compute_metrics.py b/constraintsolving/compute_metrics.py index 09988c1efea6..db6c51b7378f 100644 --- a/constraintsolving/compute_metrics.py +++ b/constraintsolving/compute_metrics.py @@ -198,19 +198,22 @@ def createReprPredicate(ctx, project_name:str, query_type:str, reprScoresFiles = print(tsm_repr_pred_file) print(repr_scores_path) - with open(repr_scores_path, "r", encoding='utf-8') as reprscores: - with open(tsm_repr_pred_file , "w", encoding='utf-8') as reprPrFile: - reprPrFile.writelines([ - "module TsmRepr {", - "float getReprScore(string repr, string t){\n"]) - reprscores = reprscores.readlines() - if len(reprscores)>0: - reprPrFile.writelines(reprscores) - else: - reprPrFile.write('\t result = 0 and (t = "src" or t = "snk" or t = "san") and repr = ""\n') - reprPrFile.writelines(["}","}"]) - # create a TSM query in the results dir - createTSMQuery(ctx, project_name, query_type) + try: + with open(repr_scores_path, "r", encoding='utf-8') as reprscores: + with open(tsm_repr_pred_file , "w", encoding='utf-8') as reprPrFile: + reprPrFile.writelines([ + "module TsmRepr {", + "float getReprScore(string repr, string t){\n"]) + reprscores = reprscores.readlines() + if len(reprscores)>0: + reprPrFile.writelines(reprscores) + else: + reprPrFile.write('\t result = 0 and (t = "src" or t = "snk" or t = "san") and repr = ""\n') + reprPrFile.writelines(["}","}"]) + # create a TSM query in the results dir + createTSMQuery(ctx, project_name, query_type) + except Exception as e: + print(e) def createTSMQuery(ctx, project_name: str, query_type: str): tsm_folder = os.path.join(global_config.sources_root, "javascript", "ql", "src", "TSM") diff --git a/constraintsolving/generation/data.py b/constraintsolving/generation/data.py index 306f60520ac6..b39bb99381e0 100644 --- a/constraintsolving/generation/data.py +++ b/constraintsolving/generation/data.py @@ -19,7 +19,7 @@ SINKS = "Sinks" SANITIZERS = "Sanitizers" -SUPPORTED_QUERY_TYPES = ["NoSql", "Sql", "Xss", "Sel"] +SUPPORTED_QUERY_TYPES = ["NoSql", "Sql", "Xss", "Sel", "Path"] class GenerateEntitiesStep(OrchestrationStep): diff --git a/javascript/ql/src/TSM/Path/Sanitizers-Path.ql b/javascript/ql/src/TSM/Path/Sanitizers-Path.ql new file mode 100644 index 000000000000..7de124f06d88 --- /dev/null +++ b/javascript/ql/src/TSM/Path/Sanitizers-Path.ql @@ -0,0 +1,13 @@ +/** + * @kind graph + */ +import javascript +import TSM.TSM + +query predicate sanitizerSqlClasses(DataFlow::Node nd, string q, string repr){ + ( + nd instanceof TaintedPath::Sanitizer and q="TaintedPath" or + nd instanceof TaintedPathWorse::Sanitizer and q="TaintedPathWorse" + ) and + repr = PropagationGraph::getconcatrep(nd) +} diff --git a/javascript/ql/src/TSM/Path/Sinks-Path.ql b/javascript/ql/src/TSM/Path/Sinks-Path.ql new file mode 100644 index 000000000000..9780883c2a57 --- /dev/null +++ b/javascript/ql/src/TSM/Path/Sinks-Path.ql @@ -0,0 +1,12 @@ +/** + * @kind graph + */ +import javascript +import TSM.TSM + +query predicate sinkSqlClasses(DataFlow::Node nd, string q, string repr){ + (nd instanceof TaintedPath::Sink and q="TaintedPath" or + nd instanceof TaintedPathWorse::Sink and q="TaintedPathWorse" + ) and + repr = PropagationGraph::getconcatrep(nd) +} diff --git a/javascript/ql/src/TSM/Path/Sources-Path.ql b/javascript/ql/src/TSM/Path/Sources-Path.ql new file mode 100644 index 000000000000..68fd12b17085 --- /dev/null +++ b/javascript/ql/src/TSM/Path/Sources-Path.ql @@ -0,0 +1,12 @@ +/** + * @kind graph + */ +import javascript +import TSM.TSM + +query predicate sourcePathClasses(DataFlow::Node nd, string q, string repr){ + (nd instanceof TaintedPath::Source and q="TaintedPath" or + nd instanceof TaintedPathWorse::Source and q="TaintedPathWorse" + ) and + repr = PropagationGraph::getconcatrep(nd) +} diff --git a/javascript/ql/src/TSM/Sinks.qll b/javascript/ql/src/TSM/Sinks.qll index 7086cfaad4f9..8744e76f1f59 100644 --- a/javascript/ql/src/TSM/Sinks.qll +++ b/javascript/ql/src/TSM/Sinks.qll @@ -38,6 +38,7 @@ import semmle.javascript.security.dataflow.SeldonCustomizationsWorse import semmle.javascript.security.dataflow.StackTraceExposureCustomizations import semmle.javascript.security.dataflow.TaintedFormatStringCustomizations import semmle.javascript.security.dataflow.TaintedPathCustomizations +import semmle.javascript.security.dataflow.TaintedPathCustomizationsWorse import semmle.javascript.security.dataflow.TypeConfusionThroughParameterTamperingCustomizations import semmle.javascript.security.dataflow.UnsafeDeserializationCustomizations import semmle.javascript.security.dataflow.UnsafeDynamicMethodAccessCustomizations diff --git a/javascript/ql/src/semmle/javascript/security/dataflow/TaintedPathCustomizationsWorse.qll b/javascript/ql/src/semmle/javascript/security/dataflow/TaintedPathCustomizationsWorse.qll new file mode 100644 index 000000000000..bd8ac96c9c95 --- /dev/null +++ b/javascript/ql/src/semmle/javascript/security/dataflow/TaintedPathCustomizationsWorse.qll @@ -0,0 +1,445 @@ +/** + * Obtained from https://raw.githubusercontent.com/github/codeql/35d81513741282f7d5df78cf05c1e0715c086ccc/javascript/ql/src/semmle/javascript/security/dataflow/TaintedPathCustomizations.qll + * Provides default sources, sinks and sanitizers for reasoning about + * tainted-path vulnerabilities, as well as extension points for + * adding your own. + */ + +import javascript + +module TaintedPathWorse { + /** + * 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 { } + + 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 (isNormalized() and isAbsolute()) + } + } + + 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 = 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 = getAnArgument() and + output = this + or + this = DataFlow::moduleMember("fs", "realpathSync").getACall() and + input = getArgument(0) and + output = this + or + this = DataFlow::moduleMember("fs", "realpath").getACall() and + input = getArgument(0) and + output = 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 = 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() { + exists(string name | name = "dirname" or name = "toNamespacedPath" | + this = NodeJSLib::Path::moduleMember(name).getACall() and + input = getAnArgument() and + output = this + ) + or + // non-global replace or replace of something other than /\.\./g + this.getCalleeName() = "replace" and + input = getReceiver() and + output = this and + not exists(RegExpLiteral literal, RegExpSequence seq | + getArgument(0).getALocalSource().asExpr() = literal and + literal.isGlobal() and + literal.getRoot() = seq and + seq.getChild(0).(RegExpConstant).getValue() = "." and + seq.getChild(1).(RegExpConstant).getValue() = "." and + seq.getNumChild() = 2 + ) + } + + /** + * 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 DataFlow::LabeledBarrierGuardNode { + 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 form `x.startsWith(dir)` that sanitizes normalized absolute paths, since it is then + * known to be in a subdirectory of `dir`. + */ + class StartsWithDirSanitizer extends DataFlow::LabeledBarrierGuardNode { + 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 DataFlow::LabeledBarrierGuardNode { + 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 DataFlow::LabeledBarrierGuardNode { + StringOps::Includes contains; + + ContainsDotDotSanitizer() { + this = contains and + isDotDotSlashPrefix(contains.getSubstring()) + } + + override predicate blocks(boolean outcome, Expr e, DataFlow::FlowLabel label) { + e = contains.getBaseString().asExpr() and + outcome = contains.getPolarity().booleanNot() and + label.(Label::PosixPath).canContainDotDotSlash() // can still be bypassed by normalized absolute path + } + } + + /** + * A source of remote user input, considered as a flow source for + * tainted-path vulnerabilities. + */ + class RemoteFlowSourceAsSource extends Source { + RemoteFlowSourceAsSource() { this instanceof RemoteFlowSource } + } + + /** + * 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() + } + } + + /** + * 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()) + or + this = 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") } + } + + /** + * The path argument of a [send](https://www.npmjs.com/package/send) call, viewed as a sink. + */ + class SendPathSink extends Sink, DataFlow::ValueNode { + SendPathSink() { this = DataFlow::moduleImport("send").getACall().getArgument(1) } + } +} \ No newline at end of file diff --git a/javascript/ql/src/semmle/javascript/security/dataflow/TaintedPathWorse.qll b/javascript/ql/src/semmle/javascript/security/dataflow/TaintedPathWorse.qll new file mode 100644 index 000000000000..5726699181e3 --- /dev/null +++ b/javascript/ql/src/semmle/javascript/security/dataflow/TaintedPathWorse.qll @@ -0,0 +1,210 @@ +/** + * Obtained from https://raw.githubusercontent.com/github/codeql/35d81513741282f7d5df78cf05c1e0715c086ccc/javascript/ql/src/semmle/javascript/security/dataflow/TaintedPath.qll + * Provides a taint tracking configuration for reasoning about + * tainted-path vulnerabilities. + * + * Note, for performance reasons: only import this file if + * `TaintedPath::Configuration` is needed, otherwise + * `TaintedPathCustomizations` should be imported instead. + */ + +import javascript + +module TaintedPathWorse { + import TaintedPathCustomizationsWorse::TaintedPathWorse + + /** + * A taint-tracking configuration for reasoning about tainted-path vulnerabilities. + */ + class Configuration extends DataFlow::Configuration { + Configuration() { this = "TaintedPath" } + + override predicate isSource(DataFlow::Node source, DataFlow::FlowLabel label) { + label = source.(Source).getAFlowLabel() + } + + override predicate isSink(DataFlow::Node sink, DataFlow::FlowLabel label) { + label = sink.(Sink).getAFlowLabel() + } + + override predicate isBarrier(DataFlow::Node node) { + super.isBarrier(node) or + node instanceof Sanitizer + } + + override predicate isBarrierGuard(DataFlow::BarrierGuardNode guard) { + guard instanceof StartsWithDotDotSanitizer or + guard instanceof StartsWithDirSanitizer or + guard instanceof IsAbsoluteSanitizer or + guard instanceof ContainsDotDotSanitizer + } + + override predicate isAdditionalFlowStep( + DataFlow::Node src, DataFlow::Node dst, DataFlow::FlowLabel srclabel, + DataFlow::FlowLabel dstlabel + ) { + isTaintedPathStep(src, dst, srclabel, dstlabel) + or + // Ignore all preliminary sanitization after decoding URI components + srclabel instanceof Label::PosixPath and + dstlabel instanceof Label::PosixPath and + ( + any(UriLibraryStep step).step(src, dst) + or + exists(DataFlow::CallNode decode | + decode.getCalleeName() = "decodeURIComponent" or decode.getCalleeName() = "decodeURI" + | + src = decode.getArgument(0) and + dst = decode + ) + ) + or + promiseTaintStep(src, dst) and srclabel = dstlabel + or + any(TaintTracking::PersistentStorageTaintStep st).step(src, dst) and srclabel = dstlabel + or + exists(DataFlow::PropRead read | read = dst | + src = read.getBase() and + read.getPropertyName() != "length" and + srclabel = dstlabel + ) + or + // string method calls of interest + exists(DataFlow::MethodCallNode mcn, string name | + srclabel = dstlabel and dst = mcn and mcn.calls(src, name) + | + exists(string substringMethodName | + substringMethodName = "substr" or + substringMethodName = "substring" or + substringMethodName = "slice" + | + name = substringMethodName and + // to avoid very dynamic transformations, require at least one fixed index + exists(mcn.getAnArgument().asExpr().getIntValue()) + ) + or + exists(string argumentlessMethodName | + argumentlessMethodName = "toLocaleLowerCase" or + argumentlessMethodName = "toLocaleUpperCase" or + argumentlessMethodName = "toLowerCase" or + argumentlessMethodName = "toUpperCase" or + argumentlessMethodName = "trim" or + argumentlessMethodName = "trimLeft" or + argumentlessMethodName = "trimRight" + | + name = argumentlessMethodName + ) + ) + or + // array method calls of interest + exists(DataFlow::MethodCallNode mcn, string name | dst = mcn and mcn.calls(src, name) | + // A `str.split()` call can either split into path elements (`str.split("/")`) or split by some other string. + name = "split" and + ( + if + exists(DataFlow::Node splitBy | splitBy = mcn.getArgument(0) | + splitBy.mayHaveStringValue("/") or + any(DataFlow::RegExpLiteralNode reg | reg.getRoot().getAMatchedString() = "/") + .flowsTo(splitBy) + ) + then + srclabel.(Label::PosixPath).canContainDotDotSlash() and + dstlabel instanceof Label::SplitPath + else srclabel = dstlabel + ) + or + ( + name = "pop" or + name = "shift" or + name = "slice" or + name = "splice" + ) 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() + ) + } + + /** + * Holds if we should include a step from `src -> dst` with labels `srclabel -> dstlabel`, and the + * standard taint step `src -> dst` should be suppresesd. + */ + predicate isTaintedPathStep( + 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 + // 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() + ) + ) + } + } +} \ No newline at end of file