11/**
2- * @name Unsafe url forward from remote source
3- * @description URL forward based on unvalidated user-input
2+ * @name Unsafe url forward or dispatch from remote source
3+ * @description URL forward or dispatch based on unvalidated user-input
44 * may cause file information disclosure.
55 * @kind path-problem
66 * @problem.severity error
77 * @precision high
8- * @id java/unsafe-url-forward
8+ * @id java/unsafe-url-forward-dispatch
99 * @tags security
1010 * external/cwe-552
1111 */
@@ -14,20 +14,137 @@ import java
1414import UnsafeUrlForward
1515import semmle.code.java.dataflow.FlowSources
1616import semmle.code.java.frameworks.Servlets
17+ import semmle.code.java.controlflow.Guards
1718import DataFlow:: PathGraph
1819
19- private class StartsWithSanitizer extends DataFlow:: BarrierGuard {
20- StartsWithSanitizer ( ) {
21- this .( MethodAccess ) .getMethod ( ) .hasName ( "startsWith" ) and
22- this .( MethodAccess ) .getMethod ( ) .getDeclaringType ( ) instanceof TypeString and
23- this .( MethodAccess ) .getMethod ( ) .getNumberOfParameters ( ) = 1
24- }
20+ /**
21+ * Holds if `ma` is a method call of matching with a path string, probably a whitelisted one.
22+ */
23+ predicate isStringPathMatch ( MethodAccess ma ) {
24+ ma .getMethod ( ) .getDeclaringType ( ) instanceof TypeString and
25+ ma .getMethod ( ) .getName ( ) = [ "startsWith" , "matches" , "regionMatches" ]
26+ }
27+
28+ /**
29+ * Holds if `ma` is a method call of `java.nio.file.Path` which matches with another
30+ * path, probably a whitelisted one.
31+ */
32+ predicate isFilePathMatch ( MethodAccess ma ) {
33+ ma .getMethod ( ) .getDeclaringType ( ) instanceof TypePath and
34+ ma .getMethod ( ) .getName ( ) = "startsWith"
35+ }
36+
37+ /**
38+ * Holds if `ma` is a method call that checks an input doesn't match using the `!`
39+ * logical negation expression.
40+ */
41+ predicate checkNoPathMatch ( MethodAccess ma ) {
42+ exists ( LogNotExpr lne |
43+ ( isStringPathMatch ( ma ) or isFilePathMatch ( ma ) ) and
44+ lne .getExpr ( ) = ma
45+ )
46+ }
47+
48+ /**
49+ * Holds if `ma` is a method call to check special characters `..` used in path traversal.
50+ */
51+ predicate isPathTraversalCheck ( MethodAccess ma ) {
52+ ma .getMethod ( ) .getDeclaringType ( ) instanceof TypeString and
53+ ma .getMethod ( ) .hasName ( [ "contains" , "indexOf" ] ) and
54+ ma .getAnArgument ( ) .( CompileTimeConstantExpr ) .getStringValue ( ) = ".."
55+ }
56+
57+ /**
58+ * Holds if `ma` is a method call to decode a url string or check url encoding.
59+ */
60+ predicate isPathDecoding ( MethodAccess ma ) {
61+ // Search the special character `%` used in url encoding
62+ ma .getMethod ( ) .getDeclaringType ( ) instanceof TypeString and
63+ ma .getMethod ( ) .hasName ( [ "contains" , "indexOf" ] ) and
64+ ma .getAnArgument ( ) .( CompileTimeConstantExpr ) .getStringValue ( ) = "%"
65+ or
66+ // Call to `URLDecoder` assuming the implementation handles double encoding correctly
67+ ma .getMethod ( ) .getDeclaringType ( ) .hasQualifiedName ( "java.net" , "URLDecoder" ) and
68+ ma .getMethod ( ) .hasName ( "decode" )
69+ }
2570
26- override predicate checks ( Expr e , boolean branch ) {
27- e = this .( MethodAccess ) .getQualifier ( ) and branch = true
71+ private class PathMatchSanitizer extends DataFlow:: Node {
72+ PathMatchSanitizer ( ) {
73+ exists ( MethodAccess ma |
74+ (
75+ isStringPathMatch ( ma ) and
76+ exists ( MethodAccess ma2 |
77+ isPathTraversalCheck ( ma2 ) and
78+ ma .getQualifier ( ) .( VarAccess ) .getVariable ( ) .getAnAccess ( ) = ma2 .getQualifier ( )
79+ )
80+ or
81+ isFilePathMatch ( ma )
82+ ) and
83+ (
84+ not checkNoPathMatch ( ma )
85+ or
86+ // non-match check needs decoding e.g. !path.startsWith("/WEB-INF/") won't detect /%57EB-INF/web.xml, which will be decoded and served by RequestDispatcher
87+ checkNoPathMatch ( ma ) and
88+ exists ( MethodAccess ma2 |
89+ isPathDecoding ( ma2 ) and
90+ ma .getQualifier ( ) .( VarAccess ) .getVariable ( ) .getAnAccess ( ) = ma2 .getQualifier ( )
91+ )
92+ ) and
93+ this .asExpr ( ) = ma .getQualifier ( )
94+ )
2895 }
2996}
3097
98+ /**
99+ * Holds if `ma` is a method call to check string content, which means an input string is not
100+ * blindly trusted and helps to reduce FPs.
101+ */
102+ predicate checkStringContent ( MethodAccess ma , Expr expr ) {
103+ ma .getMethod ( ) .getDeclaringType ( ) instanceof TypeString and
104+ ma .getMethod ( )
105+ .hasName ( [
106+ "charAt" , "contains" , "equals" , "equalsIgnoreCase" , "getBytes" , "getChars" , "indexOf" ,
107+ "lastIndexOf" , "length" , "matches" , "regionMatches" , "replace" , "replaceAll" ,
108+ "replaceFirst" , "substring"
109+ ] ) and
110+ expr = ma .getQualifier ( )
111+ or
112+ (
113+ ma .getMethod ( ) .getDeclaringType ( ) instanceof TypeStringBuffer or
114+ ma .getMethod ( ) .getDeclaringType ( ) instanceof TypeStringBuilder
115+ ) and
116+ expr = ma .getAnArgument ( )
117+ }
118+
119+ private class StringOperationSanitizer extends DataFlow:: Node {
120+ StringOperationSanitizer ( ) { exists ( MethodAccess ma | checkStringContent ( ma , this .asExpr ( ) ) ) }
121+ }
122+
123+ /**
124+ * Holds if `expr` is an expression returned from null or empty string check.
125+ */
126+ predicate isNullOrEmptyCheck ( Expr expr ) {
127+ exists ( ConditionBlock cb , ReturnStmt rt |
128+ cb .controls ( rt .getBasicBlock ( ) , true ) and
129+ (
130+ cb .getCondition ( ) .( EQExpr ) .getAnOperand ( ) instanceof NullLiteral // if (path == null)
131+ or
132+ // if (path.equals(""))
133+ exists ( MethodAccess ma |
134+ cb .getCondition ( ) = ma and
135+ ma .getMethod ( ) .getDeclaringType ( ) instanceof TypeString and
136+ ma .getMethod ( ) .hasName ( "equals" ) and
137+ ma .getArgument ( 0 ) .( CompileTimeConstantExpr ) .getStringValue ( ) = ""
138+ )
139+ ) and
140+ expr .getParent + ( ) = rt
141+ )
142+ }
143+
144+ private class NullOrEmptyCheckSanitizer extends DataFlow:: Node {
145+ NullOrEmptyCheckSanitizer ( ) { isNullOrEmptyCheck ( this .asExpr ( ) ) }
146+ }
147+
31148class UnsafeUrlForwardFlowConfig extends TaintTracking:: Configuration {
32149 UnsafeUrlForwardFlowConfig ( ) { this = "UnsafeUrlForwardFlowConfig" }
33150
@@ -45,11 +162,12 @@ class UnsafeUrlForwardFlowConfig extends TaintTracking::Configuration {
45162
46163 override predicate isSink ( DataFlow:: Node sink ) { sink instanceof UnsafeUrlForwardSink }
47164
48- override predicate isSanitizerGuard ( DataFlow:: BarrierGuard guard ) {
49- guard instanceof StartsWithSanitizer
165+ override predicate isSanitizer ( DataFlow:: Node node ) {
166+ node instanceof UnsafeUrlForwardSanitizer or
167+ node instanceof PathMatchSanitizer or
168+ node instanceof StringOperationSanitizer or
169+ node instanceof NullOrEmptyCheckSanitizer
50170 }
51-
52- override predicate isSanitizer ( DataFlow:: Node node ) { node instanceof UnsafeUrlForwardSanitizer }
53171}
54172
55173from DataFlow:: PathNode source , DataFlow:: PathNode sink , UnsafeUrlForwardFlowConfig conf
0 commit comments