mirror of
https://github.com/github/codeql.git
synced 2026-02-19 16:33:40 +01:00
Merge ZipSlip sanitization logic into PathSanitizer.qll
Apply code review suggestions regarding weak sanitizers
This commit is contained in:
@@ -2,51 +2,80 @@
|
||||
|
||||
import java
|
||||
private import semmle.code.java.controlflow.Guards
|
||||
private import semmle.code.java.dataflow.FlowSources
|
||||
private import semmle.code.java.dataflow.ExternalFlow
|
||||
private import semmle.code.java.dataflow.FlowSources
|
||||
private import semmle.code.java.dataflow.SSA
|
||||
|
||||
/** A sanitizer that protects against path injection vulnerabilities. */
|
||||
abstract class PathInjectionSanitizer extends DataFlow::Node { }
|
||||
|
||||
/**
|
||||
* Holds if `g` is guard that compares a string to a trusted value.
|
||||
* Provides a set of nodes validated by a method that uses a validation guard.
|
||||
*/
|
||||
private predicate exactStringPathMatchGuard(Guard g, Expr e, boolean branch) {
|
||||
exists(MethodAccess ma |
|
||||
private module ValidationMethod<DataFlow::guardChecksSig/3 validationGuard> {
|
||||
/** Gets a node that is safely guarded by a method that uses the given guard check. */
|
||||
DataFlow::Node getAValidatedNode() {
|
||||
exists(MethodAccess ma, int pos, RValue rv |
|
||||
validationMethod(ma.getMethod(), pos) and
|
||||
ma.getArgument(pos) = rv and
|
||||
adjacentUseUseSameVar(rv, result.asExpr()) and
|
||||
ma.getBasicBlock().bbDominates(result.asExpr().getBasicBlock())
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `m` validates its `arg`th parameter by using `validationGuard`.
|
||||
*/
|
||||
private predicate validationMethod(Method m, int arg) {
|
||||
exists(
|
||||
Guard g, SsaImplicitInit var, ControlFlowNode exit, ControlFlowNode normexit, boolean branch
|
||||
|
|
||||
validationGuard(g, var.getAUse(), branch) and
|
||||
var.isParameterDefinition(m.getParameter(arg)) and
|
||||
exit = m and
|
||||
normexit.getANormalSuccessor() = exit and
|
||||
1 = strictcount(ControlFlowNode n | n.getANormalSuccessor() = exit)
|
||||
|
|
||||
g.(ConditionNode).getABranchSuccessor(branch) = exit or
|
||||
g.controls(normexit.getBasicBlock(), branch)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `g` is guard that compares a path to a trusted value.
|
||||
*/
|
||||
private predicate exactPathMatchGuard(Guard g, Expr e, boolean branch) {
|
||||
exists(MethodAccess ma, RefType t |
|
||||
t instanceof TypeString or
|
||||
t instanceof TypeUri or
|
||||
t instanceof TypePath or
|
||||
t instanceof TypeFile or
|
||||
t.hasQualifiedName("android.net", "Uri")
|
||||
|
|
||||
ma.getMethod().getDeclaringType() = t and
|
||||
ma = g and
|
||||
ma.getMethod().getDeclaringType() instanceof TypeString and
|
||||
ma.getMethod().getName() = ["equals", "equalsIgnoreCase"] and
|
||||
e = ma.getQualifier() and
|
||||
branch = true
|
||||
)
|
||||
}
|
||||
|
||||
private class ExactStringPathMatchSanitizer extends PathInjectionSanitizer {
|
||||
ExactStringPathMatchSanitizer() {
|
||||
this = DataFlow::BarrierGuard<exactStringPathMatchGuard/3>::getABarrierNode()
|
||||
private class ExactPathMatchSanitizer extends PathInjectionSanitizer {
|
||||
ExactPathMatchSanitizer() {
|
||||
this = DataFlow::BarrierGuard<exactPathMatchGuard/3>::getABarrierNode()
|
||||
or
|
||||
this = ValidationMethod<exactPathMatchGuard/3>::getAValidatedNode()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given input `e` = `v.method1(...).method2(...)...`, returns `v` where `v` is a `VarAccess`.
|
||||
*
|
||||
* This is used to look through field accessors such as `uri.getPath()`.
|
||||
*/
|
||||
private Expr getUnderlyingVarAccess(Expr e) {
|
||||
result = getUnderlyingVarAccess(e.(MethodAccess).getQualifier().getUnderlyingExpr())
|
||||
or
|
||||
result = e.(VarAccess)
|
||||
}
|
||||
|
||||
private class AllowListGuard extends Guard instanceof MethodAccess {
|
||||
AllowListGuard() {
|
||||
(isStringPartialMatch(this) or isPathPartialMatch(this)) and
|
||||
not isDisallowedWord(super.getAnArgument())
|
||||
(isStringPrefixMatch(this) or isPathPrefixMatch(this)) and
|
||||
not isDisallowedPrefix(super.getAnArgument())
|
||||
}
|
||||
|
||||
Expr getCheckedExpr() {
|
||||
result = getUnderlyingVarAccess(super.getQualifier().getUnderlyingExpr())
|
||||
}
|
||||
Expr getCheckedExpr() { result = super.getQualifier() }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -55,22 +84,21 @@ private class AllowListGuard extends Guard instanceof MethodAccess {
|
||||
* or a sanitizer (`PathNormalizeSanitizer`), to ensure any internal `..` components are removed from the path.
|
||||
*/
|
||||
private predicate allowListGuard(Guard g, Expr e, boolean branch) {
|
||||
e = g.(AllowListGuard).getCheckedExpr() and
|
||||
branch = true and
|
||||
(
|
||||
// Either a path normalization sanitizer comes before the guard,
|
||||
exists(PathNormalizeSanitizer sanitizer | DataFlow::localExprFlow(sanitizer, e))
|
||||
TaintTracking::localExprTaint(e, g.(AllowListGuard).getCheckedExpr()) and
|
||||
exists(MethodAccess previousGuard |
|
||||
TaintTracking::localExprTaint(previousGuard.(PathNormalizeSanitizer),
|
||||
g.(AllowListGuard).getCheckedExpr())
|
||||
or
|
||||
// or a check like `!path.contains("..")` comes before the guard
|
||||
exists(PathTraversalGuard previousGuard |
|
||||
DataFlow::localExprFlow(previousGuard.getCheckedExpr(), e) and
|
||||
previousGuard.controls(g.getBasicBlock().(ConditionBlock), false)
|
||||
)
|
||||
previousGuard.(PathTraversalGuard).controls(g.getBasicBlock().(ConditionBlock), false)
|
||||
)
|
||||
}
|
||||
|
||||
private class AllowListSanitizer extends PathInjectionSanitizer {
|
||||
AllowListSanitizer() { this = DataFlow::BarrierGuard<allowListGuard/3>::getABarrierNode() }
|
||||
AllowListSanitizer() {
|
||||
this = DataFlow::BarrierGuard<allowListGuard/3>::getABarrierNode() or
|
||||
this = ValidationMethod<allowListGuard/3>::getAValidatedNode()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -78,93 +106,97 @@ private class AllowListSanitizer extends PathInjectionSanitizer {
|
||||
* been checked for a trusted prefix.
|
||||
*/
|
||||
private predicate dotDotCheckGuard(Guard g, Expr e, boolean branch) {
|
||||
e = g.(PathTraversalGuard).getCheckedExpr() and
|
||||
branch = false and
|
||||
// The same value has previously been checked against a list of allowed prefixes:
|
||||
exists(AllowListGuard previousGuard |
|
||||
DataFlow::localExprFlow(previousGuard.getCheckedExpr(), e) and
|
||||
previousGuard.controls(g.getBasicBlock().(ConditionBlock), true)
|
||||
TaintTracking::localExprTaint(e, g.(PathTraversalGuard).getCheckedExpr()) and
|
||||
exists(MethodAccess previousGuard |
|
||||
previousGuard.(AllowListGuard).controls(g.getBasicBlock().(ConditionBlock), true)
|
||||
or
|
||||
previousGuard.(BlockListGuard).controls(g.getBasicBlock().(ConditionBlock), false)
|
||||
)
|
||||
}
|
||||
|
||||
private class DotDotCheckSanitizer extends PathInjectionSanitizer {
|
||||
DotDotCheckSanitizer() { this = DataFlow::BarrierGuard<dotDotCheckGuard/3>::getABarrierNode() }
|
||||
DotDotCheckSanitizer() {
|
||||
this = DataFlow::BarrierGuard<dotDotCheckGuard/3>::getABarrierNode() or
|
||||
this = ValidationMethod<dotDotCheckGuard/3>::getAValidatedNode()
|
||||
}
|
||||
}
|
||||
|
||||
private class BlockListGuard extends Guard instanceof MethodAccess {
|
||||
BlockListGuard() {
|
||||
(isStringPartialMatch(this) or isPathPartialMatch(this)) and
|
||||
(isStringPrefixMatch(this) or isPathPrefixMatch(this)) and
|
||||
isDisallowedPrefix(super.getAnArgument())
|
||||
or
|
||||
isStringPartialMatch(this) and
|
||||
isDisallowedWord(super.getAnArgument())
|
||||
}
|
||||
|
||||
Expr getCheckedExpr() {
|
||||
result = getUnderlyingVarAccess(super.getQualifier().getUnderlyingExpr())
|
||||
}
|
||||
Expr getCheckedExpr() { result = super.getQualifier() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `g` is a guard that considers a string safe because it is checked against a blocklist of known dangerous values.
|
||||
* This requires a prior check for URL encoding concealing a forbidden value, either a guard (`UrlEncodingGuard`)
|
||||
* or a sanitizer (`UrlDecodeSanitizer`).
|
||||
* This requires additional protection against path traversal, either another guard (`PathTraversalGuard`)
|
||||
* or a sanitizer (`PathNormalizeSanitizer`), to ensure any internal `..` components are removed from the path.
|
||||
*/
|
||||
private predicate blockListGuard(Guard g, Expr e, boolean branch) {
|
||||
e = g.(BlockListGuard).getCheckedExpr() and
|
||||
branch = false and
|
||||
(
|
||||
// Either `e` has been URL decoded:
|
||||
exists(UrlDecodeSanitizer sanitizer | DataFlow::localExprFlow(sanitizer, e))
|
||||
TaintTracking::localExprTaint(e, g.(BlockListGuard).getCheckedExpr()) and
|
||||
exists(MethodAccess previousGuard |
|
||||
TaintTracking::localExprTaint(previousGuard.(PathNormalizeSanitizer),
|
||||
g.(BlockListGuard).getCheckedExpr())
|
||||
or
|
||||
// or `e` has previously been checked for URL encoding sequences:
|
||||
exists(UrlEncodingGuard previousGuard |
|
||||
DataFlow::localExprFlow(previousGuard.getCheckedExpr(), e) and
|
||||
previousGuard.controls(g.getBasicBlock(), false)
|
||||
)
|
||||
previousGuard.(PathTraversalGuard).controls(g.getBasicBlock().(ConditionBlock), false)
|
||||
)
|
||||
}
|
||||
|
||||
private class BlockListSanitizer extends PathInjectionSanitizer {
|
||||
BlockListSanitizer() { this = DataFlow::BarrierGuard<blockListGuard/3>::getABarrierNode() }
|
||||
BlockListSanitizer() {
|
||||
this = DataFlow::BarrierGuard<blockListGuard/3>::getABarrierNode() or
|
||||
this = ValidationMethod<blockListGuard/3>::getAValidatedNode()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `g` is a guard that considers a string safe because it is checked for URL encoding sequences,
|
||||
* having previously been checked against a block-list of forbidden values.
|
||||
*/
|
||||
private predicate urlEncodingGuard(Guard g, Expr e, boolean branch) {
|
||||
e = g.(UrlEncodingGuard).getCheckedExpr() and
|
||||
branch = false and
|
||||
exists(BlockListGuard previousGuard |
|
||||
DataFlow::localExprFlow(previousGuard.getCheckedExpr(), e) and
|
||||
previousGuard.controls(g.getBasicBlock(), false)
|
||||
private predicate isStringPrefixMatch(MethodAccess ma) {
|
||||
exists(Method m | m = ma.getMethod() and m.getDeclaringType() instanceof TypeString |
|
||||
m.hasName("startsWith")
|
||||
or
|
||||
m.hasName("regionMatches") and
|
||||
ma.getArgument(0).(CompileTimeConstantExpr).getIntValue() = 0
|
||||
or
|
||||
m.hasName("matches") and
|
||||
not ma.getArgument(0).(CompileTimeConstantExpr).getStringValue().matches(".*%")
|
||||
)
|
||||
}
|
||||
|
||||
private class UrlEncodingSanitizer extends PathInjectionSanitizer {
|
||||
UrlEncodingSanitizer() { this = DataFlow::BarrierGuard<urlEncodingGuard/3>::getABarrierNode() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `ma` is a call to a method that checks a partial string match.
|
||||
*/
|
||||
private predicate isStringPartialMatch(MethodAccess ma) {
|
||||
ma.getMethod().getDeclaringType() instanceof TypeString and
|
||||
ma.getMethod()
|
||||
.hasName(["contains", "startsWith", "matches", "regionMatches", "indexOf", "lastIndexOf"])
|
||||
ma.getMethod().hasName(["contains", "matches", "regionMatches", "indexOf", "lastIndexOf"])
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `ma` is a call to a method that checks a partial path match.
|
||||
* Holds if `ma` is a call to a method that checks whether a path starts with a prefix.
|
||||
*/
|
||||
private predicate isPathPartialMatch(MethodAccess ma) {
|
||||
ma.getMethod().getDeclaringType() instanceof TypePath and
|
||||
ma.getMethod().hasName("startsWith")
|
||||
or
|
||||
ma.getMethod().getDeclaringType().hasQualifiedName("kotlin.io", "FilesKt") and
|
||||
ma.getMethod().hasName("startsWith")
|
||||
private predicate isPathPrefixMatch(MethodAccess ma) {
|
||||
exists(RefType t |
|
||||
t instanceof TypePath
|
||||
or
|
||||
t.hasQualifiedName("kotlin.io", "FilesKt")
|
||||
|
|
||||
t = ma.getMethod().getDeclaringType() and
|
||||
ma.getMethod().hasName("startsWith")
|
||||
)
|
||||
}
|
||||
|
||||
private predicate isDisallowedPrefix(CompileTimeConstantExpr prefix) {
|
||||
prefix.getStringValue().matches(["%WEB-INF%", "/data%"])
|
||||
}
|
||||
|
||||
private predicate isDisallowedWord(CompileTimeConstantExpr word) {
|
||||
word.getStringValue().matches(["%WEB-INF%", "%META-INF%", "%..%"])
|
||||
word.getStringValue().matches(["/", "\\"])
|
||||
}
|
||||
|
||||
/** A complementary guard that protects against path traversal, by looking for the literal `..`. */
|
||||
@@ -175,9 +207,7 @@ private class PathTraversalGuard extends Guard instanceof MethodAccess {
|
||||
super.getAnArgument().(CompileTimeConstantExpr).getStringValue() = ".."
|
||||
}
|
||||
|
||||
Expr getCheckedExpr() {
|
||||
result = getUnderlyingVarAccess(super.getQualifier().getUnderlyingExpr())
|
||||
}
|
||||
Expr getCheckedExpr() { result = super.getQualifier() }
|
||||
}
|
||||
|
||||
/** A complementary sanitizer that protects against path traversal using path normalization. */
|
||||
@@ -196,30 +226,6 @@ private class PathNormalizeSanitizer extends MethodAccess {
|
||||
}
|
||||
}
|
||||
|
||||
/** A complementary guard that protects against double URL encoding, by looking for the literal `%`. */
|
||||
private class UrlEncodingGuard extends Guard instanceof MethodAccess {
|
||||
UrlEncodingGuard() {
|
||||
super.getMethod().getDeclaringType() instanceof TypeString and
|
||||
super.getMethod().hasName(["contains", "indexOf"]) and
|
||||
super.getAnArgument().(CompileTimeConstantExpr).getStringValue() = "%"
|
||||
}
|
||||
|
||||
Expr getCheckedExpr() { result = super.getQualifier() }
|
||||
}
|
||||
|
||||
/** A complementary sanitizer that protects against double URL encoding using URL decoding. */
|
||||
private class UrlDecodeSanitizer extends MethodAccess {
|
||||
UrlDecodeSanitizer() {
|
||||
exists(RefType t |
|
||||
this.getMethod().getDeclaringType() = t and
|
||||
this.getMethod().hasName("decode")
|
||||
|
|
||||
t.hasQualifiedName("java.net", "URLDecoder") or
|
||||
t.hasQualifiedName("android.net", "Uri")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** A node with path normalization. */
|
||||
class NormalizedPathNode extends DataFlow::Node {
|
||||
NormalizedPathNode() {
|
||||
|
||||
Reference in New Issue
Block a user