Skip to content

Commit 4e29c39

Browse files
committed
Merge ZipSlip sanitization logic into PathSanitizer.qll
Apply code review suggestions regarding weak sanitizers
1 parent 89d905c commit 4e29c39

File tree

4 files changed

+118
-185
lines changed

4 files changed

+118
-185
lines changed

java/ql/lib/semmle/code/java/security/PathSanitizer.qll

Lines changed: 113 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -2,51 +2,80 @@
22

33
import java
44
private import semmle.code.java.controlflow.Guards
5-
private import semmle.code.java.dataflow.FlowSources
65
private import semmle.code.java.dataflow.ExternalFlow
6+
private import semmle.code.java.dataflow.FlowSources
7+
private import semmle.code.java.dataflow.SSA
78

89
/** A sanitizer that protects against path injection vulnerabilities. */
910
abstract class PathInjectionSanitizer extends DataFlow::Node { }
1011

1112
/**
12-
* Holds if `g` is guard that compares a string to a trusted value.
13+
* Provides a set of nodes validated by a method that uses a validation guard.
1314
*/
14-
private predicate exactStringPathMatchGuard(Guard g, Expr e, boolean branch) {
15-
exists(MethodAccess ma |
15+
private module ValidationMethod<DataFlow::guardChecksSig/3 validationGuard> {
16+
/** Gets a node that is safely guarded by a method that uses the given guard check. */
17+
DataFlow::Node getAValidatedNode() {
18+
exists(MethodAccess ma, int pos, RValue rv |
19+
validationMethod(ma.getMethod(), pos) and
20+
ma.getArgument(pos) = rv and
21+
adjacentUseUseSameVar(rv, result.asExpr()) and
22+
ma.getBasicBlock().bbDominates(result.asExpr().getBasicBlock())
23+
)
24+
}
25+
26+
/**
27+
* Holds if `m` validates its `arg`th parameter by using `validationGuard`.
28+
*/
29+
private predicate validationMethod(Method m, int arg) {
30+
exists(
31+
Guard g, SsaImplicitInit var, ControlFlowNode exit, ControlFlowNode normexit, boolean branch
32+
|
33+
validationGuard(g, var.getAUse(), branch) and
34+
var.isParameterDefinition(m.getParameter(arg)) and
35+
exit = m and
36+
normexit.getANormalSuccessor() = exit and
37+
1 = strictcount(ControlFlowNode n | n.getANormalSuccessor() = exit)
38+
|
39+
g.(ConditionNode).getABranchSuccessor(branch) = exit or
40+
g.controls(normexit.getBasicBlock(), branch)
41+
)
42+
}
43+
}
44+
45+
/**
46+
* Holds if `g` is guard that compares a path to a trusted value.
47+
*/
48+
private predicate exactPathMatchGuard(Guard g, Expr e, boolean branch) {
49+
exists(MethodAccess ma, RefType t |
50+
t instanceof TypeString or
51+
t instanceof TypeUri or
52+
t instanceof TypePath or
53+
t instanceof TypeFile or
54+
t.hasQualifiedName("android.net", "Uri")
55+
|
56+
ma.getMethod().getDeclaringType() = t and
1657
ma = g and
17-
ma.getMethod().getDeclaringType() instanceof TypeString and
1858
ma.getMethod().getName() = ["equals", "equalsIgnoreCase"] and
1959
e = ma.getQualifier() and
2060
branch = true
2161
)
2262
}
2363

24-
private class ExactStringPathMatchSanitizer extends PathInjectionSanitizer {
25-
ExactStringPathMatchSanitizer() {
26-
this = DataFlow::BarrierGuard<exactStringPathMatchGuard/3>::getABarrierNode()
64+
private class ExactPathMatchSanitizer extends PathInjectionSanitizer {
65+
ExactPathMatchSanitizer() {
66+
this = DataFlow::BarrierGuard<exactPathMatchGuard/3>::getABarrierNode()
67+
or
68+
this = ValidationMethod<exactPathMatchGuard/3>::getAValidatedNode()
2769
}
2870
}
2971

30-
/**
31-
* Given input `e` = `v.method1(...).method2(...)...`, returns `v` where `v` is a `VarAccess`.
32-
*
33-
* This is used to look through field accessors such as `uri.getPath()`.
34-
*/
35-
private Expr getUnderlyingVarAccess(Expr e) {
36-
result = getUnderlyingVarAccess(e.(MethodAccess).getQualifier().getUnderlyingExpr())
37-
or
38-
result = e.(VarAccess)
39-
}
40-
4172
private class AllowListGuard extends Guard instanceof MethodAccess {
4273
AllowListGuard() {
43-
(isStringPartialMatch(this) or isPathPartialMatch(this)) and
44-
not isDisallowedWord(super.getAnArgument())
74+
(isStringPrefixMatch(this) or isPathPrefixMatch(this)) and
75+
not isDisallowedPrefix(super.getAnArgument())
4576
}
4677

47-
Expr getCheckedExpr() {
48-
result = getUnderlyingVarAccess(super.getQualifier().getUnderlyingExpr())
49-
}
78+
Expr getCheckedExpr() { result = super.getQualifier() }
5079
}
5180

5281
/**
@@ -55,116 +84,119 @@ private class AllowListGuard extends Guard instanceof MethodAccess {
5584
* or a sanitizer (`PathNormalizeSanitizer`), to ensure any internal `..` components are removed from the path.
5685
*/
5786
private predicate allowListGuard(Guard g, Expr e, boolean branch) {
58-
e = g.(AllowListGuard).getCheckedExpr() and
5987
branch = true and
60-
(
61-
// Either a path normalization sanitizer comes before the guard,
62-
exists(PathNormalizeSanitizer sanitizer | DataFlow::localExprFlow(sanitizer, e))
88+
TaintTracking::localExprTaint(e, g.(AllowListGuard).getCheckedExpr()) and
89+
exists(MethodAccess previousGuard |
90+
TaintTracking::localExprTaint(previousGuard.(PathNormalizeSanitizer),
91+
g.(AllowListGuard).getCheckedExpr())
6392
or
64-
// or a check like `!path.contains("..")` comes before the guard
65-
exists(PathTraversalGuard previousGuard |
66-
DataFlow::localExprFlow(previousGuard.getCheckedExpr(), e) and
67-
previousGuard.controls(g.getBasicBlock().(ConditionBlock), false)
68-
)
93+
previousGuard.(PathTraversalGuard).controls(g.getBasicBlock().(ConditionBlock), false)
6994
)
7095
}
7196

7297
private class AllowListSanitizer extends PathInjectionSanitizer {
73-
AllowListSanitizer() { this = DataFlow::BarrierGuard<allowListGuard/3>::getABarrierNode() }
98+
AllowListSanitizer() {
99+
this = DataFlow::BarrierGuard<allowListGuard/3>::getABarrierNode() or
100+
this = ValidationMethod<allowListGuard/3>::getAValidatedNode()
101+
}
74102
}
75103

76104
/**
77105
* Holds if `g` is a guard that considers a path safe because it is checked for `..` components, having previously
78106
* been checked for a trusted prefix.
79107
*/
80108
private predicate dotDotCheckGuard(Guard g, Expr e, boolean branch) {
81-
e = g.(PathTraversalGuard).getCheckedExpr() and
82109
branch = false and
83-
// The same value has previously been checked against a list of allowed prefixes:
84-
exists(AllowListGuard previousGuard |
85-
DataFlow::localExprFlow(previousGuard.getCheckedExpr(), e) and
86-
previousGuard.controls(g.getBasicBlock().(ConditionBlock), true)
110+
TaintTracking::localExprTaint(e, g.(PathTraversalGuard).getCheckedExpr()) and
111+
exists(MethodAccess previousGuard |
112+
previousGuard.(AllowListGuard).controls(g.getBasicBlock().(ConditionBlock), true)
113+
or
114+
previousGuard.(BlockListGuard).controls(g.getBasicBlock().(ConditionBlock), false)
87115
)
88116
}
89117

90118
private class DotDotCheckSanitizer extends PathInjectionSanitizer {
91-
DotDotCheckSanitizer() { this = DataFlow::BarrierGuard<dotDotCheckGuard/3>::getABarrierNode() }
119+
DotDotCheckSanitizer() {
120+
this = DataFlow::BarrierGuard<dotDotCheckGuard/3>::getABarrierNode() or
121+
this = ValidationMethod<dotDotCheckGuard/3>::getAValidatedNode()
122+
}
92123
}
93124

94125
private class BlockListGuard extends Guard instanceof MethodAccess {
95126
BlockListGuard() {
96-
(isStringPartialMatch(this) or isPathPartialMatch(this)) and
127+
(isStringPrefixMatch(this) or isPathPrefixMatch(this)) and
128+
isDisallowedPrefix(super.getAnArgument())
129+
or
130+
isStringPartialMatch(this) and
97131
isDisallowedWord(super.getAnArgument())
98132
}
99133

100-
Expr getCheckedExpr() {
101-
result = getUnderlyingVarAccess(super.getQualifier().getUnderlyingExpr())
102-
}
134+
Expr getCheckedExpr() { result = super.getQualifier() }
103135
}
104136

105137
/**
106138
* Holds if `g` is a guard that considers a string safe because it is checked against a blocklist of known dangerous values.
107-
* This requires a prior check for URL encoding concealing a forbidden value, either a guard (`UrlEncodingGuard`)
108-
* or a sanitizer (`UrlDecodeSanitizer`).
139+
* This requires additional protection against path traversal, either another guard (`PathTraversalGuard`)
140+
* or a sanitizer (`PathNormalizeSanitizer`), to ensure any internal `..` components are removed from the path.
109141
*/
110142
private predicate blockListGuard(Guard g, Expr e, boolean branch) {
111-
e = g.(BlockListGuard).getCheckedExpr() and
112143
branch = false and
113-
(
114-
// Either `e` has been URL decoded:
115-
exists(UrlDecodeSanitizer sanitizer | DataFlow::localExprFlow(sanitizer, e))
144+
TaintTracking::localExprTaint(e, g.(BlockListGuard).getCheckedExpr()) and
145+
exists(MethodAccess previousGuard |
146+
TaintTracking::localExprTaint(previousGuard.(PathNormalizeSanitizer),
147+
g.(BlockListGuard).getCheckedExpr())
116148
or
117-
// or `e` has previously been checked for URL encoding sequences:
118-
exists(UrlEncodingGuard previousGuard |
119-
DataFlow::localExprFlow(previousGuard.getCheckedExpr(), e) and
120-
previousGuard.controls(g.getBasicBlock(), false)
121-
)
149+
previousGuard.(PathTraversalGuard).controls(g.getBasicBlock().(ConditionBlock), false)
122150
)
123151
}
124152

125153
private class BlockListSanitizer extends PathInjectionSanitizer {
126-
BlockListSanitizer() { this = DataFlow::BarrierGuard<blockListGuard/3>::getABarrierNode() }
154+
BlockListSanitizer() {
155+
this = DataFlow::BarrierGuard<blockListGuard/3>::getABarrierNode() or
156+
this = ValidationMethod<blockListGuard/3>::getAValidatedNode()
157+
}
127158
}
128159

129-
/**
130-
* Holds if `g` is a guard that considers a string safe because it is checked for URL encoding sequences,
131-
* having previously been checked against a block-list of forbidden values.
132-
*/
133-
private predicate urlEncodingGuard(Guard g, Expr e, boolean branch) {
134-
e = g.(UrlEncodingGuard).getCheckedExpr() and
135-
branch = false and
136-
exists(BlockListGuard previousGuard |
137-
DataFlow::localExprFlow(previousGuard.getCheckedExpr(), e) and
138-
previousGuard.controls(g.getBasicBlock(), false)
160+
private predicate isStringPrefixMatch(MethodAccess ma) {
161+
exists(Method m | m = ma.getMethod() and m.getDeclaringType() instanceof TypeString |
162+
m.hasName("startsWith")
163+
or
164+
m.hasName("regionMatches") and
165+
ma.getArgument(0).(CompileTimeConstantExpr).getIntValue() = 0
166+
or
167+
m.hasName("matches") and
168+
not ma.getArgument(0).(CompileTimeConstantExpr).getStringValue().matches(".*%")
139169
)
140170
}
141171

142-
private class UrlEncodingSanitizer extends PathInjectionSanitizer {
143-
UrlEncodingSanitizer() { this = DataFlow::BarrierGuard<urlEncodingGuard/3>::getABarrierNode() }
144-
}
145-
146172
/**
147173
* Holds if `ma` is a call to a method that checks a partial string match.
148174
*/
149175
private predicate isStringPartialMatch(MethodAccess ma) {
150176
ma.getMethod().getDeclaringType() instanceof TypeString and
151-
ma.getMethod()
152-
.hasName(["contains", "startsWith", "matches", "regionMatches", "indexOf", "lastIndexOf"])
177+
ma.getMethod().hasName(["contains", "matches", "regionMatches", "indexOf", "lastIndexOf"])
153178
}
154179

155180
/**
156-
* Holds if `ma` is a call to a method that checks a partial path match.
181+
* Holds if `ma` is a call to a method that checks whether a path starts with a prefix.
157182
*/
158-
private predicate isPathPartialMatch(MethodAccess ma) {
159-
ma.getMethod().getDeclaringType() instanceof TypePath and
160-
ma.getMethod().hasName("startsWith")
161-
or
162-
ma.getMethod().getDeclaringType().hasQualifiedName("kotlin.io", "FilesKt") and
163-
ma.getMethod().hasName("startsWith")
183+
private predicate isPathPrefixMatch(MethodAccess ma) {
184+
exists(RefType t |
185+
t instanceof TypePath
186+
or
187+
t.hasQualifiedName("kotlin.io", "FilesKt")
188+
|
189+
t = ma.getMethod().getDeclaringType() and
190+
ma.getMethod().hasName("startsWith")
191+
)
192+
}
193+
194+
private predicate isDisallowedPrefix(CompileTimeConstantExpr prefix) {
195+
prefix.getStringValue().matches(["%WEB-INF%", "/data%"])
164196
}
165197

166198
private predicate isDisallowedWord(CompileTimeConstantExpr word) {
167-
word.getStringValue().matches(["%WEB-INF%", "%META-INF%", "%..%"])
199+
word.getStringValue().matches(["/", "\\"])
168200
}
169201

170202
/** A complementary guard that protects against path traversal, by looking for the literal `..`. */
@@ -175,9 +207,7 @@ private class PathTraversalGuard extends Guard instanceof MethodAccess {
175207
super.getAnArgument().(CompileTimeConstantExpr).getStringValue() = ".."
176208
}
177209

178-
Expr getCheckedExpr() {
179-
result = getUnderlyingVarAccess(super.getQualifier().getUnderlyingExpr())
180-
}
210+
Expr getCheckedExpr() { result = super.getQualifier() }
181211
}
182212

183213
/** A complementary sanitizer that protects against path traversal using path normalization. */
@@ -196,30 +226,6 @@ private class PathNormalizeSanitizer extends MethodAccess {
196226
}
197227
}
198228

199-
/** A complementary guard that protects against double URL encoding, by looking for the literal `%`. */
200-
private class UrlEncodingGuard extends Guard instanceof MethodAccess {
201-
UrlEncodingGuard() {
202-
super.getMethod().getDeclaringType() instanceof TypeString and
203-
super.getMethod().hasName(["contains", "indexOf"]) and
204-
super.getAnArgument().(CompileTimeConstantExpr).getStringValue() = "%"
205-
}
206-
207-
Expr getCheckedExpr() { result = super.getQualifier() }
208-
}
209-
210-
/** A complementary sanitizer that protects against double URL encoding using URL decoding. */
211-
private class UrlDecodeSanitizer extends MethodAccess {
212-
UrlDecodeSanitizer() {
213-
exists(RefType t |
214-
this.getMethod().getDeclaringType() = t and
215-
this.getMethod().hasName("decode")
216-
|
217-
t.hasQualifiedName("java.net", "URLDecoder") or
218-
t.hasQualifiedName("android.net", "Uri")
219-
)
220-
}
221-
}
222-
223229
/** A node with path normalization. */
224230
class NormalizedPathNode extends DataFlow::Node {
225231
NormalizedPathNode() {

0 commit comments

Comments
 (0)