Skip to content

Commit 321a2f5

Browse files
authored
Merge pull request github#11550 from atorralba/atorralba/kotlin/adapt-path-sanitizer
Kotlin: Adapt PathSanitizer
2 parents 2ed8d5d + 6dcc0cc commit 321a2f5

File tree

7 files changed

+623
-39
lines changed

7 files changed

+623
-39
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
category: minorAnalysis
3+
---
4+
* The library `PathSanitizer.qll` has been improved to detect more path validation patterns in Kotlin.
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: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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+
/** Gets the constant string value being converted to a regex by this call. */
18+
string getExpressionString() {
19+
result = this.getArgument(0).(CompileTimeConstantExpr).getStringValue()
20+
}
21+
}

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

Lines changed: 89 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import java
44
private import semmle.code.java.controlflow.Guards
55
private import semmle.code.java.dataflow.FlowSources
66
private import semmle.code.java.dataflow.SSA
7+
private import semmle.code.java.frameworks.kotlin.IO
8+
private import semmle.code.java.frameworks.kotlin.Text
79

810
/** A sanitizer that protects against path injection vulnerabilities. */
911
abstract class PathInjectionSanitizer extends DataFlow::Node { }
@@ -50,12 +52,14 @@ private predicate exactPathMatchGuard(Guard g, Expr e, boolean branch) {
5052
t instanceof TypeUri or
5153
t instanceof TypePath or
5254
t instanceof TypeFile or
53-
t.hasQualifiedName("android.net", "Uri")
55+
t.hasQualifiedName("android.net", "Uri") or
56+
t instanceof StringsKt or
57+
t instanceof FilesKt
5458
|
59+
e = getVisualQualifier(ma).getUnderlyingExpr() and
5560
ma.getMethod().getDeclaringType() = t and
5661
ma = g and
57-
ma.getMethod().getName() = ["equals", "equalsIgnoreCase"] and
58-
e = ma.getQualifier() and
62+
getSourceMethod(ma.getMethod()).hasName(["equals", "equalsIgnoreCase"]) and
5963
branch = true
6064
)
6165
}
@@ -86,7 +90,7 @@ private class AllowedPrefixGuard extends PathGuard instanceof MethodAccess {
8690
not isDisallowedWord(super.getAnArgument())
8791
}
8892

89-
override Expr getCheckedExpr() { result = super.getQualifier() }
93+
override Expr getCheckedExpr() { result = getVisualQualifier(this).getUnderlyingExpr() }
9094
}
9195

9296
/**
@@ -153,7 +157,7 @@ private class BlockListGuard extends PathGuard instanceof MethodAccess {
153157
isDisallowedWord(super.getAnArgument())
154158
}
155159

156-
override Expr getCheckedExpr() { result = super.getQualifier() }
160+
override Expr getCheckedExpr() { result = getVisualQualifier(this).getUnderlyingExpr() }
157161
}
158162

159163
/**
@@ -187,15 +191,31 @@ private class BlockListSanitizer extends PathInjectionSanitizer {
187191
}
188192
}
189193

194+
private class ConstantOrRegex extends Expr {
195+
ConstantOrRegex() {
196+
this instanceof CompileTimeConstantExpr or
197+
this instanceof KtToRegex
198+
}
199+
200+
string getStringValue() {
201+
result = this.(CompileTimeConstantExpr).getStringValue() or
202+
result = this.(KtToRegex).getExpressionString()
203+
}
204+
}
205+
190206
private predicate isStringPrefixMatch(MethodAccess ma) {
191-
exists(Method m | m = ma.getMethod() and m.getDeclaringType() instanceof TypeString |
192-
m.hasName("startsWith")
207+
exists(Method m, RefType t |
208+
m.getDeclaringType() = t and
209+
(t instanceof TypeString or t instanceof StringsKt) and
210+
m = ma.getMethod()
211+
|
212+
getSourceMethod(m).hasName("startsWith")
193213
or
194-
m.hasName("regionMatches") and
195-
ma.getArgument(0).(CompileTimeConstantExpr).getIntValue() = 0
214+
getSourceMethod(m).hasName("regionMatches") and
215+
getVisualArgument(ma, 0).(CompileTimeConstantExpr).getIntValue() = 0
196216
or
197217
m.hasName("matches") and
198-
not ma.getArgument(0).(CompileTimeConstantExpr).getStringValue().matches(".*%")
218+
not getVisualArgument(ma, 0).(ConstantOrRegex).getStringValue().matches(".*%")
199219
)
200220
}
201221

@@ -205,52 +225,52 @@ private predicate isStringPrefixMatch(MethodAccess ma) {
205225
private predicate isStringPartialMatch(MethodAccess ma) {
206226
isStringPrefixMatch(ma)
207227
or
208-
ma.getMethod().getDeclaringType() instanceof TypeString and
209-
ma.getMethod().hasName(["contains", "matches", "regionMatches", "indexOf", "lastIndexOf"])
228+
exists(RefType t | t = ma.getMethod().getDeclaringType() |
229+
t instanceof TypeString or t instanceof StringsKt
230+
) and
231+
getSourceMethod(ma.getMethod())
232+
.hasName(["contains", "matches", "regionMatches", "indexOf", "lastIndexOf"])
210233
}
211234

212235
/**
213236
* Holds if `ma` is a call to a method that checks whether a path starts with a prefix.
214237
*/
215238
private predicate isPathPrefixMatch(MethodAccess ma) {
216-
exists(RefType t |
217-
t instanceof TypePath
218-
or
219-
t.hasQualifiedName("kotlin.io", "FilesKt")
220-
|
221-
t = ma.getMethod().getDeclaringType() and
222-
ma.getMethod().hasName("startsWith")
223-
)
239+
exists(RefType t | t = ma.getMethod().getDeclaringType() |
240+
t instanceof TypePath or t instanceof FilesKt
241+
) and
242+
getSourceMethod(ma.getMethod()).hasName("startsWith")
224243
}
225244

