1
1
/**
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
4
4
* may cause file information disclosure.
5
5
* @kind path-problem
6
6
* @problem.severity error
7
7
* @precision high
8
- * @id java/unsafe-url-forward
8
+ * @id java/unsafe-url-forward-dispatch
9
9
* @tags security
10
10
* external/cwe-552
11
11
*/
@@ -14,20 +14,137 @@ import java
14
14
import UnsafeUrlForward
15
15
import semmle.code.java.dataflow.FlowSources
16
16
import semmle.code.java.frameworks.Servlets
17
+ import semmle.code.java.controlflow.Guards
17
18
import DataFlow:: PathGraph
18
19
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
+ }
25
70
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
+ )
28
95
}
29
96
}
30
97
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
+
31
148
class UnsafeUrlForwardFlowConfig extends TaintTracking:: Configuration {
32
149
UnsafeUrlForwardFlowConfig ( ) { this = "UnsafeUrlForwardFlowConfig" }
33
150
@@ -45,11 +162,12 @@ class UnsafeUrlForwardFlowConfig extends TaintTracking::Configuration {
45
162
46
163
override predicate isSink ( DataFlow:: Node sink ) { sink instanceof UnsafeUrlForwardSink }
47
164
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
50
170
}
51
-
52
- override predicate isSanitizer ( DataFlow:: Node node ) { node instanceof UnsafeUrlForwardSanitizer }
53
171
}
54
172
55
173
from DataFlow:: PathNode source , DataFlow:: PathNode sink , UnsafeUrlForwardFlowConfig conf
0 commit comments