Skip to content

Commit 911a61d

Browse files
Jami CogswellJami Cogswell
authored andcommitted
Java: initial update of barrier and test cases to remove FN
1 parent c8ec301 commit 911a61d

File tree

4 files changed

+237
-20
lines changed

4 files changed

+237
-20
lines changed

java/ql/lib/semmle/code/java/security/PathSanitizer.qll

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ private predicate exactPathMatchGuard(Guard g, Expr e, boolean branch) {
6464
)
6565
}
6666

67-
private class ExactPathMatchSanitizer extends PathInjectionSanitizer {
67+
// TODO: switch back to private if possible
68+
class ExactPathMatchSanitizer extends PathInjectionSanitizer {
6869
ExactPathMatchSanitizer() {
6970
this = DataFlow::BarrierGuard<exactPathMatchGuard/3>::getABarrierNode()
7071
or
@@ -151,7 +152,8 @@ private class DotDotCheckSanitizer extends PathInjectionSanitizer {
151152
}
152153
}
153154

154-
private class BlockListGuard extends PathGuard instanceof MethodCall {
155+
// TODO: switch back to private if possible
156+
class BlockListGuard extends PathGuard instanceof MethodCall {
155157
BlockListGuard() {
156158
(isStringPartialMatch(this) or isPathPrefixMatch(this)) and
157159
isDisallowedWord(super.getAnArgument())
@@ -228,6 +230,7 @@ private predicate isStringPartialMatch(MethodCall ma) {
228230
exists(RefType t | t = ma.getMethod().getDeclaringType() |
229231
t instanceof TypeString or t instanceof StringsKt
230232
) and
233+
// TODO ! Why not use `StringPartialMatchMethod` for the below?
231234
getSourceMethod(ma.getMethod())
232235
.hasName(["contains", "matches", "regionMatches", "indexOf", "lastIndexOf"])
233236
}

java/ql/lib/semmle/code/java/security/UrlForward.qll

Lines changed: 109 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import java
44
private import semmle.code.java.dataflow.ExternalFlow
55
private import semmle.code.java.dataflow.FlowSources
66
private import semmle.code.java.dataflow.StringPrefixes
7+
private import semmle.code.java.security.PathSanitizer
8+
private import semmle.code.java.dataflow.DataFlow
9+
private import semmle.code.java.controlflow.Guards
710

811
/** A URL forward sink. */
912
abstract class UrlForwardSink extends DataFlow::Node { }
@@ -44,16 +47,121 @@ private class PrimitiveBarrier extends UrlForwardBarrier {
4447
}
4548
}
4649

50+
// TODO: should this also take URL encoding/decoding into account?
51+
// TODO: and PathSanitization in general?
4752
private class FollowsBarrierPrefix extends UrlForwardBarrier {
4853
FollowsBarrierPrefix() { this.asExpr() = any(BarrierPrefix fp).getAnAppendedExpression() }
4954
}
5055

5156
private class BarrierPrefix extends InterestingPrefix {
5257
BarrierPrefix() {
53-
// TODO: why not META-INF here as well? (and are `/` correct?)
5458
not this.getStringValue().matches("/WEB-INF/%") and
5559
not this.getStringValue() = "forward:"
5660
}
5761

5862
override int getOffset() { result = 0 }
5963
}
64+
65+
private class UrlPathBarrier extends UrlForwardBarrier {
66+
UrlPathBarrier() {
67+
this instanceof PathInjectionSanitizer and
68+
(
69+
this instanceof ExactPathMatchSanitizer //TODO: still need a better solution for this edge case...
70+
or
71+
// TODO: these don't enforce order of checks and PathSanitization... make bypass test cases.
72+
this instanceof NoEncodingBarrier
73+
or
74+
this instanceof FullyDecodesBarrier
75+
)
76+
}
77+
}
78+
79+
abstract class UrlDecodeCall extends MethodCall { }
80+
81+
private class DefaultUrlDecodeCall extends UrlDecodeCall {
82+
DefaultUrlDecodeCall() {
83+
this.getMethod().hasQualifiedName("java.net", "URLDecoder", "decode") or // TODO: reuse existing class? Or make this a class?
84+
this.getMethod().hasQualifiedName("org.eclipse.jetty.util.URIUtil", "URIUtil", "decodePath")
85+
}
86+
}
87+
88+
// TODO: this can probably be named/designed better...
89+
abstract class RepeatedStmt extends Stmt { }
90+
91+
private class DefaultRepeatedStmt extends RepeatedStmt {
92+
DefaultRepeatedStmt() { this instanceof LoopStmt }
93+
}
94+
95+
abstract class CheckEncodingCall extends MethodCall { }
96+
97+
private class DefaultCheckEncodingCall extends CheckEncodingCall {
98+
DefaultCheckEncodingCall() {
99+
// TODO: indexOf?, etc.
100+
this.getMethod().hasQualifiedName("java.lang", "String", "contains") and // TODO: reuse existing class? Or make this a class?
101+
this.getArgument(0).(CompileTimeConstantExpr).getStringValue() = "%"
102+
}
103+
}
104+
105+
// TODO: better naming?
106+
// TODO: check if any URL decoding implementations _fully_ decode... or if all need to be called in a loop?
107+
// TODO: make this extendable instead of `RepeatedStmt`?
108+
private class RepeatedUrlDecodeCall extends MethodCall {
109+
RepeatedUrlDecodeCall() {
110+
this instanceof UrlDecodeCall and
111+
this.getAnEnclosingStmt() instanceof RepeatedStmt
112+
}
113+
}
114+
115+
private class CheckEncodingGuard extends Guard instanceof MethodCall {
116+
CheckEncodingGuard() { this instanceof CheckEncodingCall }
117+
118+
Expr getCheckedExpr() { result = this.(MethodCall).getQualifier() }
119+
}
120+
121+
private predicate noEncodingGuard(Guard g, Expr e, boolean branch) {
122+
g instanceof CheckEncodingGuard and
123+
e = g.(CheckEncodingGuard).getCheckedExpr() and
124+
branch = false
125+
or
126+
// branch = false and
127+
// g instanceof AssignExpr and // AssignExpr
128+
// exists(CheckEncodingCall call | g.(AssignExpr).getSource() = call | e = call.getQualifier())
129+
branch = false and
130+
g.(Expr).getType() instanceof BooleanType and // AssignExpr
131+
(
132+
exists(CheckEncodingCall call, AssignExpr ae |
133+
ae.getSource() = call and
134+
e = call.getQualifier() and
135+
g = ae.getDest()
136+
)
137+
or
138+
exists(CheckEncodingCall call, LocalVariableDeclExpr vde |
139+
vde.getInitOrPatternSource() = call and
140+
e = call.getQualifier() and
141+
g = vde.getAnAccess() //and
142+
//vde.getVariable() = g
143+
)
144+
)
145+
}
146+
147+
// TODO: check edge case of !contains(%), make sure that behaves as expected at least.
148+
private class NoEncodingBarrier extends DataFlow::Node {
149+
NoEncodingBarrier() { this = DataFlow::BarrierGuard<noEncodingGuard/3>::getABarrierNode() }
150+
}
151+
152+
private predicate fullyDecodesGuard(Expr e) {
153+
exists(CheckEncodingGuard g, RepeatedUrlDecodeCall decodeCall |
154+
e = g.getCheckedExpr() and
155+
g.controls(decodeCall.getBasicBlock(), true)
156+
)
157+
}
158+
159+
private class FullyDecodesBarrier extends DataFlow::Node {
160+
FullyDecodesBarrier() {
161+
exists(Variable v, Expr e | this.asExpr() = v.getAnAccess() |
162+
fullyDecodesGuard(e) and
163+
e = v.getAnAccess() and
164+
e.getBasicBlock().bbDominates(this.asExpr().getBasicBlock())
165+
)
166+
}
167+
}

java/ql/lib/semmle/code/java/security/UrlForwardQuery.qll

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,7 @@ module UrlForwardFlowConfig implements DataFlow::ConfigSig {
2525

2626
predicate isSink(DataFlow::Node sink) { sink instanceof UrlForwardSink }
2727

28-
predicate isBarrier(DataFlow::Node node) {
29-
node instanceof UrlForwardBarrier or
30-
node instanceof PathInjectionSanitizer
31-
}
28+
predicate isBarrier(DataFlow::Node node) { node instanceof UrlForwardBarrier }
3229

3330
DataFlow::FlowFeature getAFeature() { result instanceof DataFlow::FeatureHasSourceCallContext }
3431
}

java/ql/test/query-tests/security/CWE-552/UrlForwardTest.java

Lines changed: 122 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,9 @@ public void doFilter2(ServletRequest request, ServletResponse response, FilterCh
112112
throws IOException, ServletException {
113113
String path = ((HttpServletRequest) request).getServletPath();
114114

115+
// actually BAD since could potentially bypass with ".." encoded as "%2e%2e"?
115116
if (path.startsWith(BASE_PATH) && !path.contains("..")) {
116-
request.getRequestDispatcher(path).forward(request, response);
117+
request.getRequestDispatcher(path).forward(request, response); // $ hasUrlForward
117118
} else {
118119
chain.doFilter(request, response);
119120
}
@@ -124,6 +125,7 @@ public void doFilter3(ServletRequest request, ServletResponse response, FilterCh
124125
throws IOException, ServletException {
125126
String path = ((HttpServletRequest) request).getServletPath();
126127

128+
// this is still good, should not flag here..., url-decoding first doesn't matter if looking for exact match... :(
127129
if (path.equals("/comaction")) {
128130
request.getRequestDispatcher(path).forward(request, response);
129131
} else {
@@ -199,8 +201,9 @@ protected void doHead3(HttpServletRequest request, HttpServletResponse response)
199201
throws ServletException, IOException {
200202
String path = request.getParameter("path");
201203

204+
// actually BAD since could potentially bypass with ".." encoded as "%2e%2e"?
202205
if (path.startsWith(BASE_PATH) && !path.contains("..")) {
203-
request.getServletContext().getRequestDispatcher(path).include(request, response);
206+
request.getServletContext().getRequestDispatcher(path).include(request, response); // $ hasUrlForward
204207
}
205208
}
206209

@@ -212,45 +215,151 @@ protected void doHead4(HttpServletRequest request, HttpServletResponse response)
212215

213216
// /pages/welcome.jsp/../../WEB-INF/web.xml becomes /WEB-INF/web.xml
214217
// /pages/welcome.jsp/../../%57EB-INF/web.xml becomes /%57EB-INF/web.xml
218+
// actually BAD since could potentially bypass with ".." encoded as "%2e%2e": "/pages/welcome.jsp/%2e%2e/%2e%2e/WEB-INF/web.xml" becomes /pages/welcome.jsp/%2e%2e/%2e%2e/WEB-INF/web.xml, which will pass this check and potentially be problematic if decoded later?
215219
if (requestedPath.startsWith(BASE_PATH)) {
216-
request.getServletContext().getRequestDispatcher(requestedPath.toString()).forward(request, response);
220+
request.getServletContext().getRequestDispatcher(requestedPath.toString()).forward(request, response); // $ hasUrlForward
217221
}
218222
}
219223

220-
// FN: Request dispatcher with negation check and path normalization, but without URL decoding
221-
// When promoting this query, consider using FlowStates to make `getRequestDispatcher` a sink
222-
// only if a URL-decoding step has NOT been crossed (i.e. make URLDecoder.decode change the
223-
// state to a different value than the one required at the sink).
224-
// TODO: but does this need to take into account URLDecoder.decode in a loop...?
224+
// BAD (original FN): Request dispatcher with negation check and path normalization, but without URL decoding
225225
protected void doHead5(HttpServletRequest request, HttpServletResponse response)
226226
throws ServletException, IOException {
227227
String path = request.getParameter("path");
228228
Path requestedPath = Paths.get(BASE_PATH).resolve(path).normalize();
229229

230230
if (!requestedPath.startsWith("/WEB-INF") && !requestedPath.startsWith("/META-INF")) {
231-
request.getServletContext().getRequestDispatcher(requestedPath.toString()).forward(request, response); // $ MISSING: hasUrlForward
231+
request.getServletContext().getRequestDispatcher(requestedPath.toString()).forward(request, response); // $ hasUrlForward
232232
}
233233
}
234234

235-
// GOOD: Request dispatcher with path traversal check and URL decoding
236-
protected void doHead6(HttpServletRequest request, HttpServletResponse response)
235+
// BAD (I added to test decode with no loop): Request dispatcher with path traversal check and single URL decoding; may be vulnerable to double-encoding
236+
protected void doHead7(HttpServletRequest request, HttpServletResponse response)
237237
throws ServletException, IOException {
238238
String path = request.getParameter("path");
239+
path = URLDecoder.decode(path, "UTF-8");
240+
241+
if (!path.startsWith("/WEB-INF/") && !path.contains("..")) {
242+
request.getServletContext().getRequestDispatcher(path).include(request, response); // $ hasUrlForward
243+
}
244+
}
245+
246+
// GOOD: Request dispatcher with path traversal check and URL decoding in a loop to avoid double-encoding bypass
247+
protected void doHead6(HttpServletRequest request, HttpServletResponse response)
248+
throws ServletException, IOException {
249+
String path = request.getParameter("path"); // v
250+
251+
if (path.contains("%")){ // v.getAnAccess()
252+
while (path.contains("%")) {
253+
path = URLDecoder.decode(path, "UTF-8");
254+
}
255+
}
256+
257+
if (!path.startsWith("/WEB-INF/") && !path.contains("..")) {
258+
request.getServletContext().getRequestDispatcher(path).include(request, response);
259+
}
260+
}
261+
262+
// GOOD: Request dispatcher with path traversal check and URL decoding in a loop to avoid double-encoding bypass
263+
protected void doHead8(HttpServletRequest request, HttpServletResponse response)
264+
throws ServletException, IOException {
265+
String path = request.getParameter("path"); // v
266+
while (path.contains("%")) {
267+
path = URLDecoder.decode(path, "UTF-8");
268+
}
269+
270+
if (!path.startsWith("/WEB-INF/") && !path.contains("..")) {
271+
request.getServletContext().getRequestDispatcher(path).include(request, response);
272+
}
273+
}
274+
275+
// FP now....
276+
// GOOD: Request dispatcher with path traversal check and URL decoding in a loop to avoid double-encoding bypass
277+
protected void doHead9(HttpServletRequest request, HttpServletResponse response)
278+
throws ServletException, IOException {
279+
String path = request.getParameter("path"); // v
239280
boolean hasEncoding = path.contains("%");
240281
while (hasEncoding) {
241282
path = URLDecoder.decode(path, "UTF-8");
242283
hasEncoding = path.contains("%");
243284
}
244285

245286
if (!path.startsWith("/WEB-INF/") && !path.contains("..")) {
246-
request.getServletContext().getRequestDispatcher(path).include(request, response);
287+
request.getServletContext().getRequestDispatcher(path).include(request, response); // $ SPURIOUS: hasUrlForward
247288
}
248289
}
249290

250-
// New Tests (i.e. Added by me)
291+
// New Tests
251292
public void generateResponse(StaplerRequest req, StaplerResponse rsp, Object obj) throws IOException, ServletException {
252293
String url = req.getParameter("target");
253294
rsp.forward(obj, url, req); // $ hasUrlForward
254295
}
255296

297+
// Other Tests for edge cases:
298+
// // GOOD (I added): Request dispatcher with path traversal check and URL decoding in a loop to avoid double-encoding bypass
299+
// // testing `if` stmt requirement for BB controlling
300+
// protected void doHead12(HttpServletRequest request, HttpServletResponse response)
301+
// throws ServletException, IOException {
302+
// String path = request.getParameter("path");
303+
// if (path.contains("%")) {
304+
// while (path.contains("%")) {
305+
// path = URLDecoder.decode(path, "UTF-8");
306+
// }
307+
// }
308+
// if (!path.startsWith("/WEB-INF/") && !path.contains("..")) {
309+
// request.getServletContext().getRequestDispatcher(path).include(request, response);
310+
// }
311+
// }
312+
// // BAD (I added): Request dispatcher with path traversal check and single URL decoding; may be vulnerable to double-encoding
313+
// // Tests urlEncoding BarrierGuard "a guard that considers a string safe because it is checked for URL encoding sequences,
314+
// // having previously been checked against a block-list of forbidden values."
315+
// protected void doHead8(HttpServletRequest request, HttpServletResponse response)
316+
// throws ServletException, IOException {
317+
// String path = request.getParameter("path");
318+
319+
// if (!path.startsWith("/WEB-INF/") && !path.contains("..")) {
320+
// boolean hasEncoding = path.contains("%"); // BAD: doesn't do anything with the check...
321+
// request.getServletContext().getRequestDispatcher(path).include(request, response); // $ hasUrlForward
322+
// }
323+
// }
324+
// // BAD (I added): Request dispatcher with path traversal check and single URL decoding; may be vulnerable to double-encoding
325+
// // Tests urlEncoding BarrierGuard "a guard that considers a string safe because it is checked for URL encoding sequences,
326+
// // having previously been checked against a block-list of forbidden values."
327+
// protected void doHead9(HttpServletRequest request, HttpServletResponse response)
328+
// throws ServletException, IOException {
329+
// String path = request.getParameter("path");
330+
331+
// boolean hasEncoding = path.contains("%"); // BAD: doesn't do anything with the check... and check comes BEFORE blocklist so guard should not trigger
332+
// if (!path.startsWith("/WEB-INF/") && !path.contains("..")) {
333+
// request.getServletContext().getRequestDispatcher(path).include(request, response); // $ hasUrlForward
334+
// }
335+
// }
336+
337+
// // BAD (I added): Request dispatcher with path traversal check and single URL decoding; may be vulnerable to double-encoding
338+
// // Tests urlEncoding BarrierGuard "a guard that considers a string safe because it is checked for URL encoding sequences,
339+
// // having previously been checked against a block-list of forbidden values."
340+
// protected void doHead10(HttpServletRequest request, HttpServletResponse response)
341+
// throws ServletException, IOException {
342+
// String path = request.getParameter("path");
343+
344+
// if (!path.startsWith("/WEB-INF/") && !path.contains("..")) {
345+
// if (path.contains("%")){ // BAD: wrong check
346+
// request.getServletContext().getRequestDispatcher(path).include(request, response); // $ hasUrlForward
347+
// }
348+
// }
349+
// }
350+
351+
// // "GOOD" (I added): Request dispatcher with path traversal check and single URL decoding; may be vulnerable to double-encoding
352+
// // Tests urlEncoding BarrierGuard "a guard that considers a string safe because it is checked for URL encoding sequences,
353+
// // having previously been checked against a block-list of forbidden values."
354+
// protected void doHead11(HttpServletRequest request, HttpServletResponse response)
355+
// throws ServletException, IOException {
356+
// String path = request.getParameter("path");
357+
358+
// if (!path.startsWith("/WEB-INF/") && !path.contains("..")) {
359+
// if (!path.contains("%")){ // GOOD: right check
360+
// request.getServletContext().getRequestDispatcher(path).include(request, response);
361+
// }
362+
// }
363+
// }
364+
256365
}

0 commit comments

Comments
 (0)