@@ -283,24 +283,34 @@ class MessageBodyReaderRead extends Method {
283
283
}
284
284
}
285
285
286
+ /**
287
+ * Gets a constant content-type described by expression `e` (either a string constant or a Jax-RS MediaType field access).
288
+ */
289
+ string getContentTypeString ( Expr e ) {
290
+ result = e .( CompileTimeConstantExpr ) .getStringValue ( ) and
291
+ result != ""
292
+ or
293
+ exists ( Field jaxMediaType |
294
+ // Accesses to static fields on `MediaType` class do not have constant strings in the database
295
+ // so convert the field name to a content type string
296
+ jaxMediaType .getDeclaringType ( ) .hasQualifiedName ( getAJaxRsPackage ( "core" ) , "MediaType" ) and
297
+ jaxMediaType .getAnAccess ( ) = e and
298
+ // e.g. MediaType.TEXT_PLAIN => text/plain
299
+ result = jaxMediaType .getName ( ) .toLowerCase ( ) .replaceAll ( "_value" , "" ) .replaceAll ( "_" , "/" )
300
+ )
301
+ }
302
+
286
303
/** An `@Produces` annotation that describes which content types can be produced by this resource. */
287
304
class JaxRSProducesAnnotation extends JaxRSAnnotation {
288
305
JaxRSProducesAnnotation ( ) { this .getType ( ) .hasQualifiedName ( getAJaxRsPackage ( ) , "Produces" ) }
289
306
290
307
/**
291
308
* Gets a declared content type that can be produced by this resource.
292
309
*/
293
- string getADeclaredContentType ( ) {
294
- result = this .getAValue ( ) . ( CompileTimeConstantExpr ) . getStringValue ( )
310
+ Expr getADeclaredContentTypeExpr ( ) {
311
+ result = this .getAValue ( ) and not result instanceof ArrayInit
295
312
or
296
- exists ( Field jaxMediaType |
297
- // Accesses to static fields on `MediaType` class do not have constant strings in the database
298
- // so convert the field name to a content type string
299
- jaxMediaType .getDeclaringType ( ) .hasQualifiedName ( getAJaxRsPackage ( "core" ) , "MediaType" ) and
300
- jaxMediaType .getAnAccess ( ) = this .getAValue ( ) and
301
- // e.g. MediaType.TEXT_PLAIN => text/plain
302
- result = jaxMediaType .getName ( ) .toLowerCase ( ) .replaceAll ( "_" , "/" )
303
- )
313
+ result = this .getAValue ( ) .( ArrayInit ) .getAnInit ( )
304
314
}
305
315
}
306
316
@@ -319,7 +329,9 @@ private class JaxRSXssSink extends XssSink {
319
329
|
320
330
not exists ( resourceMethod .getProducesAnnotation ( ) )
321
331
or
322
- resourceMethod .getProducesAnnotation ( ) .getADeclaredContentType ( ) = "text/plain"
332
+ isXssVulnerableContentType ( getContentTypeString ( resourceMethod
333
+ .getProducesAnnotation ( )
334
+ .getADeclaredContentTypeExpr ( ) ) )
323
335
)
324
336
}
325
337
}
@@ -796,3 +808,150 @@ private class JaxRsUrlOpenSink extends SinkModelCsv {
796
808
]
797
809
}
798
810
}
811
+
812
+ private predicate isXssVulnerableContentTypeExpr ( Expr e ) {
813
+ isXssVulnerableContentType ( getContentTypeString ( e ) )
814
+ }
815
+
816
+ private predicate isXssSafeContentTypeExpr ( Expr e ) { isXssSafeContentType ( getContentTypeString ( e ) ) }
817
+
818
+ /**
819
+ * Gets a builder expression or related type that is configured to use the given `contentType`.
820
+ *
821
+ * This could be an instance of `Response.ResponseBuilder`, `Variant`, `Variant.VariantListBuilder` or
822
+ * a `List<Variant>`.
823
+ *
824
+ * This predicate is used to search forwards for response entities set after the content-type is configured.
825
+ * It does not need to consider cases where the entity is set in the same call, or the entity has already
826
+ * been set: these are handled by simple sanitization below.
827
+ */
828
+ private DataFlow:: Node getABuilderWithExplicitContentType ( Expr contentType ) {
829
+ // Base case: ResponseBuilder.type(contentType)
830
+ result .asExpr ( ) =
831
+ any ( MethodAccess ma |
832
+ ma .getCallee ( ) .hasQualifiedName ( getAJaxRsPackage ( "core" ) , "Response$ResponseBuilder" , "type" ) and
833
+ contentType = ma .getArgument ( 0 )
834
+ )
835
+ or
836
+ // Base case: new Variant(contentType, ...)
837
+ result .asExpr ( ) =
838
+ any ( ClassInstanceExpr cie |
839
+ cie .getConstructedType ( ) .hasQualifiedName ( getAJaxRsPackage ( "core" ) , "Variant" ) and
840
+ contentType = cie .getArgument ( 0 )
841
+ )
842
+ or
843
+ // Base case: Variant[.VariantListBuilder].mediaTypes(...)
844
+ result .asExpr ( ) =
845
+ any ( MethodAccess ma |
846
+ ma .getCallee ( )
847
+ .hasQualifiedName ( getAJaxRsPackage ( "core" ) , [ "Variant" , "Variant$VariantListBuilder" ] ,
848
+ "mediaTypes" ) and
849
+ contentType = ma .getAnArgument ( )
850
+ )
851
+ or
852
+ // Recursive case: propagate through variant list building:
853
+ result .asExpr ( ) =
854
+ any ( MethodAccess ma |
855
+ (
856
+ ma .getType ( )
857
+ .( RefType )
858
+ .hasQualifiedName ( getAJaxRsPackage ( "core" ) , "Variant$VariantListBuilder" )
859
+ or
860
+ ma .getMethod ( )
861
+ .hasQualifiedName ( getAJaxRsPackage ( "core" ) , "Variant$VariantListBuilder" , "build" )
862
+ ) and
863
+ [ ma .getAnArgument ( ) , ma .getQualifier ( ) ] =
864
+ getABuilderWithExplicitContentType ( contentType ) .asExpr ( )
865
+ )
866
+ or
867
+ // Recursive case: propagate through a List.get operation
868
+ result .asExpr ( ) =
869
+ any ( MethodAccess ma |
870
+ ma .getMethod ( ) .hasQualifiedName ( "java.util" , "List<Variant>" , "get" ) and
871
+ ma .getQualifier ( ) = getABuilderWithExplicitContentType ( contentType ) .asExpr ( )
872
+ )
873
+ or
874
+ // Recursive case: propagate through Response.ResponseBuilder operations, including the `variant(...)` operation.
875
+ result .asExpr ( ) =
876
+ any ( MethodAccess ma |
877
+ ma .getType ( ) .( RefType ) .hasQualifiedName ( getAJaxRsPackage ( "core" ) , "Response$ResponseBuilder" ) and
878
+ [ ma .getQualifier ( ) , ma .getArgument ( 0 ) ] =
879
+ getABuilderWithExplicitContentType ( contentType ) .asExpr ( )
880
+ )
881
+ or
882
+ // Recursive case: ordinary local dataflow
883
+ DataFlow:: localFlowStep ( getABuilderWithExplicitContentType ( contentType ) , result )
884
+ }
885
+
886
+ private DataFlow:: Node getASanitizedBuilder ( ) {
887
+ result = getABuilderWithExplicitContentType ( any ( Expr e | isXssSafeContentTypeExpr ( e ) ) )
888
+ }
889
+
890
+ private DataFlow:: Node getAVulnerableBuilder ( ) {
891
+ result = getABuilderWithExplicitContentType ( any ( Expr e | isXssVulnerableContentTypeExpr ( e ) ) )
892
+ }
893
+
894
+ /**
895
+ * A response builder sanitized by setting a safe content type.
896
+ *
897
+ * The content type could be set before the `entity(...)` call that needs sanitizing
898
+ * (e.g. `Response.ok().type("application/json").entity(sanitizeMe)`)
899
+ * or at the same time (e.g. `Response.ok(sanitizeMe, "application/json")`
900
+ * or the content-type could be set afterwards (e.g. `Response.ok().entity(userControlled).type("application/json")`)
901
+ *
902
+ * This differs from `getASanitizedBuilder` in that we also include functions that must set the entity
903
+ * at the same time, or the entity must already have been set, so propagating forwards to sanitize future
904
+ * build steps is not necessary.
905
+ */
906
+ private class SanitizedResponseBuilder extends XssSanitizer {
907
+ SanitizedResponseBuilder ( ) {
908
+ // e.g. sanitizeMe.type("application/json")
909
+ this = getASanitizedBuilder ( )
910
+ or
911
+ this .asExpr ( ) =
912
+ any ( MethodAccess ma |
913
+ ma .getMethod ( ) .hasQualifiedName ( getAJaxRsPackage ( "core" ) , "Response" , "ok" ) and
914
+ (
915
+ // e.g. Response.ok(sanitizeMe, new Variant("application/json", ...))
916
+ ma .getArgument ( 1 ) = getASanitizedBuilder ( ) .asExpr ( )
917
+ or
918
+ // e.g. Response.ok(sanitizeMe, "application/json")
919
+ isXssSafeContentTypeExpr ( ma .getArgument ( 1 ) )
920
+ )
921
+ )
922
+ }
923
+ }
924
+
925
+ /**
926
+ * An entity call that serves as a sink and barrier because it has a vulnerable content-type set.
927
+ *
928
+ * We flag these as direct sinks because otherwise it may be sanitized when it reaches a resource
929
+ * method with a safe-looking `@Produces` annotation. They are barriers because otherwise if the
930
+ * resource method does *not* have a safe-looking `@Produces` annotation then it would be doubly
931
+ * reported, once at the `entity(...)` call and once on return from the resource method.
932
+ */
933
+ private class VulnerableEntity extends XssSinkBarrier {
934
+ VulnerableEntity ( ) {
935
+ this .asExpr ( ) =
936
+ any ( MethodAccess ma |
937
+ (
938
+ // Vulnerable content-type already set:
939
+ ma .getQualifier ( ) = getAVulnerableBuilder ( ) .asExpr ( )
940
+ or
941
+ // Vulnerable content-type set in the future:
942
+ getAVulnerableBuilder ( ) .asExpr ( ) .( MethodAccess ) .getQualifier * ( ) = ma
943
+ ) and
944
+ ma .getMethod ( ) .hasName ( "entity" )
945
+ ) .getArgument ( 0 )
946
+ or
947
+ this .asExpr ( ) =
948
+ any ( MethodAccess ma |
949
+ (
950
+ isXssVulnerableContentTypeExpr ( ma .getArgument ( 1 ) )
951
+ or
952
+ ma .getArgument ( 1 ) = getAVulnerableBuilder ( ) .asExpr ( )
953
+ ) and
954
+ ma .getMethod ( ) .hasName ( "ok" )
955
+ ) .getArgument ( 0 )
956
+ }
957
+ }
0 commit comments