226-
private predicate isDisallowedWord(CompileTimeConstantExpr word) {
245+
private predicate isDisallowedWord(ConstantOrRegex word) {
227246
word.getStringValue().matches(["/", "\\", "%WEB-INF%", "%/data%"])
228247
}
229248

230249
/** A complementary guard that protects against path traversal, by looking for the literal `..`. */
231250
private class PathTraversalGuard extends PathGuard {
251+
Expr checkedExpr;
252+
232253
PathTraversalGuard() {
233-
exists(MethodAccess ma |
234-
ma.getMethod().getDeclaringType() instanceof TypeString and
254+
exists(MethodAccess ma, Method m, RefType t |
255+
m = ma.getMethod() and
256+
t = m.getDeclaringType() and
257+
(t instanceof TypeString or t instanceof StringsKt) and
258+
checkedExpr = getVisualQualifier(ma).getUnderlyingExpr() and
235259
ma.getAnArgument().(CompileTimeConstantExpr).getStringValue() = ".."
236260
|
237261
this = ma and
238-
ma.getMethod().hasName("contains")
262+
getSourceMethod(m).hasName("contains")
239263
or
240264
exists(EqualityTest eq |
241265
this = eq and
242-
ma.getMethod().hasName(["indexOf", "lastIndexOf"]) and
266+
getSourceMethod(m).hasName(["indexOf", "lastIndexOf"]) and
243267
eq.getAnOperand() = ma and
244268
eq.getAnOperand().(CompileTimeConstantExpr).getIntValue() = -1
245269
)
246270
)
247271
}
248272

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

255275
boolean getBranch() {
256276
this instanceof MethodAccess and result = false
@@ -262,15 +282,46 @@ private class PathTraversalGuard extends PathGuard {
262282
/** A complementary sanitizer that protects against path traversal using path normalization. */
263283
private class PathNormalizeSanitizer extends MethodAccess {
264284
PathNormalizeSanitizer() {
265-
exists(RefType t |
266-
t instanceof TypePath or
267-
t.hasQualifiedName("kotlin.io", "FilesKt")
268-
|
269-
this.getMethod().getDeclaringType() = t and
285+
exists(RefType t | this.getMethod().getDeclaringType() = t |
286+
(t instanceof TypePath or t instanceof FilesKt) and
270287
this.getMethod().hasName("normalize")
288+
or
289+
t instanceof TypeFile and
290+
this.getMethod().hasName(["getCanonicalPath", "getCanonicalFile"])
271291
)
272-
or
273-
this.getMethod().getDeclaringType() instanceof TypeFile and
274-
this.getMethod().hasName(["getCanonicalPath", "getCanonicalFile"])
275292
}
276293
}
294+
295+
/**
296+
* Gets the qualifier of `ma` as seen in the source code.
297+
* This is a helper predicate to solve discrepancies between
298+
* what `getQualifier` actually gets in Java and Kotlin.
299+
*/
300+
private Expr getVisualQualifier(MethodAccess ma) {
301+
if getSourceMethod(ma.getMethod()) instanceof ExtensionMethod
302+
then result = ma.getArgument(0)
303+
else result = ma.getQualifier()
304+
}
305+
306+
/**
307+
* Gets the argument of `ma` at position `argPos` as seen in the source code.
308+
* This is a helper predicate to solve discrepancies between
309+
* what `getArgument` actually gets in Java and Kotlin.
310+
*/
311+
bindingset[argPos]
312+
private Argument getVisualArgument(MethodAccess ma, int argPos) {
313+
if getSourceMethod(ma.getMethod()) instanceof ExtensionMethod
314+
then result = ma.getArgument(argPos + 1)
315+
else result = ma.getArgument(argPos)
316+
}
317+
318+
/**
319+
* Gets the proxied method if `m` is a Kotlin proxy that supplies default parameter values.
320+
* Otherwise, just gets `m`.
321+
*/
322+
private Method getSourceMethod(Method m) {
323+
m = result.getKotlinParameterDefaultsProxy()
324+
or
325+
not exists(Method src | m = src.getKotlinParameterDefaultsProxy()) and
326+
result = m
327+
}

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)