@@ -4,6 +4,8 @@ import java
44private import semmle.code.java.controlflow.Guards
55private import semmle.code.java.dataflow.FlowSources
66private 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. */
911abstract 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+
190206private 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) {
205225private 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 */
215238private 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 `..`. */
231250private 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. */
263283private 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+ }
0 commit comments