Skip to content

Commit 856046c

Browse files
committed
Jax-RS: implement content-type tracking
This follows content-type specifications across Variant-related functions and the ResponseBuilder class in order to sanitize or sink entities as appropriate.
1 parent 1071421 commit 856046c

File tree

2 files changed

+174
-21
lines changed
  • java/ql

2 files changed

+174
-21
lines changed

java/ql/src/semmle/code/java/frameworks/JaxWS.qll

Lines changed: 152 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,8 @@ class MessageBodyReaderRead extends Method {
284284
}
285285

286286
private string getContentTypeString(Expr e) {
287-
result = e.(CompileTimeConstantExpr).getStringValue()
287+
result = e.(CompileTimeConstantExpr).getStringValue() and
288+
result != ""
288289
or
289290
exists(Field jaxMediaType |
290291
// Accesses to static fields on `MediaType` class do not have constant strings in the database
@@ -327,8 +328,9 @@ private class JaxRSXssSink extends XssSink {
327328
|
328329
not exists(resourceMethod.getProducesAnnotation())
329330
or
330-
getContentTypeString(resourceMethod.getProducesAnnotation().getADeclaredContentTypeExpr()) =
331-
"text/plain"
331+
isXssVulnerableContentType(getContentTypeString(resourceMethod
332+
.getProducesAnnotation()
333+
.getADeclaredContentTypeExpr()))
332334
)
333335
}
334336
}
@@ -805,3 +807,150 @@ private class JaxRsUrlOpenSink extends SinkModelCsv {
805807
]
806808
}
807809
}
810+
811+
private predicate isXssVulnerableContentTypeExpr(Expr e) {
812+
isXssVulnerableContentType(getContentTypeString(e))
813+
}
814+
815+
private predicate isXssSafeContentTypeExpr(Expr e) { isXssSafeContentType(getContentTypeString(e)) }
816+
817+
/**
818+
* Gets a builder expression or related type that is configured to use the given `contentType`.
819+
*
820+
* This could be an instance of `Response.ResponseBuilder`, `Variant`, `Variant.VariantListBuilder` or
821+
* a `List<Variant>`.
822+
*
823+
* This routine is used to search forwards for response entities set after the content-type is configured.
824+
* It does not need to consider cases where the entity is set in the same call, or the entity has already
825+
* been set: these are handled by simple sanitization below.
826+
*/
827+
private DataFlow::Node getABuilderWithExplicitContentType(Expr contentType) {
828+
// Base case: ResponseBuilder.type(contentType)
829+
result.asExpr() =
830+
any(MethodAccess ma |
831+
ma.getCallee().hasQualifiedName(getAJaxRsPackage("core"), "Response$ResponseBuilder", "type") and
832+
contentType = ma.getArgument(0)
833+
)
834+
or
835+
// Base case: new Variant(contentType, ...)
836+
result.asExpr() =
837+
any(ClassInstanceExpr cie |
838+
cie.getConstructedType().hasQualifiedName(getAJaxRsPackage("core"), "Variant") and
839+
contentType = cie.getArgument(0)
840+
)
841+
or
842+
// Base case: Variant[.VariantListBuilder].mediaTypes(...)
843+
result.asExpr() =
844+
any(MethodAccess ma |
845+
ma.getCallee()
846+
.hasQualifiedName(getAJaxRsPackage("core"), ["Variant", "Variant$VariantListBuilder"],
847+
"mediaTypes") and
848+
contentType = ma.getAnArgument()
849+
)
850+
or
851+
// Recursive case: propagate through variant list building:
852+
result.asExpr() =
853+
any(MethodAccess ma |
854+
(
855+
ma.getType()
856+
.(RefType)
857+
.hasQualifiedName(getAJaxRsPackage("core"), "Variant$VariantListBuilder")
858+
or
859+
ma.getMethod()
860+
.hasQualifiedName(getAJaxRsPackage("core"), "Variant$VariantListBuilder", "build")
861+
) and
862+
[ma.getAnArgument(), ma.getQualifier()] =
863+
getABuilderWithExplicitContentType(contentType).asExpr()
864+
)
865+
or
866+
// Recursive case: propagate through a List.get operation
867+
result.asExpr() =
868+
any(MethodAccess ma |
869+
ma.getMethod().hasQualifiedName("java.util", "List<Variant>", "get") and
870+
ma.getQualifier() = getABuilderWithExplicitContentType(contentType).asExpr()
871+
)
872+
or
873+
// Recursive case: propagate through Response.ResponseBuilder operations, including the `variant(...)` operation.
874+
result.asExpr() =
875+
any(MethodAccess ma |
876+
ma.getType().(RefType).hasQualifiedName(getAJaxRsPackage("core"), "Response$ResponseBuilder") and
877+
[ma.getQualifier(), ma.getArgument(0)] =
878+
getABuilderWithExplicitContentType(contentType).asExpr()
879+
)
880+
or
881+
// Recursive case: ordinary local dataflow
882+
DataFlow::localFlow(getABuilderWithExplicitContentType(contentType), result)
883+
}
884+
885+
private DataFlow::Node getASanitizedBuilder() {
886+
result = getABuilderWithExplicitContentType(any(Expr e | isXssSafeContentTypeExpr(e)))
887+
}
888+
889+
private DataFlow::Node getAVulnerableBuilder() {
890+
result = getABuilderWithExplicitContentType(any(Expr e | isXssVulnerableContentTypeExpr(e)))
891+
}
892+
893+
/**
894+
* A response builder sanitized by setting a safe content type.
895+
*
896+
* The content type could be set before the `entity(...)` call that needs sanitizing
897+
* (e.g. `Response.ok().type("application/json").entity(sanitizeMe)`)
898+
* or at the same time (e.g. `Response.ok(sanitizeMe, "application/json")`
899+
* or the content-type could be set afterwards (e.g. `Response.ok().entity(userControlled).type("application/json")`)
900+
*
901+
* This differs from `getASanitizedBuilder` in that we also include functions that must set the entity
902+
* at the same time, or the entity must already have been set, so propagating forwards to sanitize future
903+
* build steps is not necessary.
904+
*/
905+
private class SanitizedResponseBuilder extends XssSanitizer {
906+
SanitizedResponseBuilder() {
907+
// e.g. sanitizeMe.type("application/json")
908+
this = getASanitizedBuilder()
909+
or
910+
this.asExpr() =
911+
any(MethodAccess ma |
912+
ma.getMethod().hasQualifiedName(getAJaxRsPackage("core"), "Response", "ok") and
913+
(
914+
// e.g. Response.ok(sanitizeMe, new Variant("application/json", ...))
915+
ma.getArgument(1) = getASanitizedBuilder().asExpr()
916+
or
917+
// e.g. Response.ok(sanitizeMe, "application/json")
918+
isXssSafeContentTypeExpr(ma.getArgument(1))
919+
)
920+
)
921+
}
922+
}
923+
924+
/**
925+
* An entity call that serves as a sink and barrier because it has a vulnerable content-type set.
926+
*
927+
* We flag these as direct sinks because otherwise it may be sanitized when it reaches a resource
928+
* method with a safe-looking `@Produces` annotation. They are barriers because otherwise if the
929+
* resource method does *not* have a safe-looking `@Produces` annotation then it would be doubly
930+
* reported, once at the `entity(...)` call and once on return from the resource method.
931+
*/
932+
private class VulnerableEntity extends XssSinkBarrier {
933+
VulnerableEntity() {
934+
this.asExpr() =
935+
any(MethodAccess ma |
936+
(
937+
// Vulnerable content-type already set:
938+
ma.getQualifier() = getAVulnerableBuilder().asExpr()
939+
or
940+
// Vulnerable content-type set in the future:
941+
getAVulnerableBuilder().asExpr().(MethodAccess).getQualifier*() = ma
942+
) and
943+
ma.getMethod().hasName("entity")
944+
).getArgument(0)
945+
or
946+
this.asExpr() =
947+
any(MethodAccess ma |
948+
(
949+
isXssVulnerableContentTypeExpr(ma.getArgument(1))
950+
or
951+
ma.getArgument(1) = getAVulnerableBuilder().asExpr()
952+
) and
953+
ma.getMethod().hasName("ok")
954+
).getArgument(0)
955+
}
956+
}

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

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -63,39 +63,43 @@ public static Response specificContentTypeSetterMethods(int route, boolean safeC
6363
if(safeContentType) {
6464
if(route == 0) {
6565
// via ok, as a string literal:
66-
return Response.ok(userControlled, "application/json").build(); // $SPURIOUS: xss
66+
return Response.ok(userControlled, "application/json").build();
6767
}
6868
else if(route == 1) {
6969
// via ok, as a string constant:
70-
return Response.ok(userControlled, MediaType.APPLICATION_JSON).build(); // $SPURIOUS: xss
70+
return Response.ok(userControlled, MediaType.APPLICATION_JSON).build();
7171
}
7272
else if(route == 2) {
7373
// via ok, as a MediaType constant:
74-
return Response.ok(userControlled, MediaType.APPLICATION_JSON_TYPE).build(); // $SPURIOUS: xss
74+
return Response.ok(userControlled, MediaType.APPLICATION_JSON_TYPE).build();
7575
}
7676
else if(route == 3) {
7777
// via ok, as a Variant, via constructor:
78-
return Response.ok(userControlled, new Variant(MediaType.APPLICATION_JSON_TYPE, "language", "encoding")).build(); // $SPURIOUS: xss
78+
return Response.ok(userControlled, new Variant(MediaType.APPLICATION_JSON_TYPE, "language", "encoding")).build();
7979
}
8080
else if(route == 4) {
8181
// via ok, as a Variant, via static method:
82-
return Response.ok(userControlled, Variant.mediaTypes(MediaType.APPLICATION_JSON_TYPE).build().get(0)).build(); // $SPURIOUS: xss
82+
return Response.ok(userControlled, Variant.mediaTypes(MediaType.APPLICATION_JSON_TYPE).build().get(0)).build();
83+
}
84+
else if(route == -4) {
85+
// via ok, as a Variant, via static method (testing multiple media types):
86+
return Response.ok(userControlled, Variant.mediaTypes(MediaType.APPLICATION_JSON_TYPE, MediaType.APPLICATION_OCTET_STREAM_TYPE).build().get(0)).build();
8387
}
8488
else if(route == 5) {
8589
// via ok, as a Variant, via instance method:
86-
return Response.ok(userControlled, Variant.languages(Locale.UK).mediaTypes(MediaType.APPLICATION_JSON_TYPE).build().get(0)).build(); // $SPURIOUS: xss
90+
return Response.ok(userControlled, Variant.languages(Locale.UK).mediaTypes(MediaType.APPLICATION_JSON_TYPE).build().get(0)).build();
8791
}
8892
else if(route == 6) {
8993
// via builder variant, before entity:
90-
return Response.ok().variant(new Variant(MediaType.APPLICATION_JSON_TYPE, "language", "encoding")).entity(userControlled).build(); // $SPURIOUS: xss
94+
return Response.ok().variant(new Variant(MediaType.APPLICATION_JSON_TYPE, "language", "encoding")).entity(userControlled).build();
9195
}
9296
else if(route == 7) {
9397
// via builder variant, after entity:
94-
return Response.ok().entity(userControlled).variant(new Variant(MediaType.APPLICATION_JSON_TYPE, "language", "encoding")).build(); // $SPURIOUS: xss
98+
return Response.ok().entity(userControlled).variant(new Variant(MediaType.APPLICATION_JSON_TYPE, "language", "encoding")).build();
9599
}
96100
else if(route == 8) {
97101
// provide entity via ok, then content-type via builder:
98-
return Response.ok(userControlled).type(MediaType.APPLICATION_JSON_TYPE).build(); // $SPURIOUS: xss
102+
return Response.ok(userControlled).type(MediaType.APPLICATION_JSON_TYPE).build();
99103
}
100104
}
101105
else {
@@ -158,27 +162,27 @@ public static Response methodContentTypeSafeStringLiteral(String userControlled)
158162

159163
@GET @Produces(MediaType.TEXT_HTML)
160164
public static Response methodContentTypeUnsafe(String userControlled) {
161-
return Response.ok(userControlled).build(); // $MISSING: xss
165+
return Response.ok(userControlled).build(); // $xss
162166
}
163167

164168
@POST @Produces(MediaType.TEXT_HTML)
165169
public static Response methodContentTypeUnsafePost(String userControlled) {
166-
return Response.ok(userControlled).build(); // $MISSING: xss
170+
return Response.ok(userControlled).build(); // $xss
167171
}
168172

169173
@GET @Produces("text/html")
170174
public static Response methodContentTypeUnsafeStringLiteral(String userControlled) {
171-
return Response.ok(userControlled).build(); // $MISSING: xss
175+
return Response.ok(userControlled).build(); // $xss
172176
}
173177

174178
@GET @Produces({MediaType.TEXT_HTML, MediaType.APPLICATION_JSON})
175179
public static Response methodContentTypeMaybeSafe(String userControlled) {
176-
return Response.ok(userControlled).build(); // $MISSING: xss
180+
return Response.ok(userControlled).build(); // $xss
177181
}
178182

179183
@GET @Produces(MediaType.APPLICATION_JSON)
180184
public static Response methodContentTypeSafeOverriddenWithUnsafe(String userControlled) {
181-
return Response.ok().type(MediaType.TEXT_HTML).entity(userControlled).build(); // $MISSING: xss
185+
return Response.ok().type(MediaType.TEXT_HTML).entity(userControlled).build(); // $xss
182186
}
183187

184188
@GET @Produces(MediaType.TEXT_HTML)
@@ -201,12 +205,12 @@ public String testDirectReturn(String userControlled) {
201205

202206
@GET @Produces({"text/html"})
203207
public Response overridesWithUnsafe(String userControlled) {
204-
return Response.ok(userControlled).build(); // $MISSING: xss
208+
return Response.ok(userControlled).build(); // $xss
205209
}
206210

207211
@GET
208212
public Response overridesWithUnsafe2(String userControlled) {
209-
return Response.ok().type(MediaType.TEXT_HTML).entity(userControlled).build(); // $MISSING: xss
213+
return Response.ok().type(MediaType.TEXT_HTML).entity(userControlled).build(); // $xss
210214
}
211215
}
212216

@@ -215,12 +219,12 @@ public Response overridesWithUnsafe2(String userControlled) {
215219
public static class ClassContentTypeUnsafe {
216220
@GET
217221
public Response test(String userControlled) {
218-
return Response.ok(userControlled).build(); // $MISSING: xss
222+
return Response.ok(userControlled).build(); // $xss
219223
}
220224

221225
@GET
222226
public String testDirectReturn(String userControlled) {
223-
return userControlled; // $MISSING: xss
227+
return userControlled; // $xss
224228
}
225229

226230
@GET @Produces({"application/json"})

0 commit comments

Comments
 (0)