Skip to content

Commit bdd135d

Browse files
committed
Spring HTTP: mark explicitly content-typed body calls as sinks
Previously only the return from the request-handler method constituted a sink, and was filtered by the Produces annotation if any, even though a BodyBuilder could explicitly override. These sinks are also marked as out-barriers to avoid duplicate paths when the Produces annotation is in agreement.
1 parent 701d0bc commit bdd135d

File tree

2 files changed

+31
-5
lines changed

2 files changed

+31
-5
lines changed

java/ql/lib/semmle/code/java/frameworks/spring/SpringHttp.qll

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -266,24 +266,32 @@ private predicate isXssSafeContentTypeExpr(Expr e) {
266266
XSS::isXssSafeContentType(getSpringConstantContentType(e))
267267
}
268268

269-
private DataFlow::Node getASanitizedBodyBuilder() {
269+
private DataFlow::Node getABodyBuilderWithExplicitContentType(Expr contentType) {
270270
result.asExpr() =
271271
any(MethodAccess ma |
272272
ma.getCallee()
273273
.hasQualifiedName("org.springframework.http", "ResponseEntity<>$BodyBuilder",
274274
"contentType") and
275-
isXssSafeContentTypeExpr(ma.getArgument(0))
275+
contentType = ma.getArgument(0)
276276
)
277277
or
278278
result.asExpr() =
279279
any(MethodAccess ma |
280-
ma.getQualifier() = getASanitizedBodyBuilder().asExpr() and
280+
ma.getQualifier() = getABodyBuilderWithExplicitContentType(contentType).asExpr() and
281281
ma.getType()
282282
.(RefType)
283283
.hasQualifiedName("org.springframework.http", "ResponseEntity<>$BodyBuilder")
284284
)
285285
or
286-
DataFlow::localFlow(getASanitizedBodyBuilder(), result)
286+
DataFlow::localFlow(getABodyBuilderWithExplicitContentType(contentType), result)
287+
}
288+
289+
private DataFlow::Node getASanitizedBodyBuilder() {
290+
result = getABodyBuilderWithExplicitContentType(any(Expr e | isXssSafeContentTypeExpr(e)))
291+
}
292+
293+
private DataFlow::Node getAVulnerableBodyBuilder() {
294+
result = getABodyBuilderWithExplicitContentType(any(Expr e | isXssVulnerableContentTypeExpr(e)))
287295
}
288296

289297
private class SanitizedBodyCall extends XSS::XssSanitizer {
@@ -295,3 +303,21 @@ private class SanitizedBodyCall extends XSS::XssSanitizer {
295303
).getArgument(0)
296304
}
297305
}
306+
307+
/**
308+
* Mark BodyBuilder.body calls with an explicitly vulnerable Content-Type as themselves sinks,
309+
* as the eventual return site from a RequestHandler may have a benign @Produces annotation that
310+
* would otherwise sanitise the result.
311+
*
312+
* Note these are SinkBarriers so that a return from a RequestHandlerMethod is not also flagged
313+
* for the same path.
314+
*/
315+
private class ExplicitlyVulnerableBodyArgument extends XSS::XssSinkBarrier {
316+
ExplicitlyVulnerableBodyArgument() {
317+
this.asExpr() =
318+
any(MethodAccess ma |
319+
ma.getQualifier() = getAVulnerableBodyBuilder().asExpr() and
320+
ma.getCallee().hasName("body")
321+
).getArgument(0)
322+
}
323+
}

java/ql/test/query-tests/security/CWE-079/semmle/tests/SpringXSS.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ public static ResponseEntity<String> methodContentTypeMaybeSafe(String userContr
7575

7676
@GetMapping(value = "/xyz", produces = MediaType.APPLICATION_JSON_VALUE)
7777
public static ResponseEntity<String> methodContentTypeSafeOverriddenWithUnsafe(String userControlled) {
78-
return ResponseEntity.ok().contentType(MediaType.TEXT_HTML).body(userControlled); // $MISSING: xss
78+
return ResponseEntity.ok().contentType(MediaType.TEXT_HTML).body(userControlled); // $xss
7979
}
8080

8181
@GetMapping(value = "/xyz", produces = MediaType.TEXT_HTML_VALUE)

0 commit comments

Comments
 (0)