Skip to content

Commit 81feaae

Browse files
committed
Refactor PathMatchGuard
1 parent cd9a485 commit 81feaae

File tree

3 files changed

+72
-102
lines changed

3 files changed

+72
-102
lines changed

java/ql/src/experimental/Security/CWE/CWE-552/UnsafeUrlForward.ql

Lines changed: 57 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,15 @@ import semmle.code.java.dataflow.NullGuards
1919
import DataFlow::PathGraph
2020

2121
/**
22-
* Holds if `ma` is a call to a method that checks exact match of string, probably a whitelisted one.
22+
* Holds if `ma` is a call to a method that checks exact match of string.
2323
*/
2424
predicate isExactStringPathMatch(MethodAccess ma) {
2525
ma.getMethod().getDeclaringType() instanceof TypeString and
2626
ma.getMethod().getName() = ["equals", "equalsIgnoreCase"]
2727
}
2828

2929
/**
30-
* Holds if `ma` is a call to a method that checks a path string, probably a whitelisted one.
30+
* Holds if `ma` is a call to a method that checks a path string.
3131
*/
3232
predicate isStringPathMatch(MethodAccess ma) {
3333
ma.getMethod().getDeclaringType() instanceof TypeString and
@@ -36,46 +36,44 @@ predicate isStringPathMatch(MethodAccess ma) {
3636
}
3737

3838
/**
39-
* Holds if `ma` is a call to a method of `java.nio.file.Path` that checks a path, probably
40-
* a whitelisted one.
39+
* Holds if `ma` is a call to a method of `java.nio.file.Path` that checks a path.
4140
*/
4241
predicate isFilePathMatch(MethodAccess ma) {
4342
ma.getMethod().getDeclaringType() instanceof TypePath and
4443
ma.getMethod().getName() = "startsWith"
4544
}
4645

4746
/**
48-
* Holds if `ma` is a call to a method that checks an input doesn't match using the `!`
49-
* logical negation expression.
47+
* Holds if `ma` protects against path traversal, by either:
48+
* * looking for the literal `..`
49+
* * performing path normalization
5050
*/
51-
predicate checkNoPathMatch(MethodAccess ma) {
52-
exists(LogNotExpr lne |
53-
(isStringPathMatch(ma) or isFilePathMatch(ma)) and
54-
lne.getExpr() = ma
55-
)
56-
}
57-
58-
/**
59-
* Holds if `ma` is a call to a method that checks special characters `..` used in path traversal.
60-
*/
61-
predicate isPathTraversalCheck(MethodAccess ma) {
51+
predicate isPathTraversalCheck(MethodAccess ma, Expr checked) {
6252
ma.getMethod().getDeclaringType() instanceof TypeString and
6353
ma.getMethod().hasName(["contains", "indexOf"]) and
64-
ma.getAnArgument().(CompileTimeConstantExpr).getStringValue() = ".."
54+
ma.getAnArgument().(CompileTimeConstantExpr).getStringValue() = ".." and
55+
ma.(Guard).controls(checked.getBasicBlock(), false)
56+
or
57+
ma.getMethod() instanceof PathNormalizeMethod and
58+
checked = ma
6559
}
6660

6761
/**
68-
* Holds if `ma` is a call to a method that decodes a URL string or check URL encoding.
62+
* Holds if `ma` protects against double URL encoding, by either:
63+
* * looking for the literal `%`
64+
* * performing URL decoding
6965
*/
70-
predicate isPathDecoding(MethodAccess ma) {
66+
predicate isURLEncodingCheck(MethodAccess ma, Expr checked) {
7167
// Search the special character `%` used in url encoding
7268
ma.getMethod().getDeclaringType() instanceof TypeString and
7369
ma.getMethod().hasName(["contains", "indexOf"]) and
74-
ma.getAnArgument().(CompileTimeConstantExpr).getStringValue() = "%"
70+
ma.getAnArgument().(CompileTimeConstantExpr).getStringValue() = "%" and
71+
ma.(Guard).controls(checked.getBasicBlock(), false)
7572
or
7673
// Call to `URLDecoder` assuming the implementation handles double encoding correctly
7774
ma.getMethod().getDeclaringType().hasQualifiedName("java.net", "URLDecoder") and
78-
ma.getMethod().hasName("decode")
75+
ma.getMethod().hasName("decode") and
76+
checked = ma
7977
}
8078

8179
/** The Java method `normalize` of `java.nio.file.Path`. */
@@ -86,90 +84,54 @@ class PathNormalizeMethod extends Method {
8684
}
8785
}
8886

87+
private predicate isDisallowedWord(CompileTimeConstantExpr word) {
88+
word.getStringValue().matches(["%WEB-INF%", "%META-INF%", "%..%"])
89+
}
90+
91+
private predicate isAllowListCheck(MethodAccess ma) {
92+
(isStringPathMatch(ma) or isFilePathMatch(ma)) and
93+
not isDisallowedWord(ma.getAnArgument())
94+
}
95+
96+
private predicate isDisallowListCheck(MethodAccess ma) {
97+
(isStringPathMatch(ma) or isFilePathMatch(ma)) and
98+
isDisallowedWord(ma.getAnArgument())
99+
}
100+
89101
/**
90-
* Sanitizer to check the following scenarios in a web application:
102+
* A guard that checks a path with the following methods:
91103
* 1. Exact string match
92-
* 2. String startsWith or match check with path traversal validation
93-
* 3. String not startsWith or not match check with decoding processing
94-
* 4. java.nio.file.Path startsWith check having path normalization
104+
* 2. Path matches allowed values (needs to protect against path traversal)
105+
* 3. Path matches disallowed values (needs to protect against URL encoding)
95106
*/
96107
private class PathMatchGuard extends DataFlow::BarrierGuard {
97108
PathMatchGuard() {
98-
isExactStringPathMatch(this)
99-
or
100-
isStringPathMatch(this) and
101-
not checkNoPathMatch(this) and
102-
exists(MethodAccess tma |
103-
isPathTraversalCheck(tma) and
104-
DataFlow::localExprFlow(this.(MethodAccess).getQualifier(), tma.getQualifier())
105-
)
106-
or
107-
checkNoPathMatch(this) and
108-
exists(MethodAccess dma |
109-
isPathDecoding(dma) and
110-
DataFlow::localExprFlow(dma, this.(MethodAccess).getQualifier())
111-
)
112-
or
113-
isFilePathMatch(this) and
114-
exists(MethodAccess pma |
115-
pma.getMethod() instanceof PathNormalizeMethod and
116-
DataFlow::localExprFlow(pma, this.(MethodAccess).getQualifier())
117-
)
109+
isExactStringPathMatch(this) or isStringPathMatch(this) or isFilePathMatch(this)
118110
}
119111

120112
override predicate checks(Expr e, boolean branch) {
121113
e = this.(MethodAccess).getQualifier() and
122114
(
123-
branch = true and not checkNoPathMatch(this)
115+
isExactStringPathMatch(this) and
116+
branch = true
124117
or
125-
branch = false and checkNoPathMatch(this)
126-
)
127-
}
128-
}
129-
130-
/**
131-
* Holds if `ma` is a call to a method that checks string content, which means an input string is not
132-
* blindly trusted and helps to reduce FPs.
133-
*/
134-
predicate checkStringContent(MethodAccess ma, Expr expr) {
135-
ma.getMethod().getDeclaringType() instanceof TypeString and
136-
ma.getMethod()
137-
.hasName([
138-
"charAt", "getBytes", "getChars", "length", "replace", "replaceAll", "replaceFirst",
139-
"substring"
140-
]) and
141-
expr = ma.getQualifier()
142-
or
143-
(
144-
ma.getMethod().getDeclaringType() instanceof TypeStringBuffer or
145-
ma.getMethod().getDeclaringType() instanceof TypeStringBuilder
146-
) and
147-
expr = ma.getAnArgument()
148-
}
149-
150-
private class StringOperationSanitizer extends DataFlow::Node {
151-
StringOperationSanitizer() { exists(MethodAccess ma | checkStringContent(ma, this.asExpr())) }
152-
}
153-
154-
private class NullOrEmptyCheckGuard extends DataFlow::BarrierGuard {
155-
NullOrEmptyCheckGuard() {
156-
this = nullGuard(_, _, _)
157-
or
158-
exists(MethodAccess ma |
159-
cb.getCondition() = ma and
160-
ma.getMethod().getDeclaringType() instanceof TypeString and
161-
ma.getMethod().hasName("equals") and
162-
ma.getArgument(0).(CompileTimeConstantExpr).getStringValue() = "" and
163-
this = ma
118+
isAllowListCheck(this) and
119+
exists(MethodAccess ma, Expr checked | isPathTraversalCheck(ma, checked) |
120+
DataFlow::localExprFlow(checked, e)
121+
or
122+
ma.getParent*().(BinaryExpr) = this.(MethodAccess).getParent*()
123+
) and
124+
branch = true
125+
or
126+
isDisallowListCheck(this) and
127+
exists(MethodAccess ma, Expr checked | isURLEncodingCheck(ma, checked) |
128+
DataFlow::localExprFlow(checked, e)
129+
or
130+
ma.getParent*().(BinaryExpr) = this.(MethodAccess).getParent*()
131+
) and
132+
branch = false
164133
)
165134
}
166-
167-
override predicate checks(Expr e, boolean branch) {
168-
exists(SsaVariable ssa | this = nullGuard(ssa, branch, true) and e = ssa.getAFirstUse())
169-
or
170-
e = this.(MethodAccess).getQualifier() and
171-
branch = true
172-
}
173135
}
174136

175137
class UnsafeUrlForwardFlowConfig extends TaintTracking::Configuration {
@@ -189,14 +151,10 @@ class UnsafeUrlForwardFlowConfig extends TaintTracking::Configuration {
189151

190152
override predicate isSink(DataFlow::Node sink) { sink instanceof UnsafeUrlForwardSink }
191153

192-
override predicate isSanitizer(DataFlow::Node node) {
193-
node instanceof UnsafeUrlForwardSanitizer or
194-
node instanceof StringOperationSanitizer
195-
}
154+
override predicate isSanitizer(DataFlow::Node node) { node instanceof UnsafeUrlForwardSanitizer }
196155

197156
override predicate isSanitizerGuard(DataFlow::BarrierGuard guard) {
198-
guard instanceof PathMatchGuard or
199-
guard instanceof NullOrEmptyCheckGuard
157+
guard instanceof PathMatchGuard
200158
}
201159

202160
override DataFlow::FlowFeature getAFeature() {

java/ql/test/experimental/query-tests/security/CWE-552/UnsafeServletRequestDispatch.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ protected void doHead2(HttpServletRequest request, HttpServletResponse response)
8080
// GOOD: Request dispatcher with path traversal check
8181
protected void doHead3(HttpServletRequest request, HttpServletResponse response)
8282
throws ServletException, IOException {
83-
String path = request.getParameter("path");
83+
String path = request.getParameter("path");
8484

8585
if (path.startsWith(BASE_PATH) && !path.contains("..")) {
8686
request.getServletContext().getRequestDispatcher(path).include(request, response);
@@ -100,7 +100,7 @@ protected void doHead4(HttpServletRequest request, HttpServletResponse response)
100100
}
101101
}
102102

103-
// GOOD: Request dispatcher with negation check and path normalization
103+
// BAD: Request dispatcher with negation check and path normalization, but without URL decoding
104104
protected void doHead5(HttpServletRequest request, HttpServletResponse response)
105105
throws ServletException, IOException {
106106
String path = request.getParameter("path");
@@ -111,7 +111,7 @@ protected void doHead5(HttpServletRequest request, HttpServletResponse response)
111111
}
112112
}
113113

114-
// GOOD: Request dispatcher with path traversal check and url decoding
114+
// GOOD: Request dispatcher with path traversal check and URL decoding
115115
protected void doHead6(HttpServletRequest request, HttpServletResponse response)
116116
throws ServletException, IOException {
117117
String path = request.getParameter("path");

java/ql/test/experimental/query-tests/security/CWE-552/UnsafeUrlForward.expected

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ edges
33
| UnsafeServletRequestDispatch.java:23:22:23:54 | getParameter(...) : String | UnsafeServletRequestDispatch.java:32:51:32:59 | returnURL |
44
| UnsafeServletRequestDispatch.java:42:22:42:54 | getParameter(...) : String | UnsafeServletRequestDispatch.java:48:56:48:64 | returnURL |
55
| UnsafeServletRequestDispatch.java:71:17:71:44 | getParameter(...) : String | UnsafeServletRequestDispatch.java:76:53:76:56 | path |
6+
| UnsafeServletRequestDispatch.java:106:17:106:44 | getParameter(...) : String | UnsafeServletRequestDispatch.java:107:53:107:56 | path : String |
7+
| UnsafeServletRequestDispatch.java:107:24:107:57 | resolve(...) : Path | UnsafeServletRequestDispatch.java:107:24:107:69 | normalize(...) : Path |
8+
| UnsafeServletRequestDispatch.java:107:24:107:69 | normalize(...) : Path | UnsafeServletRequestDispatch.java:110:53:110:65 | requestedPath : Path |
9+
| UnsafeServletRequestDispatch.java:107:53:107:56 | path : String | UnsafeServletRequestDispatch.java:107:24:107:57 | resolve(...) : Path |
10+
| UnsafeServletRequestDispatch.java:110:53:110:65 | requestedPath : Path | UnsafeServletRequestDispatch.java:110:53:110:76 | toString(...) |
611
| UnsafeUrlForward.java:13:27:13:36 | url : String | UnsafeUrlForward.java:14:27:14:29 | url |
712
| UnsafeUrlForward.java:18:27:18:36 | url : String | UnsafeUrlForward.java:20:28:20:30 | url |
813
| UnsafeUrlForward.java:25:21:25:30 | url : String | UnsafeUrlForward.java:26:23:26:25 | url |
@@ -20,6 +25,12 @@ nodes
2025
| UnsafeServletRequestDispatch.java:48:56:48:64 | returnURL | semmle.label | returnURL |
2126
| UnsafeServletRequestDispatch.java:71:17:71:44 | getParameter(...) : String | semmle.label | getParameter(...) : String |
2227
| UnsafeServletRequestDispatch.java:76:53:76:56 | path | semmle.label | path |
28+
| UnsafeServletRequestDispatch.java:106:17:106:44 | getParameter(...) : String | semmle.label | getParameter(...) : String |
29+
| UnsafeServletRequestDispatch.java:107:24:107:57 | resolve(...) : Path | semmle.label | resolve(...) : Path |
30+
| UnsafeServletRequestDispatch.java:107:24:107:69 | normalize(...) : Path | semmle.label | normalize(...) : Path |
31+
| UnsafeServletRequestDispatch.java:107:53:107:56 | path : String | semmle.label | path : String |
32+
| UnsafeServletRequestDispatch.java:110:53:110:65 | requestedPath : Path | semmle.label | requestedPath : Path |
33+
| UnsafeServletRequestDispatch.java:110:53:110:76 | toString(...) | semmle.label | toString(...) |
2334
| UnsafeUrlForward.java:13:27:13:36 | url : String | semmle.label | url : String |
2435
| UnsafeUrlForward.java:14:27:14:29 | url | semmle.label | url |
2536
| UnsafeUrlForward.java:18:27:18:36 | url : String | semmle.label | url : String |
@@ -41,6 +52,7 @@ subpaths
4152
| UnsafeServletRequestDispatch.java:32:51:32:59 | returnURL | UnsafeServletRequestDispatch.java:23:22:23:54 | getParameter(...) : String | UnsafeServletRequestDispatch.java:32:51:32:59 | returnURL | Potentially untrusted URL forward due to $@. | UnsafeServletRequestDispatch.java:23:22:23:54 | getParameter(...) | user-provided value |
4253
| UnsafeServletRequestDispatch.java:48:56:48:64 | returnURL | UnsafeServletRequestDispatch.java:42:22:42:54 | getParameter(...) : String | UnsafeServletRequestDispatch.java:48:56:48:64 | returnURL | Potentially untrusted URL forward due to $@. | UnsafeServletRequestDispatch.java:42:22:42:54 | getParameter(...) | user-provided value |
4354
| UnsafeServletRequestDispatch.java:76:53:76:56 | path | UnsafeServletRequestDispatch.java:71:17:71:44 | getParameter(...) : String | UnsafeServletRequestDispatch.java:76:53:76:56 | path | Potentially untrusted URL forward due to $@. | UnsafeServletRequestDispatch.java:71:17:71:44 | getParameter(...) | user-provided value |
55+
| UnsafeServletRequestDispatch.java:110:53:110:76 | toString(...) | UnsafeServletRequestDispatch.java:106:17:106:44 | getParameter(...) : String | UnsafeServletRequestDispatch.java:110:53:110:76 | toString(...) | Potentially untrusted URL forward due to $@. | UnsafeServletRequestDispatch.java:106:17:106:44 | getParameter(...) | user-provided value |
4456
| UnsafeUrlForward.java:14:27:14:29 | url | UnsafeUrlForward.java:13:27:13:36 | url : String | UnsafeUrlForward.java:14:27:14:29 | url | Potentially untrusted URL forward due to $@. | UnsafeUrlForward.java:13:27:13:36 | url | user-provided value |
4557
| UnsafeUrlForward.java:20:28:20:30 | url | UnsafeUrlForward.java:18:27:18:36 | url : String | UnsafeUrlForward.java:20:28:20:30 | url | Potentially untrusted URL forward due to $@. | UnsafeUrlForward.java:18:27:18:36 | url | user-provided value |
4658
| UnsafeUrlForward.java:26:23:26:25 | url | UnsafeUrlForward.java:25:21:25:30 | url : String | UnsafeUrlForward.java:26:23:26:25 | url | Potentially untrusted URL forward due to $@. | UnsafeUrlForward.java:25:21:25:30 | url | user-provided value |

0 commit comments

Comments
 (0)