Skip to content

Commit 21b51b4

Browse files
committed
Adapt PathSanitizer to Kotlin
1 parent 6bffb11 commit 21b51b4

File tree

6 files changed

+610
-40
lines changed

6 files changed

+610
-40
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/** Provides classes and predicates related to `kotlin.io`. */
2+
3+
import java
4+
5+
/** The type `kotlin.io.FilesKt`, where `File` extension methods are declared. */
6+
class FilesKt extends RefType {
7+
FilesKt() { this.hasQualifiedName("kotlin.io", "FilesKt") }
8+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/** Provides classes and predicates related to `kotlin.text`. */
2+
3+
import java
4+
5+
/** The type `kotlin.text.StringsKt`, where `String` extension methods are declared. */
6+
class StringsKt extends RefType {
7+
StringsKt() { this.hasQualifiedName("kotlin.text", "StringsKt") }
8+
}
9+
10+
/** A call to the extension method `String.toRegex` from `kotlin.text`. */
11+
class KtToRegex extends MethodAccess {
12+
KtToRegex() {
13+
this.getMethod().getDeclaringType() instanceof StringsKt and
14+
this.getMethod().hasName("toRegex")
15+
}
16+
17+
string getExpressionString() {
18+
result = this.getArgument(0).(CompileTimeConstantExpr).getStringValue()
19+
}
20+
}

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

Lines changed: 101 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ private import semmle.code.java.controlflow.Guards
55
private import semmle.code.java.dataflow.ExternalFlow
66
private import semmle.code.java.dataflow.FlowSources
77
private import semmle.code.java.dataflow.SSA
8+
private import semmle.code.java.frameworks.kotlin.IO
9+
private import semmle.code.java.frameworks.kotlin.Text
810

911
/** A sanitizer that protects against path injection vulnerabilities. */
1012
abstract class PathInjectionSanitizer extends DataFlow::Node { }
@@ -47,16 +49,25 @@ private module ValidationMethod<DataFlow::guardChecksSig/3 validationGuard> {
4749
*/
4850
private predicate exactPathMatchGuard(Guard g, Expr e, boolean branch) {
4951
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")
52+
(
53+
t instanceof TypeString or
54+
t instanceof TypeUri or
55+
t instanceof TypePath or
56+
t instanceof TypeFile or
57+
t.hasQualifiedName("android.net", "Uri")
58+
) and
59+
e = ma.getQualifier().getUnderlyingExpr()
60+
or
61+
(
62+
ma.getMethod().getDeclaringType() instanceof StringsKt
63+
or
64+
ma.getMethod().getDeclaringType() instanceof FilesKt
65+
) and
66+
e = ma.getArgument(0).getUnderlyingExpr()
5567
|
5668
ma.getMethod().getDeclaringType() = t and
5769
ma = g and
58-
ma.getMethod().getName() = ["equals", "equalsIgnoreCase"] and
59-
e = ma.getQualifier() and
70+
ma.getMethod().getName() = ["equals", "equalsIgnoreCase"] + ["", "$default"] and
6071
branch = true
6172
)
6273
}
@@ -82,12 +93,18 @@ private predicate localTaintFlowToPathGuard(Expr e, PathGuard g) {
8293
}
8394

8495
private class AllowedPrefixGuard extends PathGuard instanceof MethodAccess {
96+
Expr checkedExpr;
97+
8598
AllowedPrefixGuard() {
86-
(isStringPrefixMatch(this) or isPathPrefixMatch(this)) and
99+
(
100+
isStringPrefixMatch(this, checkedExpr)
101+
or
102+
isPathPrefixMatch(this, checkedExpr)
103+
) and
87104
not isDisallowedWord(super.getAnArgument())
88105
}
89106

90-
override Expr getCheckedExpr() { result = super.getQualifier() }
107+
override Expr getCheckedExpr() { result = checkedExpr }
91108
}
92109

93110
/**
@@ -149,12 +166,18 @@ private class DotDotCheckSanitizer extends PathInjectionSanitizer {
149166
}
150167

151168
private class BlockListGuard extends PathGuard instanceof MethodAccess {
169+
Expr checkedExpr;
170+
152171
BlockListGuard() {
153-
(isStringPartialMatch(this) or isPathPrefixMatch(this)) and
172+
(
173+
isStringPartialMatch(this, checkedExpr)
174+
or
175+
isPathPrefixMatch(this, checkedExpr)
176+
) and
154177
isDisallowedWord(super.getAnArgument())
155178
}
156179

157-
override Expr getCheckedExpr() { result = super.getQualifier() }
180+
override Expr getCheckedExpr() { result = checkedExpr }
158181
}
159182

160183
/**
@@ -188,70 +211,109 @@ private class BlockListSanitizer extends PathInjectionSanitizer {
188211
}
189212
}
190213

191-
private predicate isStringPrefixMatch(MethodAccess ma) {
192-
exists(Method m | m = ma.getMethod() and m.getDeclaringType() instanceof TypeString |
193-
m.hasName("startsWith")
194-
or
195-
m.hasName("regionMatches") and
196-
ma.getArgument(0).(CompileTimeConstantExpr).getIntValue() = 0
214+
private class ConstantOrRegex extends Expr {
215+
ConstantOrRegex() {
216+
this instanceof CompileTimeConstantExpr or
217+
this instanceof KtToRegex
218+
}
219+
220+
string getStringValue() {
221+
result = this.(CompileTimeConstantExpr).getStringValue() or
222+
result = this.(KtToRegex).getExpressionString()
223+
}
224+
}
225+
226+
private predicate isStringPrefixMatch(MethodAccess ma, Expr checkedExpr) {
227+
exists(Method m |
228+
m = ma.getMethod() and
229+
(
230+
m.getDeclaringType() instanceof TypeString and
231+
checkedExpr = ma.getQualifier().getUnderlyingExpr()
232+
or
233+
m.getDeclaringType() instanceof StringsKt and
234+
checkedExpr = ma.getArgument(0).getUnderlyingExpr()
235+
)
236+
|
237+
m.hasName("startsWith" + ["", "$default"])
197238
or
198-
m.hasName("matches") and
199-
not ma.getArgument(0).(CompileTimeConstantExpr).getStringValue().matches(".*%")
239+
exists(int argPos |
240+
m.getDeclaringType() instanceof TypeString and argPos = 0
241+
or
242+
m.getDeclaringType() instanceof StringsKt and argPos = 1
243+
|
244+
m.hasName("regionMatches" + ["", "$default"]) and
245+
ma.getArgument(argPos).(CompileTimeConstantExpr).getIntValue() = 0
246+
or
247+
m.hasName("matches") and
248+
not ma.getArgument(argPos).(ConstantOrRegex).getStringValue().matches(".*%")
249+
)
200250
)
201251
}
202252

203253
/**
204-
* Holds if `ma` is a call to a method that checks a partial string match.
254+
* Holds if `ma` is a call to a method that checks a partial string match on `checkedExpr`.
205255
*/
206-
private predicate isStringPartialMatch(MethodAccess ma) {
207-
isStringPrefixMatch(ma)
256+
private predicate isStringPartialMatch(MethodAccess ma, Expr checkedExpr) {
257+
isStringPrefixMatch(ma, checkedExpr)
208258
or
209-
ma.getMethod().getDeclaringType() instanceof TypeString and
210-
ma.getMethod().hasName(["contains", "matches", "regionMatches", "indexOf", "lastIndexOf"])
259+
(
260+
ma.getMethod().getDeclaringType() instanceof TypeString and
261+
checkedExpr = ma.getQualifier().getUnderlyingExpr()
262+
or
263+
ma.getMethod().getDeclaringType() instanceof StringsKt and
264+
checkedExpr = ma.getArgument(0).getUnderlyingExpr()
265+
) and
266+
ma.getMethod()
267+
.hasName(["contains", "matches", "regionMatches", "indexOf", "lastIndexOf"] + ["", "$default"])
211268
}
212269

213270
/**
214-
* Holds if `ma` is a call to a method that checks whether a path starts with a prefix.
271+
* Holds if `ma` is a call to a method that checks whether `checkedExpr` starts with a prefix.
215272
*/
216-
private predicate isPathPrefixMatch(MethodAccess ma) {
273+
private predicate isPathPrefixMatch(MethodAccess ma, Expr checkedExpr) {
217274
exists(RefType t |
218-
t instanceof TypePath
275+
t instanceof TypePath and checkedExpr = ma.getQualifier().getUnderlyingExpr()
219276
or
220-
t.hasQualifiedName("kotlin.io", "FilesKt")
277+
t instanceof FilesKt and
278+
checkedExpr = ma.getArgument(0).getUnderlyingExpr()
221279
|
222280
t = ma.getMethod().getDeclaringType() and
223-
ma.getMethod().hasName("startsWith")
281+
ma.getMethod().hasName("startsWith" + ["", "$default"])
224282
)
225283
}
226284

227-
private predicate isDisallowedWord(CompileTimeConstantExpr word) {
285+
private predicate isDisallowedWord(ConstantOrRegex word) {
228286
word.getStringValue().matches(["/", "\\", "%WEB-INF%", "%/data%"])
229287
}
230288

231289
/** A complementary guard that protects against path traversal, by looking for the literal `..`. */
232290
private class PathTraversalGuard extends PathGuard {
291+
Expr checkedExpr;
292+
233293
PathTraversalGuard() {
234294
exists(MethodAccess ma |
235-
ma.getMethod().getDeclaringType() instanceof TypeString and
295+
(
296+
ma.getMethod().getDeclaringType() instanceof TypeString and
297+
checkedExpr = ma.getQualifier().getUnderlyingExpr()
298+
or
299+
ma.getMethod().getDeclaringType() instanceof StringsKt and
300+
checkedExpr = ma.getArgument(0).getUnderlyingExpr()
301+
) and
236302
ma.getAnArgument().(CompileTimeConstantExpr).getStringValue() = ".."
237303
|
238304
this = ma and
239-
ma.getMethod().hasName("contains")
305+
ma.getMethod().hasName("contains" + ["", "$default"])
240306
or
241307
exists(EqualityTest eq |
242308
this = eq and
243-
ma.getMethod().hasName(["indexOf", "lastIndexOf"]) and
309+
ma.getMethod().hasName(["indexOf", "lastIndexOf"] + ["", "$default"]) and
244310
eq.getAnOperand() = ma and
245311
eq.getAnOperand().(CompileTimeConstantExpr).getIntValue() = -1
246312
)
247313
)
248314
}
249315

250-
override Expr getCheckedExpr() {
251-
exists(MethodAccess ma | ma = this.(EqualityTest).getAnOperand() or ma = this |
252-
result = ma.getQualifier()
253-
)
254-
}
316+
override Expr getCheckedExpr() { result = checkedExpr }
255317

256318
boolean getBranch() {
257319
this instanceof MethodAccess and result = false
@@ -265,7 +327,7 @@ private class PathNormalizeSanitizer extends MethodAccess {
265327
PathNormalizeSanitizer() {
266328
exists(RefType t |
267329
t instanceof TypePath or
268-
t.hasQualifiedName("kotlin.io", "FilesKt")
330+
t instanceof FilesKt
269331
|
270332
this.getMethod().getDeclaringType() = t and
271333
this.getMethod().hasName("normalize")

java/ql/test/library-tests/pathsanitizer/Test.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,7 @@ public void dotDotCheckGuard() throws Exception {
279279
}
280280

281281
private void blockListGuardValidation(String path) throws Exception {
282-
if (path.contains("..") || !path.startsWith("/data"))
282+
if (path.contains("..") || path.startsWith("/data"))
283283
throw new Exception();
284284
}
285285

0 commit comments

Comments
 (0)