22
33import java
44private import semmle.code.java.controlflow.Guards
5- private import semmle.code.java.dataflow.FlowSources
65private 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. */
910abstract 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-
4172private 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 */
5786private 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
7297private 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 */
80108private 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
90118private 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
94125private 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 */
110142private 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
125153private 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 */
149175private 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
166198private 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. */
224230class NormalizedPathNode extends DataFlow:: Node {
225231 NormalizedPathNode ( ) {
0 commit comments