mirror of
https://github.com/github/codeql.git
synced 2026-05-20 14:17:11 +02:00
882 lines
27 KiB
Plaintext
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()
|
|
)
|
|
)
|
|
}
|
|
}
|