diff --git a/java/change-notes/2021-05-17-jackson-deserialization-sink.md b/java/change-notes/2021-05-17-jackson-deserialization-sink.md
new file mode 100644
index 000000000000..6db65d9dba41
--- /dev/null
+++ b/java/change-notes/2021-05-17-jackson-deserialization-sink.md
@@ -0,0 +1,3 @@
+lgtm,codescanning
+* The "Deserialization of user-controlled data" (`java/unsafe-deserialization`) query
+ now recognizes `Jackson` deserialization.
diff --git a/java/ql/src/Security/CWE/CWE-502/UnsafeDeserialization.qhelp b/java/ql/src/Security/CWE/CWE-502/UnsafeDeserialization.qhelp
index e9600f11b937..c580d36e2cc4 100644
--- a/java/ql/src/Security/CWE/CWE-502/UnsafeDeserialization.qhelp
+++ b/java/ql/src/Security/CWE/CWE-502/UnsafeDeserialization.qhelp
@@ -14,8 +14,8 @@ may have unforeseen effects, such as the execution of arbitrary code.
There are many different serialization frameworks. This query currently
-supports Kryo, XmlDecoder, XStream, SnakeYaml, JYaml, JsonIO, YAMLBeans, HessianBurlap, Castor, Burlap
-and Java IO serialization through ObjectInputStream
/ObjectOutputStream
.
+supports Kryo, XmlDecoder, XStream, SnakeYaml, JYaml, JsonIO, YAMLBeans, HessianBurlap, Castor, Burlap,
+Jackson and Java IO serialization through ObjectInputStream
/ObjectOutputStream
.
@@ -91,6 +91,15 @@ Remote code execution in JYaml library:
JsonIO deserialization vulnerabilities:
JsonIO deserialization .
+
+Research by Moritz Bechler:
+Java Unmarshaller Security - Turning your data into code execution
+
+
+Blog posts by the developer of Jackson libraries:
+On Jackson CVEs: Don’t Panic — Here is what you need to know
+Jackson 2.10: Safe Default Typing
+
diff --git a/java/ql/src/Security/CWE/CWE-502/UnsafeDeserialization.ql b/java/ql/src/Security/CWE/CWE-502/UnsafeDeserialization.ql
index 606468d451d1..6bb4cdc3561d 100644
--- a/java/ql/src/Security/CWE/CWE-502/UnsafeDeserialization.ql
+++ b/java/ql/src/Security/CWE/CWE-502/UnsafeDeserialization.ql
@@ -12,51 +12,9 @@
*/
import java
-import semmle.code.java.dataflow.FlowSources
-import semmle.code.java.security.UnsafeDeserialization
+import semmle.code.java.security.UnsafeDeserializationQuery
import DataFlow::PathGraph
-class UnsafeDeserializationConfig extends TaintTracking::Configuration {
- UnsafeDeserializationConfig() { this = "UnsafeDeserializationConfig" }
-
- override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
-
- override predicate isSink(DataFlow::Node sink) { sink instanceof UnsafeDeserializationSink }
-
- override predicate isAdditionalTaintStep(DataFlow::Node pred, DataFlow::Node succ) {
- exists(ClassInstanceExpr cie |
- cie.getArgument(0) = pred.asExpr() and
- cie = succ.asExpr() and
- (
- cie.getConstructor().getDeclaringType() instanceof JsonIoJsonReader or
- cie.getConstructor().getDeclaringType() instanceof YamlBeansReader or
- cie.getConstructor().getDeclaringType().getASupertype*() instanceof UnsafeHessianInput or
- cie.getConstructor().getDeclaringType() instanceof BurlapInput
- )
- )
- or
- exists(MethodAccess ma |
- ma.getMethod() instanceof BurlapInputInitMethod and
- ma.getArgument(0) = pred.asExpr() and
- ma.getQualifier() = succ.asExpr()
- )
- }
-
- override predicate isSanitizer(DataFlow::Node node) {
- exists(ClassInstanceExpr cie |
- cie.getConstructor().getDeclaringType() instanceof JsonIoJsonReader and
- cie = node.asExpr() and
- exists(SafeJsonIoConfig sji | sji.hasFlowToExpr(cie.getArgument(1)))
- )
- or
- exists(MethodAccess ma |
- ma.getMethod() instanceof JsonIoJsonToJavaMethod and
- ma.getArgument(0) = node.asExpr() and
- exists(SafeJsonIoConfig sji | sji.hasFlowToExpr(ma.getArgument(1)))
- )
- }
-}
-
from DataFlow::PathNode source, DataFlow::PathNode sink, UnsafeDeserializationConfig conf
where conf.hasFlowPath(source, sink)
select sink.getNode().(UnsafeDeserializationSink).getMethodAccess(), source, sink,
diff --git a/java/ql/src/semmle/code/java/frameworks/Jackson.qll b/java/ql/src/semmle/code/java/frameworks/Jackson.qll
new file mode 100644
index 000000000000..505a8e9cdf01
--- /dev/null
+++ b/java/ql/src/semmle/code/java/frameworks/Jackson.qll
@@ -0,0 +1,174 @@
+/**
+ * Provides classes and predicates for working with the Jackson serialization framework.
+ */
+
+import java
+private import semmle.code.java.Reflection
+private import semmle.code.java.dataflow.DataFlow
+
+private class ObjectMapper extends RefType {
+ ObjectMapper() {
+ getASupertype*().hasQualifiedName("com.fasterxml.jackson.databind", "ObjectMapper")
+ }
+}
+
+/** A builder for building Jackson's `JsonMapper`. */
+class MapperBuilder extends RefType {
+ MapperBuilder() {
+ hasQualifiedName("com.fasterxml.jackson.databind.cfg", "MapperBuilder")
+ }
+}
+
+private class JsonFactory extends RefType {
+ JsonFactory() { hasQualifiedName("com.fasterxml.jackson.core", "JsonFactory") }
+}
+
+private class JsonParser extends RefType {
+ JsonParser() { hasQualifiedName("com.fasterxml.jackson.core", "JsonParser") }
+}
+
+/** A type descriptor in Jackson libraries. For example, `java.lang.Class`. */
+class JacksonTypeDescriptorType extends RefType {
+ JacksonTypeDescriptorType() {
+ this instanceof TypeClass or
+ hasQualifiedName("com.fasterxml.jackson.databind", "JavaType") or
+ hasQualifiedName("com.fasterxml.jackson.core.type", "TypeReference")
+ }
+}
+
+/** A method in `ObjectMapper` that deserialize data. */
+class ObjectMapperReadMethod extends Method {
+ ObjectMapperReadMethod() {
+ this.getDeclaringType() instanceof ObjectMapper and
+ this.hasName(["readValue", "readValues", "treeToValue"])
+ }
+}
+
+/** A call that enables the default typing in `ObjectMapper`. */
+class EnableJacksonDefaultTyping extends MethodAccess {
+ EnableJacksonDefaultTyping() {
+ this.getMethod().getDeclaringType() instanceof ObjectMapper and
+ this.getMethod().hasName("enableDefaultTyping")
+ }
+}
+
+/** A qualifier of a call to one of the methods in `ObjectMapper` that deserialize data. */
+class ObjectMapperReadQualifier extends DataFlow::ExprNode {
+ ObjectMapperReadQualifier() {
+ exists(MethodAccess ma | ma.getQualifier() = this.asExpr() |
+ ma.getMethod() instanceof ObjectMapperReadMethod
+ )
+ }
+}
+
+/** A source that sets a type validator. */
+class SetPolymorphicTypeValidatorSource extends DataFlow::ExprNode {
+ SetPolymorphicTypeValidatorSource() {
+ exists(MethodAccess ma, Method m | m = ma.getMethod() |
+ (
+ m.getDeclaringType() instanceof ObjectMapper and
+ m.hasName("setPolymorphicTypeValidator")
+ or
+ m.getDeclaringType() instanceof MapperBuilder and
+ m.hasName("polymorphicTypeValidator")
+ ) and
+ this.asExpr() = ma.getQualifier()
+ )
+ }
+}
+
+/** Holds if `fromNode` to `toNode` is a dataflow step that resolves a class. */
+predicate resolveClassStep(DataFlow::Node fromNode, DataFlow::Node toNode) {
+ exists(ReflectiveClassIdentifierMethodAccess ma |
+ ma.getArgument(0) = fromNode.asExpr() and
+ ma = toNode.asExpr()
+ )
+}
+
+/**
+ * Holds if `fromNode` to `toNode` is a dataflow step that creates a Jackson parser.
+ *
+ * For example, a `createParser(userString)` call yields a `JsonParser`, which becomes dangerous
+ * if passed to an unsafely-configured `ObjectMapper`'s `readValue` method.
+ */
+predicate createJacksonJsonParserStep(DataFlow::Node fromNode, DataFlow::Node toNode) {
+ exists(MethodAccess ma, Method m | m = ma.getMethod() |
+ (m.getDeclaringType() instanceof ObjectMapper or m.getDeclaringType() instanceof JsonFactory) and
+ m.hasName("createParser") and
+ ma.getArgument(0) = fromNode.asExpr() and
+ ma = toNode.asExpr()
+ )
+}
+
+/**
+ * Holds if `fromNode` to `toNode` is a dataflow step that creates a Jackson `TreeNode`.
+ *
+ * These are parse trees of user-supplied JSON, which may lead to arbitrary code execution
+ * if passed to an unsafely-configured `ObjectMapper`'s `treeToValue` method.
+ */
+predicate createJacksonTreeNodeStep(DataFlow::Node fromNode, DataFlow::Node toNode) {
+ exists(MethodAccess ma, Method m | m = ma.getMethod() |
+ m.getDeclaringType() instanceof ObjectMapper and
+ m.hasName("readTree") and
+ ma.getArgument(0) = fromNode.asExpr() and
+ ma = toNode.asExpr()
+ )
+ or
+ exists(MethodAccess ma, Method m | m = ma.getMethod() |
+ m.getDeclaringType() instanceof JsonParser and
+ m.hasName("readValueAsTree") and
+ ma.getQualifier() = fromNode.asExpr() and
+ ma = toNode.asExpr()
+ )
+}
+
+/**
+ * Holds if `type` or one of its supertypes has a field with `JsonTypeInfo` annotation
+ * that enables polymorphic type handling.
+ */
+private predicate hasJsonTypeInfoAnnotation(RefType type) {
+ hasFieldWithJsonTypeAnnotation(type.getASupertype*()) or
+ hasJsonTypeInfoAnnotation(type.getAField().getType())
+}
+
+/**
+ * Holds if `type` has a field with `JsonTypeInfo` annotation
+ * that enables polymorphic type handling.
+ */
+private predicate hasFieldWithJsonTypeAnnotation(RefType type) {
+ exists(Annotation a |
+ type.getAField().getAnAnnotation() = a and
+ a.getType().hasQualifiedName("com.fasterxml.jackson.annotation", "JsonTypeInfo") and
+ a.getValue("use").(VarAccess).getVariable().hasName(["CLASS", "MINIMAL_CLASS"])
+ )
+}
+
+/**
+ * Holds if `call` is a method call to a Jackson deserialization method such as `ObjectMapper.readValue(String, Class)`,
+ * and the target deserialized class has a field with a `JsonTypeInfo` annotation that enables polymorphic typing.
+ */
+predicate hasArgumentWithUnsafeJacksonAnnotation(MethodAccess call) {
+ call.getMethod() instanceof ObjectMapperReadMethod and
+ exists(RefType argType, int i | i > 0 and argType = call.getArgument(i).getType() |
+ hasJsonTypeInfoAnnotation(argType.(ParameterizedType).getATypeArgument())
+ )
+}
+
+/**
+ * Holds if `fromNode` to `toNode` is a dataflow step that looks like resolving a class.
+ * A method probably resolves a class if it takes a string, returns a type descriptor,
+ * and its name contains "resolve", "load", etc.
+ *
+ * Any method call that satisfies the rule above is assumed to propagate taint from its string arguments,
+ * so methods that accept user-controlled data but sanitize it or use it for some
+ * completely different purpose before returning a type descriptor could result in false positives.
+ */
+predicate looksLikeResolveClassStep(DataFlow::Node fromNode, DataFlow::Node toNode) {
+ exists(MethodAccess ma, Method m, Expr arg | m = ma.getMethod() and arg = ma.getAnArgument() |
+ m.getReturnType() instanceof JacksonTypeDescriptorType and
+ m.getName().toLowerCase().regexpMatch("(.*)(resolve|load|class|type)(.*)") and
+ arg.getType() instanceof TypeString and
+ arg = fromNode.asExpr() and
+ ma = toNode.asExpr()
+ )
+}
diff --git a/java/ql/src/semmle/code/java/security/UnsafeDeserialization.qll b/java/ql/src/semmle/code/java/security/UnsafeDeserialization.qll
deleted file mode 100644
index d035837ad6a0..000000000000
--- a/java/ql/src/semmle/code/java/security/UnsafeDeserialization.qll
+++ /dev/null
@@ -1,172 +0,0 @@
-import semmle.code.java.frameworks.Kryo
-import semmle.code.java.frameworks.XStream
-import semmle.code.java.frameworks.SnakeYaml
-import semmle.code.java.frameworks.FastJson
-import semmle.code.java.frameworks.JYaml
-import semmle.code.java.frameworks.JsonIo
-import semmle.code.java.frameworks.YamlBeans
-import semmle.code.java.frameworks.HessianBurlap
-import semmle.code.java.frameworks.Castor
-import semmle.code.java.frameworks.apache.Lang
-
-class ObjectInputStreamReadObjectMethod extends Method {
- ObjectInputStreamReadObjectMethod() {
- this.getDeclaringType().getASourceSupertype*().hasQualifiedName("java.io", "ObjectInputStream") and
- (this.hasName("readObject") or this.hasName("readUnshared"))
- }
-}
-
-class XMLDecoderReadObjectMethod extends Method {
- XMLDecoderReadObjectMethod() {
- this.getDeclaringType().hasQualifiedName("java.beans", "XMLDecoder") and
- this.hasName("readObject")
- }
-}
-
-class SafeXStream extends DataFlow2::Configuration {
- SafeXStream() { this = "UnsafeDeserialization::SafeXStream" }
-
- override predicate isSource(DataFlow::Node src) {
- any(XStreamEnableWhiteListing ma).getQualifier().(VarAccess).getVariable().getAnAccess() =
- src.asExpr()
- }
-
- override predicate isSink(DataFlow::Node sink) {
- exists(MethodAccess ma |
- sink.asExpr() = ma.getQualifier() and
- ma.getMethod() instanceof XStreamReadObjectMethod
- )
- }
-}
-
-class SafeKryo extends DataFlow2::Configuration {
- SafeKryo() { this = "UnsafeDeserialization::SafeKryo" }
-
- override predicate isSource(DataFlow::Node src) {
- any(KryoEnableWhiteListing ma).getQualifier().(VarAccess).getVariable().getAnAccess() =
- src.asExpr()
- }
-
- override predicate isSink(DataFlow::Node sink) {
- exists(MethodAccess ma |
- sink.asExpr() = ma.getQualifier() and
- ma.getMethod() instanceof KryoReadObjectMethod
- )
- }
-
- override predicate isAdditionalFlowStep(DataFlow::Node node1, DataFlow::Node node2) {
- stepKryoPoolBuilderFactoryArgToConstructor(node1, node2) or
- stepKryoPoolRunMethodAccessQualifierToFunctionalArgument(node1, node2) or
- stepKryoPoolBuilderChainMethod(node1, node2) or
- stepKryoPoolBorrowMethod(node1, node2)
- }
-
- /**
- * Holds when a functional expression is used to create a `KryoPool.Builder`.
- * Eg. `new KryoPool.Builder(() -> new Kryo())`
- */
- private predicate stepKryoPoolBuilderFactoryArgToConstructor(
- DataFlow::Node node1, DataFlow::Node node2
- ) {
- exists(ConstructorCall cc, FunctionalExpr fe |
- cc.getConstructedType() instanceof KryoPoolBuilder and
- fe.asMethod().getBody().getAStmt().(ReturnStmt).getResult() = node1.asExpr() and
- node2.asExpr() = cc and
- cc.getArgument(0) = fe
- )
- }
-
- /**
- * Holds when a `KryoPool.run` is called to use a `Kryo` instance.
- * Eg. `pool.run(kryo -> ...)`
- */
- private predicate stepKryoPoolRunMethodAccessQualifierToFunctionalArgument(
- DataFlow::Node node1, DataFlow::Node node2
- ) {
- exists(MethodAccess ma |
- ma.getMethod() instanceof KryoPoolRunMethod and
- node1.asExpr() = ma.getQualifier() and
- ma.getArgument(0).(FunctionalExpr).asMethod().getParameter(0) = node2.asParameter()
- )
- }
-
- /**
- * Holds when a `KryoPool.Builder` method is called fluently.
- */
- private predicate stepKryoPoolBuilderChainMethod(DataFlow::Node node1, DataFlow::Node node2) {
- exists(MethodAccess ma |
- ma.getMethod() instanceof KryoPoolBuilderMethod and
- ma = node2.asExpr() and
- ma.getQualifier() = node1.asExpr()
- )
- }
-
- /**
- * Holds when a `KryoPool.borrow` method is called.
- */
- private predicate stepKryoPoolBorrowMethod(DataFlow::Node node1, DataFlow::Node node2) {
- exists(MethodAccess ma |
- ma.getMethod() =
- any(Method m | m.getDeclaringType() instanceof KryoPool and m.hasName("borrow")) and
- node1.asExpr() = ma.getQualifier() and
- node2.asExpr() = ma
- )
- }
-}
-
-predicate unsafeDeserialization(MethodAccess ma, Expr sink) {
- exists(Method m | m = ma.getMethod() |
- m instanceof ObjectInputStreamReadObjectMethod and
- sink = ma.getQualifier() and
- not exists(DataFlow::ExprNode node |
- node.getExpr() = sink and
- node.getTypeBound()
- .(RefType)
- .hasQualifiedName("org.apache.commons.io.serialization", "ValidatingObjectInputStream")
- )
- or
- m instanceof XMLDecoderReadObjectMethod and
- sink = ma.getQualifier()
- or
- m instanceof XStreamReadObjectMethod and
- sink = ma.getAnArgument() and
- not exists(SafeXStream sxs | sxs.hasFlowToExpr(ma.getQualifier()))
- or
- m instanceof KryoReadObjectMethod and
- sink = ma.getAnArgument() and
- not exists(SafeKryo sk | sk.hasFlowToExpr(ma.getQualifier()))
- or
- m instanceof MethodApacheSerializationUtilsDeserialize and
- sink = ma.getArgument(0)
- or
- ma instanceof UnsafeSnakeYamlParse and
- sink = ma.getArgument(0)
- or
- ma.getMethod() instanceof FastJsonParseMethod and
- not fastJsonLooksSafe() and
- sink = ma.getArgument(0)
- or
- ma.getMethod() instanceof JYamlLoaderUnsafeLoadMethod and
- sink = ma.getArgument(0)
- or
- ma.getMethod() instanceof JsonIoJsonToJavaMethod and
- sink = ma.getArgument(0)
- or
- ma.getMethod() instanceof JsonIoReadObjectMethod and
- sink = ma.getQualifier()
- or
- ma.getMethod() instanceof YamlBeansReaderReadMethod and sink = ma.getQualifier()
- or
- ma.getMethod() instanceof UnsafeHessianInputReadObjectMethod and sink = ma.getQualifier()
- or
- ma.getMethod() instanceof CastorUnmarshalMethod and sink = ma.getAnArgument()
- or
- ma.getMethod() instanceof BurlapInputReadObjectMethod and sink = ma.getQualifier()
- )
-}
-
-class UnsafeDeserializationSink extends DataFlow::ExprNode {
- UnsafeDeserializationSink() { unsafeDeserialization(_, this.getExpr()) }
-
- MethodAccess getMethodAccess() { unsafeDeserialization(result, this.getExpr()) }
-}
diff --git a/java/ql/src/semmle/code/java/security/UnsafeDeserializationQuery.qll b/java/ql/src/semmle/code/java/security/UnsafeDeserializationQuery.qll
new file mode 100644
index 000000000000..b9cfa1ddde8b
--- /dev/null
+++ b/java/ql/src/semmle/code/java/security/UnsafeDeserializationQuery.qll
@@ -0,0 +1,318 @@
+/**
+ * Provides classes and predicates for finding deserialization vulnerabilities.
+ */
+
+import semmle.code.java.dataflow.FlowSources
+private import semmle.code.java.dataflow.TaintTracking2
+private import semmle.code.java.frameworks.Kryo
+private import semmle.code.java.frameworks.XStream
+private import semmle.code.java.frameworks.SnakeYaml
+private import semmle.code.java.frameworks.FastJson
+private import semmle.code.java.frameworks.JYaml
+private import semmle.code.java.frameworks.JsonIo
+private import semmle.code.java.frameworks.YamlBeans
+private import semmle.code.java.frameworks.HessianBurlap
+private import semmle.code.java.frameworks.Castor
+private import semmle.code.java.frameworks.Jackson
+private import semmle.code.java.frameworks.apache.Lang
+private import semmle.code.java.Reflection
+
+private class ObjectInputStreamReadObjectMethod extends Method {
+ ObjectInputStreamReadObjectMethod() {
+ this.getDeclaringType().getASourceSupertype*().hasQualifiedName("java.io", "ObjectInputStream") and
+ (this.hasName("readObject") or this.hasName("readUnshared"))
+ }
+}
+
+private class XMLDecoderReadObjectMethod extends Method {
+ XMLDecoderReadObjectMethod() {
+ this.getDeclaringType().hasQualifiedName("java.beans", "XMLDecoder") and
+ this.hasName("readObject")
+ }
+}
+
+private class SafeXStream extends DataFlow2::Configuration {
+ SafeXStream() { this = "UnsafeDeserialization::SafeXStream" }
+
+ override predicate isSource(DataFlow::Node src) {
+ any(XStreamEnableWhiteListing ma).getQualifier().(VarAccess).getVariable().getAnAccess() =
+ src.asExpr()
+ }
+
+ override predicate isSink(DataFlow::Node sink) {
+ exists(MethodAccess ma |
+ sink.asExpr() = ma.getQualifier() and
+ ma.getMethod() instanceof XStreamReadObjectMethod
+ )
+ }
+}
+
+private class SafeKryo extends DataFlow2::Configuration {
+ SafeKryo() { this = "UnsafeDeserialization::SafeKryo" }
+
+ override predicate isSource(DataFlow::Node src) {
+ any(KryoEnableWhiteListing ma).getQualifier().(VarAccess).getVariable().getAnAccess() =
+ src.asExpr()
+ }
+
+ override predicate isSink(DataFlow::Node sink) {
+ exists(MethodAccess ma |
+ sink.asExpr() = ma.getQualifier() and
+ ma.getMethod() instanceof KryoReadObjectMethod
+ )
+ }
+
+ override predicate isAdditionalFlowStep(DataFlow::Node node1, DataFlow::Node node2) {
+ stepKryoPoolBuilderFactoryArgToConstructor(node1, node2) or
+ stepKryoPoolRunMethodAccessQualifierToFunctionalArgument(node1, node2) or
+ stepKryoPoolBuilderChainMethod(node1, node2) or
+ stepKryoPoolBorrowMethod(node1, node2)
+ }
+
+ /**
+ * Holds when a functional expression is used to create a `KryoPool.Builder`.
+ * Eg. `new KryoPool.Builder(() -> new Kryo())`
+ */
+ private predicate stepKryoPoolBuilderFactoryArgToConstructor(
+ DataFlow::Node node1, DataFlow::Node node2
+ ) {
+ exists(ConstructorCall cc, FunctionalExpr fe |
+ cc.getConstructedType() instanceof KryoPoolBuilder and
+ fe.asMethod().getBody().getAStmt().(ReturnStmt).getResult() = node1.asExpr() and
+ node2.asExpr() = cc and
+ cc.getArgument(0) = fe
+ )
+ }
+
+ /**
+ * Holds when a `KryoPool.run` is called to use a `Kryo` instance.
+ * Eg. `pool.run(kryo -> ...)`
+ */
+ private predicate stepKryoPoolRunMethodAccessQualifierToFunctionalArgument(
+ DataFlow::Node node1, DataFlow::Node node2
+ ) {
+ exists(MethodAccess ma |
+ ma.getMethod() instanceof KryoPoolRunMethod and
+ node1.asExpr() = ma.getQualifier() and
+ ma.getArgument(0).(FunctionalExpr).asMethod().getParameter(0) = node2.asParameter()
+ )
+ }
+
+ /**
+ * Holds when a `KryoPool.Builder` method is called fluently.
+ */
+ private predicate stepKryoPoolBuilderChainMethod(DataFlow::Node node1, DataFlow::Node node2) {
+ exists(MethodAccess ma |
+ ma.getMethod() instanceof KryoPoolBuilderMethod and
+ ma = node2.asExpr() and
+ ma.getQualifier() = node1.asExpr()
+ )
+ }
+
+ /**
+ * Holds when a `KryoPool.borrow` method is called.
+ */
+ private predicate stepKryoPoolBorrowMethod(DataFlow::Node node1, DataFlow::Node node2) {
+ exists(MethodAccess ma |
+ ma.getMethod() =
+ any(Method m | m.getDeclaringType() instanceof KryoPool and m.hasName("borrow")) and
+ node1.asExpr() = ma.getQualifier() and
+ node2.asExpr() = ma
+ )
+ }
+}
+
+/**
+ * Holds if `ma` is a call that deserializes data from `sink`.
+ */
+predicate unsafeDeserialization(MethodAccess ma, Expr sink) {
+ exists(Method m | m = ma.getMethod() |
+ m instanceof ObjectInputStreamReadObjectMethod and
+ sink = ma.getQualifier() and
+ not exists(DataFlow::ExprNode node |
+ node.getExpr() = sink and
+ node.getTypeBound()
+ .(RefType)
+ .hasQualifiedName("org.apache.commons.io.serialization", "ValidatingObjectInputStream")
+ )
+ or
+ m instanceof XMLDecoderReadObjectMethod and
+ sink = ma.getQualifier()
+ or
+ m instanceof XStreamReadObjectMethod and
+ sink = ma.getAnArgument() and
+ not exists(SafeXStream sxs | sxs.hasFlowToExpr(ma.getQualifier()))
+ or
+ m instanceof KryoReadObjectMethod and
+ sink = ma.getAnArgument() and
+ not exists(SafeKryo sk | sk.hasFlowToExpr(ma.getQualifier()))
+ or
+ m instanceof MethodApacheSerializationUtilsDeserialize and
+ sink = ma.getArgument(0)
+ or
+ ma instanceof UnsafeSnakeYamlParse and
+ sink = ma.getArgument(0)
+ or
+ ma.getMethod() instanceof FastJsonParseMethod and
+ not fastJsonLooksSafe() and
+ sink = ma.getArgument(0)
+ or
+ ma.getMethod() instanceof JYamlLoaderUnsafeLoadMethod and
+ sink = ma.getArgument(0)
+ or
+ ma.getMethod() instanceof JsonIoJsonToJavaMethod and
+ sink = ma.getArgument(0)
+ or
+ ma.getMethod() instanceof JsonIoReadObjectMethod and
+ sink = ma.getQualifier()
+ or
+ ma.getMethod() instanceof YamlBeansReaderReadMethod and sink = ma.getQualifier()
+ or
+ ma.getMethod() instanceof UnsafeHessianInputReadObjectMethod and sink = ma.getQualifier()
+ or
+ ma.getMethod() instanceof CastorUnmarshalMethod and sink = ma.getAnArgument()
+ or
+ ma.getMethod() instanceof BurlapInputReadObjectMethod and sink = ma.getQualifier()
+ or
+ ma.getMethod() instanceof ObjectMapperReadMethod and
+ sink = ma.getArgument(0) and
+ (
+ exists(UnsafeTypeConfig config | config.hasFlowToExpr(ma.getAnArgument()))
+ or
+ exists(EnableJacksonDefaultTypingConfig config | config.hasFlowToExpr(ma.getQualifier()))
+ or
+ hasArgumentWithUnsafeJacksonAnnotation(ma)
+ ) and
+ not exists(SafeObjectMapperConfig config | config.hasFlowToExpr(ma.getQualifier()))
+ )
+}
+
+/** A sink for unsafe deserialization. */
+class UnsafeDeserializationSink extends DataFlow::ExprNode {
+ UnsafeDeserializationSink() { unsafeDeserialization(_, this.getExpr()) }
+
+ /** Gets a call that triggers unsafe deserialization. */
+ MethodAccess getMethodAccess() { unsafeDeserialization(result, this.getExpr()) }
+}
+
+/**
+ * Tracks flows from remote user input to a deserialization sink.
+ */
+class UnsafeDeserializationConfig extends TaintTracking::Configuration {
+ UnsafeDeserializationConfig() { this = "UnsafeDeserializationConfig" }
+
+ override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
+
+ override predicate isSink(DataFlow::Node sink) { sink instanceof UnsafeDeserializationSink }
+
+ override predicate isAdditionalTaintStep(DataFlow::Node pred, DataFlow::Node succ) {
+ exists(ClassInstanceExpr cie |
+ cie.getArgument(0) = pred.asExpr() and
+ cie = succ.asExpr() and
+ (
+ cie.getConstructor().getDeclaringType() instanceof JsonIoJsonReader or
+ cie.getConstructor().getDeclaringType() instanceof YamlBeansReader or
+ cie.getConstructor().getDeclaringType().getASupertype*() instanceof UnsafeHessianInput or
+ cie.getConstructor().getDeclaringType() instanceof BurlapInput
+ )
+ )
+ or
+ exists(MethodAccess ma |
+ ma.getMethod() instanceof BurlapInputInitMethod and
+ ma.getArgument(0) = pred.asExpr() and
+ ma.getQualifier() = succ.asExpr()
+ )
+ or
+ createJacksonJsonParserStep(pred, succ)
+ or
+ createJacksonTreeNodeStep(pred, succ)
+ }
+
+ override predicate isSanitizer(DataFlow::Node node) {
+ exists(ClassInstanceExpr cie |
+ cie.getConstructor().getDeclaringType() instanceof JsonIoJsonReader and
+ cie = node.asExpr() and
+ exists(SafeJsonIoConfig sji | sji.hasFlowToExpr(cie.getArgument(1)))
+ )
+ or
+ exists(MethodAccess ma |
+ ma.getMethod() instanceof JsonIoJsonToJavaMethod and
+ ma.getArgument(0) = node.asExpr() and
+ exists(SafeJsonIoConfig sji | sji.hasFlowToExpr(ma.getArgument(1)))
+ )
+ }
+}
+
+/**
+ * Tracks flow from a remote source to a type descriptor (e.g. a `java.lang.Class` instance)
+ * passed to a Jackson deserialization method.
+ *
+ * If this is user-controlled, arbitrary code could be executed while instantiating the user-specified type.
+ */
+class UnsafeTypeConfig extends TaintTracking2::Configuration {
+ UnsafeTypeConfig() { this = "UnsafeTypeConfig" }
+
+ override predicate isSource(DataFlow::Node src) { src instanceof RemoteFlowSource }
+
+ override predicate isSink(DataFlow::Node sink) {
+ exists(MethodAccess ma, int i, Expr arg | i > 0 and ma.getArgument(i) = arg |
+ ma.getMethod() instanceof ObjectMapperReadMethod and
+ arg.getType() instanceof JacksonTypeDescriptorType and
+ arg = sink.asExpr()
+ )
+ }
+
+ /**
+ * Holds if `fromNode` to `toNode` is a dataflow step that resolves a class
+ * or at least looks like resolving a class.
+ */
+ override predicate isAdditionalTaintStep(DataFlow::Node fromNode, DataFlow::Node toNode) {
+ resolveClassStep(fromNode, toNode) or
+ looksLikeResolveClassStep(fromNode, toNode)
+ }
+}
+
+/**
+ * Tracks flow from `enableDefaultTyping` calls to a subsequent Jackson deserialization method call.
+ */
+class EnableJacksonDefaultTypingConfig extends DataFlow2::Configuration {
+ EnableJacksonDefaultTypingConfig() { this = "EnableJacksonDefaultTypingConfig" }
+
+ override predicate isSource(DataFlow::Node src) {
+ any(EnableJacksonDefaultTyping ma).getQualifier() = src.asExpr()
+ }
+
+ override predicate isSink(DataFlow::Node sink) { sink instanceof ObjectMapperReadQualifier }
+}
+
+/**
+ * Tracks flow from calls that set a type validator to a subsequent Jackson deserialization method call,
+ * including across builder method calls.
+ *
+ * Such a Jackson deserialization method call is safe because validation will likely prevent instantiating unexpected types.
+ */
+class SafeObjectMapperConfig extends DataFlow2::Configuration {
+ SafeObjectMapperConfig() { this = "SafeObjectMapperConfig" }
+
+ override predicate isSource(DataFlow::Node src) {
+ src instanceof SetPolymorphicTypeValidatorSource
+ }
+
+ override predicate isSink(DataFlow::Node sink) { sink instanceof ObjectMapperReadQualifier }
+
+ /**
+ * Holds if `fromNode` to `toNode` is a dataflow step
+ * that configures or creates an `ObjectMapper` via a builder.
+ */
+ override predicate isAdditionalFlowStep(DataFlow::Node fromNode, DataFlow::Node toNode) {
+ exists(MethodAccess ma, Method m | m = ma.getMethod() |
+ m.getDeclaringType() instanceof MapperBuilder and
+ m.getReturnType()
+ .(RefType)
+ .hasQualifiedName("com.fasterxml.jackson.databind.json",
+ ["JsonMapper$Builder", "JsonMapper"]) and
+ fromNode.asExpr() = ma.getQualifier() and
+ ma = toNode.asExpr()
+ )
+ }
+}
diff --git a/java/ql/test/library-tests/UnsafeDeserialization/unsafeDeserialization.ql b/java/ql/test/library-tests/UnsafeDeserialization/unsafeDeserialization.ql
index 9433eba7f7f3..0e0217a2472d 100644
--- a/java/ql/test/library-tests/UnsafeDeserialization/unsafeDeserialization.ql
+++ b/java/ql/test/library-tests/UnsafeDeserialization/unsafeDeserialization.ql
@@ -1,5 +1,5 @@
import default
-import semmle.code.java.security.UnsafeDeserialization
+import semmle.code.java.security.UnsafeDeserializationQuery
from Method m, MethodAccess ma
where ma.getMethod() = m and unsafeDeserialization(ma, _)
diff --git a/java/ql/test/query-tests/security/CWE-502/A.java b/java/ql/test/query-tests/security/CWE-502/A.java
index 50090f69132e..e95f15bff4f8 100644
--- a/java/ql/test/query-tests/security/CWE-502/A.java
+++ b/java/ql/test/query-tests/security/CWE-502/A.java
@@ -12,34 +12,34 @@ public class A {
public Object deserialize1(Socket sock) throws java.io.IOException, ClassNotFoundException {
InputStream inputStream = sock.getInputStream();
ObjectInputStream in = new ObjectInputStream(inputStream);
- return in.readObject(); // unsafe
+ return in.readObject(); // $unsafeDeserialization
}
public Object deserialize2(Socket sock) throws java.io.IOException, ClassNotFoundException {
InputStream inputStream = sock.getInputStream();
ObjectInputStream in = new ObjectInputStream(inputStream);
- return in.readUnshared(); // unsafe
+ return in.readUnshared(); // $unsafeDeserialization
}
public Object deserialize3(Socket sock) throws java.io.IOException {
InputStream inputStream = sock.getInputStream();
XMLDecoder d = new XMLDecoder(inputStream);
- return d.readObject(); // unsafe
+ return d.readObject(); // $unsafeDeserialization
}
public Object deserialize4(Socket sock) throws java.io.IOException {
XStream xs = new XStream();
InputStream inputStream = sock.getInputStream();
Reader reader = new InputStreamReader(inputStream);
- return xs.fromXML(reader); // unsafe
+ return xs.fromXML(reader); // $unsafeDeserialization
}
public void deserialize5(Socket sock) throws java.io.IOException {
Kryo kryo = new Kryo();
Input input = new Input(sock.getInputStream());
- A a1 = kryo.readObject(input, A.class); // unsafe
- A a2 = kryo.readObjectOrNull(input, A.class); // unsafe
- Object o = kryo.readClassAndObject(input); // unsafe
+ A a1 = kryo.readObject(input, A.class); // $unsafeDeserialization
+ A a2 = kryo.readObjectOrNull(input, A.class); // $unsafeDeserialization
+ Object o = kryo.readClassAndObject(input); // $unsafeDeserialization
}
private Kryo getSafeKryo() throws java.io.IOException {
@@ -58,21 +58,21 @@ public void deserialize6(Socket sock) throws java.io.IOException {
public void deserializeSnakeYaml(Socket sock) throws java.io.IOException {
Yaml yaml = new Yaml();
InputStream input = sock.getInputStream();
- Object o = yaml.load(input); //unsafe
- Object o2 = yaml.loadAll(input); //unsafe
- Object o3 = yaml.parse(new InputStreamReader(input)); //unsafe
- A o4 = yaml.loadAs(input, A.class); //unsafe
- A o5 = yaml.loadAs(new InputStreamReader(input), A.class); //unsafe
+ Object o = yaml.load(input); // $unsafeDeserialization
+ Object o2 = yaml.loadAll(input); // $unsafeDeserialization
+ Object o3 = yaml.parse(new InputStreamReader(input)); // $unsafeDeserialization
+ A o4 = yaml.loadAs(input, A.class); // $unsafeDeserialization
+ A o5 = yaml.loadAs(new InputStreamReader(input), A.class); // $unsafeDeserialization
}
public void deserializeSnakeYaml2(Socket sock) throws java.io.IOException {
Yaml yaml = new Yaml(new Constructor());
InputStream input = sock.getInputStream();
- Object o = yaml.load(input); //unsafe
- Object o2 = yaml.loadAll(input); //unsafe
- Object o3 = yaml.parse(new InputStreamReader(input)); //unsafe
- A o4 = yaml.loadAs(input, A.class); //unsafe
- A o5 = yaml.loadAs(new InputStreamReader(input), A.class); //unsafe
+ Object o = yaml.load(input); // $unsafeDeserialization
+ Object o2 = yaml.loadAll(input); // $unsafeDeserialization
+ Object o3 = yaml.parse(new InputStreamReader(input)); // $unsafeDeserialization
+ A o4 = yaml.loadAs(input, A.class); // $unsafeDeserialization
+ A o5 = yaml.loadAs(new InputStreamReader(input), A.class); // $unsafeDeserialization
}
public void deserializeSnakeYaml3(Socket sock) throws java.io.IOException {
@@ -88,10 +88,10 @@ public void deserializeSnakeYaml3(Socket sock) throws java.io.IOException {
public void deserializeSnakeYaml4(Socket sock) throws java.io.IOException {
Yaml yaml = new Yaml(new Constructor(A.class));
InputStream input = sock.getInputStream();
- Object o = yaml.load(input); //unsafe
- Object o2 = yaml.loadAll(input); //unsafe
- Object o3 = yaml.parse(new InputStreamReader(input)); //unsafe
- A o4 = yaml.loadAs(input, A.class); //unsafe
- A o5 = yaml.loadAs(new InputStreamReader(input), A.class); //unsafe
+ Object o = yaml.load(input); // $unsafeDeserialization
+ Object o2 = yaml.loadAll(input); // $unsafeDeserialization
+ Object o3 = yaml.parse(new InputStreamReader(input)); // $unsafeDeserialization
+ A o4 = yaml.loadAs(input, A.class); // $unsafeDeserialization
+ A o5 = yaml.loadAs(new InputStreamReader(input), A.class); // $unsafeDeserialization
}
}
diff --git a/java/ql/test/query-tests/security/CWE-502/B.java b/java/ql/test/query-tests/security/CWE-502/B.java
index a12555edd701..d97a44cfd58a 100644
--- a/java/ql/test/query-tests/security/CWE-502/B.java
+++ b/java/ql/test/query-tests/security/CWE-502/B.java
@@ -5,14 +5,14 @@
public class B {
public Object deserializeJson1(Socket sock) throws java.io.IOException {
InputStream inputStream = sock.getInputStream();
- return JSON.parseObject(inputStream, null); // unsafe
+ return JSON.parseObject(inputStream, null); // $unsafeDeserialization
}
public Object deserializeJson2(Socket sock) throws java.io.IOException {
InputStream inputStream = sock.getInputStream();
byte[] bytes = new byte[100];
inputStream.read(bytes);
- return JSON.parse(bytes); // unsafe
+ return JSON.parse(bytes); // $unsafeDeserialization
}
public Object deserializeJson3(Socket sock) throws java.io.IOException {
@@ -20,7 +20,7 @@ public Object deserializeJson3(Socket sock) throws java.io.IOException {
byte[] bytes = new byte[100];
inputStream.read(bytes);
String s = new String(bytes);
- return JSON.parseObject(s); // unsafe
+ return JSON.parseObject(s); // $unsafeDeserialization
}
public Object deserializeJson4(Socket sock) throws java.io.IOException {
@@ -28,6 +28,6 @@ public Object deserializeJson4(Socket sock) throws java.io.IOException {
byte[] bytes = new byte[100];
inputStream.read(bytes);
String s = new String(bytes);
- return JSON.parse(s); // unsafe
+ return JSON.parse(s); // $unsafeDeserialization
}
}
diff --git a/java/ql/test/query-tests/security/CWE-502/C.java b/java/ql/test/query-tests/security/CWE-502/C.java
index bae0ca8ceaec..e13ae98ead52 100644
--- a/java/ql/test/query-tests/security/CWE-502/C.java
+++ b/java/ql/test/query-tests/security/CWE-502/C.java
@@ -21,16 +21,16 @@ public class C {
@GetMapping(value = "jyaml")
public void bad1(HttpServletRequest request) throws Exception {
String data = request.getParameter("data");
- Yaml.load(data); //bad
- Yaml.loadStream(data); //bad
- Yaml.loadStreamOfType(data, Object.class); //bad
- Yaml.loadType(data, Object.class); //bad
+ Yaml.load(data); // $unsafeDeserialization
+ Yaml.loadStream(data); // $unsafeDeserialization
+ Yaml.loadStreamOfType(data, Object.class); // $unsafeDeserialization
+ Yaml.loadType(data, Object.class); // $unsafeDeserialization
org.ho.yaml.YamlConfig yamlConfig = new YamlConfig();
- yamlConfig.load(data); //bad
- yamlConfig.loadStream(data); //bad
- yamlConfig.loadStreamOfType(data, Object.class); //bad
- yamlConfig.loadType(data, Object.class); //bad
+ yamlConfig.load(data); // $unsafeDeserialization
+ yamlConfig.loadStream(data); // $unsafeDeserialization
+ yamlConfig.loadStreamOfType(data, Object.class); // $unsafeDeserialization
+ yamlConfig.loadType(data, Object.class); // $unsafeDeserialization
}
@GetMapping(value = "jsonio")
@@ -40,19 +40,19 @@ public void bad2(HttpServletRequest request) {
HashMap hashMap = new HashMap();
hashMap.put("USE_MAPS", true);
- JsonReader.jsonToJava(data); //bad
+ JsonReader.jsonToJava(data); // $unsafeDeserialization
- JsonReader jr = new JsonReader(data, null); //bad
- jr.readObject();
+ JsonReader jr = new JsonReader(data, null);
+ jr.readObject(); // $unsafeDeserialization
}
@GetMapping(value = "yamlbeans")
public void bad3(HttpServletRequest request) throws Exception {
String data = request.getParameter("data");
YamlReader r = new YamlReader(data);
- r.read(); //bad
- r.read(Object.class); //bad
- r.read(Object.class, Object.class); //bad
+ r.read(); // $unsafeDeserialization
+ r.read(Object.class); // $unsafeDeserialization
+ r.read(Object.class, Object.class); // $unsafeDeserialization
}
@GetMapping(value = "hessian")
@@ -60,8 +60,8 @@ public void bad4(HttpServletRequest request) throws Exception {
byte[] bytes = request.getParameter("data").getBytes();
ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
HessianInput hessianInput = new HessianInput(bis);
- hessianInput.readObject(); //bad
- hessianInput.readObject(Object.class); //bad
+ hessianInput.readObject(); // $unsafeDeserialization
+ hessianInput.readObject(Object.class); // $unsafeDeserialization
}
@GetMapping(value = "hessian2")
@@ -69,14 +69,14 @@ public void bad5(HttpServletRequest request) throws Exception {
byte[] bytes = request.getParameter("data").getBytes();
ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
Hessian2Input hessianInput = new Hessian2Input(bis);
- hessianInput.readObject(); //bad
- hessianInput.readObject(Object.class); //bad
+ hessianInput.readObject(); // $unsafeDeserialization
+ hessianInput.readObject(Object.class); // $unsafeDeserialization
}
@GetMapping(value = "castor")
public void bad6(HttpServletRequest request) throws Exception {
Unmarshaller unmarshaller = new Unmarshaller();
- unmarshaller.unmarshal(new StringReader(request.getParameter("data"))); //bad
+ unmarshaller.unmarshal(new StringReader(request.getParameter("data"))); // $unsafeDeserialization
}
@GetMapping(value = "burlap")
@@ -84,11 +84,11 @@ public void bad7(HttpServletRequest request) throws Exception {
byte[] serializedData = request.getParameter("data").getBytes();
ByteArrayInputStream is = new ByteArrayInputStream(serializedData);
BurlapInput burlapInput = new BurlapInput(is);
- burlapInput.readObject(); //bad
+ burlapInput.readObject(); // $unsafeDeserialization
BurlapInput burlapInput1 = new BurlapInput();
burlapInput1.init(is);
- burlapInput1.readObject(); //bad
+ burlapInput1.readObject(); // $unsafeDeserialization
}
@GetMapping(value = "jsonio1")
diff --git a/java/ql/test/query-tests/security/CWE-502/JacksonTest.java b/java/ql/test/query-tests/security/CWE-502/JacksonTest.java
new file mode 100644
index 000000000000..3520e4eaa116
--- /dev/null
+++ b/java/ql/test/query-tests/security/CWE-502/JacksonTest.java
@@ -0,0 +1,209 @@
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.databind.JavaType;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.json.JsonMapper;
+import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator;
+import com.fasterxml.jackson.databind.jsontype.PolymorphicTypeValidator;
+import java.io.IOException;
+import java.io.Serializable;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.util.List;
+
+public class JacksonTest {
+
+ public static void withSocket(Action action) throws Exception {
+ try (ServerSocket serverSocket = new ServerSocket(0)) {
+ try (Socket socket = serverSocket.accept()) {
+ byte[] bytes = new byte[1024];
+ int n = socket.getInputStream().read(bytes);
+ String jexlExpr = new String(bytes, 0, n);
+ action.run(jexlExpr);
+ }
+ }
+ }
+}
+
+interface Action {
+ void run(T object) throws Exception;
+}
+
+abstract class PhoneNumber implements Serializable {
+ public int areaCode;
+ public int local;
+}
+
+class DomesticNumber extends PhoneNumber {
+}
+
+class InternationalNumber extends PhoneNumber {
+ public int countryCode;
+}
+
+class Employee extends Person {
+}
+
+class Person {
+ public String name;
+ public int age;
+
+ // this annotation enables polymorphic type handling
+ @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
+ public Object phone;
+}
+
+class Task {
+ public Person assignee;
+}
+
+class Tag implements Serializable {
+ public String title;
+}
+
+class Cat {
+ public String name;
+ public Serializable tag;
+}
+
+class UnsafePersonDeserialization {
+
+ // BAD: Person has a field with an annotation that enables polymorphic type
+ // handling
+ private static void testUnsafeDeserialization() throws Exception {
+ JacksonTest.withSocket(string -> {
+ ObjectMapper mapper = new ObjectMapper();
+ mapper.readValue(string, Person.class); // $unsafeDeserialization
+ });
+ }
+
+ // BAD: Employee extends Person that has a field with an annotation that enables
+ // polymorphic type handling
+ private static void testUnsafeDeserializationWithExtendedClass() throws Exception {
+ JacksonTest.withSocket(string -> {
+ ObjectMapper mapper = new ObjectMapper();
+ mapper.readValue(string, Employee.class); // $unsafeDeserialization
+ });
+ }
+
+ // BAD: Task has a Person field that has a field with an annotation that enables
+ // polymorphic type handling
+ private static void testUnsafeDeserializationWithWrapper() throws Exception {
+ JacksonTest.withSocket(string -> {
+ ObjectMapper mapper = new ObjectMapper();
+ mapper.readValue(string, Task.class); // $unsafeDeserialization
+ });
+ }
+}
+
+class SaferPersonDeserialization {
+
+ // GOOD: Despite enabled polymorphic type handling, this is safe because ObjectMapper
+ // has a validator
+ private static void testSafeDeserializationWithValidator() throws Exception {
+ JacksonTest.withSocket(string -> {
+ PolymorphicTypeValidator ptv =
+ BasicPolymorphicTypeValidator.builder()
+ .allowIfSubType("only.allowed.package")
+ .build();
+
+ ObjectMapper mapper = new ObjectMapper();
+ mapper.setPolymorphicTypeValidator(ptv);
+
+ mapper.readValue(string, Person.class);
+ });
+ }
+
+ // GOOD: Despite enabled polymorphic type handling, this is safe because ObjectMapper
+ // has a validator
+ private static void testSafeDeserializationWithValidatorAndBuilder() throws Exception {
+ JacksonTest.withSocket(string -> {
+ PolymorphicTypeValidator ptv =
+ BasicPolymorphicTypeValidator.builder()
+ .allowIfSubType("only.allowed.package")
+ .build();
+
+ ObjectMapper mapper = JsonMapper.builder()
+ .polymorphicTypeValidator(ptv)
+ .build();
+
+ mapper.readValue(string, Person.class);
+ });
+ }
+}
+
+class UnsafeCatDeserialization {
+
+ // BAD: deserializing untrusted input while polymorphic type handling is on
+ private static void testUnsafeDeserialization() throws Exception {
+ JacksonTest.withSocket(string -> {
+ ObjectMapper mapper = new ObjectMapper();
+ mapper.enableDefaultTyping(); // this enables polymorphic type handling
+ mapper.readValue(string, Cat.class); // $unsafeDeserialization
+ });
+ }
+
+ // BAD: deserializing untrusted input while polymorphic type handling is on
+ private static void testUnsafeDeserializationWithObjectMapperReadValues() throws Exception {
+ JacksonTest.withSocket(string -> {
+ ObjectMapper mapper = new ObjectMapper();
+ mapper.enableDefaultTyping();
+ mapper.readValues(new JsonFactory().createParser(string), Cat.class).readAll(); // $unsafeDeserialization
+ });
+ }
+
+ // BAD: deserializing untrusted input while polymorphic type handling is on
+ private static void testUnsafeDeserializationWithObjectMapperTreeToValue() throws Exception {
+ JacksonTest.withSocket(string -> {
+ ObjectMapper mapper = new ObjectMapper();
+ mapper.enableDefaultTyping();
+ mapper.treeToValue(mapper.readTree(string), Cat.class); // $unsafeDeserialization
+ });
+ }
+
+ // BAD: an attacker can control both data and type of deserialized object
+ private static void testUnsafeDeserializationWithUnsafeClass() throws Exception {
+ JacksonTest.withSocket(input -> {
+ String[] parts = input.split(";");
+ String data = parts[0];
+ String type = parts[1];
+ Class clazz = Class.forName(type);
+ ObjectMapper mapper = new ObjectMapper();
+ mapper.readValue(data, clazz); // $unsafeDeserialization
+ });
+ }
+
+ // BAD: an attacker can control both data and type of deserialized object
+ private static void testUnsafeDeserializationWithUnsafeClassAndCustomTypeResolver() throws Exception {
+ JacksonTest.withSocket(input -> {
+ String[] parts = input.split(";");
+ String data = parts[0];
+ String type = parts[1];
+ ObjectMapper mapper = new ObjectMapper();
+ mapper.readValue(data, resolveImpl(type, mapper)); // $unsafeDeserialization
+ });
+ }
+
+ private static JavaType resolveImpl(String type, ObjectMapper mapper) throws Exception {
+ return mapper.constructType(Class.forName(type));
+ }
+}
+
+class SaferCatDeserialization {
+
+ // GOOD: Despite enabled polymorphic type handling, this is safe because ObjectMapper
+ // has a validator
+ private static void testUnsafeDeserialization() throws Exception {
+ JacksonTest.withSocket(string -> {
+ PolymorphicTypeValidator ptv =
+ BasicPolymorphicTypeValidator.builder()
+ .allowIfSubType("only.allowed.pachage")
+ .build();
+
+ ObjectMapper mapper = JsonMapper.builder().polymorphicTypeValidator(ptv).build();
+ mapper.enableDefaultTyping(); // this enables polymorphic type handling
+
+ mapper.readValue(string, Cat.class);
+ });
+ }
+}
\ No newline at end of file
diff --git a/java/ql/test/query-tests/security/CWE-502/TestMessageBodyReader.java b/java/ql/test/query-tests/security/CWE-502/TestMessageBodyReader.java
index 7940b9c77010..2132041254da 100644
--- a/java/ql/test/query-tests/security/CWE-502/TestMessageBodyReader.java
+++ b/java/ql/test/query-tests/security/CWE-502/TestMessageBodyReader.java
@@ -19,7 +19,7 @@ public boolean isReadable(Class> type, Type genericType, Annotation[] annotati
public Object readFrom(Class type, Type genericType, Annotation[] annotations, MediaType mediaType,
MultivaluedMap httpHeaders, InputStream entityStream) throws IOException {
try {
- return new ObjectInputStream(entityStream).readObject();
+ return new ObjectInputStream(entityStream).readObject(); // $unsafeDeserialization
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
diff --git a/java/ql/test/query-tests/security/CWE-502/UnsafeDeserialization.expected b/java/ql/test/query-tests/security/CWE-502/UnsafeDeserialization.expected
index 7b02131cd734..e69de29bb2d1 100644
--- a/java/ql/test/query-tests/security/CWE-502/UnsafeDeserialization.expected
+++ b/java/ql/test/query-tests/security/CWE-502/UnsafeDeserialization.expected
@@ -1,229 +0,0 @@
-edges
-| A.java:13:31:13:51 | getInputStream(...) : InputStream | A.java:14:50:14:60 | inputStream : InputStream |
-| A.java:13:31:13:51 | getInputStream(...) : InputStream | A.java:15:12:15:13 | in |
-| A.java:14:28:14:61 | new ObjectInputStream(...) : ObjectInputStream | A.java:15:12:15:13 | in |
-| A.java:14:50:14:60 | inputStream : InputStream | A.java:14:28:14:61 | new ObjectInputStream(...) : ObjectInputStream |
-| A.java:19:31:19:51 | getInputStream(...) : InputStream | A.java:20:50:20:60 | inputStream : InputStream |
-| A.java:19:31:19:51 | getInputStream(...) : InputStream | A.java:21:12:21:13 | in |
-| A.java:20:28:20:61 | new ObjectInputStream(...) : ObjectInputStream | A.java:21:12:21:13 | in |
-| A.java:20:50:20:60 | inputStream : InputStream | A.java:20:28:20:61 | new ObjectInputStream(...) : ObjectInputStream |
-| A.java:25:31:25:51 | getInputStream(...) : InputStream | A.java:26:35:26:45 | inputStream : InputStream |
-| A.java:26:20:26:46 | new XMLDecoder(...) : XMLDecoder | A.java:27:12:27:12 | d |
-| A.java:26:35:26:45 | inputStream : InputStream | A.java:26:20:26:46 | new XMLDecoder(...) : XMLDecoder |
-| A.java:32:31:32:51 | getInputStream(...) : InputStream | A.java:33:43:33:53 | inputStream : InputStream |
-| A.java:33:21:33:54 | new InputStreamReader(...) : InputStreamReader | A.java:34:23:34:28 | reader |
-| A.java:33:43:33:53 | inputStream : InputStream | A.java:33:21:33:54 | new InputStreamReader(...) : InputStreamReader |
-| A.java:39:19:39:50 | new Input(...) : Input | A.java:40:28:40:32 | input |
-| A.java:39:19:39:50 | new Input(...) : Input | A.java:41:34:41:38 | input |
-| A.java:39:19:39:50 | new Input(...) : Input | A.java:42:40:42:44 | input |
-| A.java:39:29:39:49 | getInputStream(...) : InputStream | A.java:39:19:39:50 | new Input(...) : Input |
-| A.java:60:25:60:45 | getInputStream(...) : InputStream | A.java:61:26:61:30 | input |
-| A.java:60:25:60:45 | getInputStream(...) : InputStream | A.java:62:30:62:34 | input |
-| A.java:60:25:60:45 | getInputStream(...) : InputStream | A.java:63:50:63:54 | input : InputStream |
-| A.java:60:25:60:45 | getInputStream(...) : InputStream | A.java:64:24:64:28 | input |
-| A.java:60:25:60:45 | getInputStream(...) : InputStream | A.java:65:46:65:50 | input : InputStream |
-| A.java:63:50:63:54 | input : InputStream | A.java:63:28:63:55 | new InputStreamReader(...) |
-| A.java:65:46:65:50 | input : InputStream | A.java:65:24:65:51 | new InputStreamReader(...) |
-| A.java:70:25:70:45 | getInputStream(...) : InputStream | A.java:71:26:71:30 | input |
-| A.java:70:25:70:45 | getInputStream(...) : InputStream | A.java:72:30:72:34 | input |
-| A.java:70:25:70:45 | getInputStream(...) : InputStream | A.java:73:50:73:54 | input : InputStream |
-| A.java:70:25:70:45 | getInputStream(...) : InputStream | A.java:74:24:74:28 | input |
-| A.java:70:25:70:45 | getInputStream(...) : InputStream | A.java:75:46:75:50 | input : InputStream |
-| A.java:73:50:73:54 | input : InputStream | A.java:73:28:73:55 | new InputStreamReader(...) |
-| A.java:75:46:75:50 | input : InputStream | A.java:75:24:75:51 | new InputStreamReader(...) |
-| A.java:90:25:90:45 | getInputStream(...) : InputStream | A.java:91:26:91:30 | input |
-| A.java:90:25:90:45 | getInputStream(...) : InputStream | A.java:92:30:92:34 | input |
-| A.java:90:25:90:45 | getInputStream(...) : InputStream | A.java:93:50:93:54 | input : InputStream |
-| A.java:90:25:90:45 | getInputStream(...) : InputStream | A.java:94:24:94:28 | input |
-| A.java:90:25:90:45 | getInputStream(...) : InputStream | A.java:95:46:95:50 | input : InputStream |
-| A.java:93:50:93:54 | input : InputStream | A.java:93:28:93:55 | new InputStreamReader(...) |
-| A.java:95:46:95:50 | input : InputStream | A.java:95:24:95:51 | new InputStreamReader(...) |
-| B.java:7:31:7:51 | getInputStream(...) : InputStream | B.java:8:29:8:39 | inputStream |
-| B.java:12:31:12:51 | getInputStream(...) : InputStream | B.java:14:5:14:15 | inputStream : InputStream |
-| B.java:14:5:14:15 | inputStream : InputStream | B.java:14:22:14:26 | bytes [post update] : byte[] |
-| B.java:14:22:14:26 | bytes [post update] : byte[] | B.java:15:23:15:27 | bytes |
-| B.java:19:31:19:51 | getInputStream(...) : InputStream | B.java:21:5:21:15 | inputStream : InputStream |
-| B.java:21:5:21:15 | inputStream : InputStream | B.java:21:22:21:26 | bytes [post update] : byte[] |
-| B.java:21:22:21:26 | bytes [post update] : byte[] | B.java:23:29:23:29 | s |
-| B.java:27:31:27:51 | getInputStream(...) : InputStream | B.java:29:5:29:15 | inputStream : InputStream |
-| B.java:29:5:29:15 | inputStream : InputStream | B.java:29:22:29:26 | bytes [post update] : byte[] |
-| B.java:29:22:29:26 | bytes [post update] : byte[] | B.java:31:23:31:23 | s |
-| C.java:23:17:23:44 | getParameter(...) : String | C.java:24:13:24:16 | data |
-| C.java:23:17:23:44 | getParameter(...) : String | C.java:25:19:25:22 | data |
-| C.java:23:17:23:44 | getParameter(...) : String | C.java:26:25:26:28 | data |
-| C.java:23:17:23:44 | getParameter(...) : String | C.java:27:17:27:20 | data |
-| C.java:23:17:23:44 | getParameter(...) : String | C.java:30:19:30:22 | data |
-| C.java:23:17:23:44 | getParameter(...) : String | C.java:31:25:31:28 | data |
-| C.java:23:17:23:44 | getParameter(...) : String | C.java:32:31:32:34 | data |
-| C.java:23:17:23:44 | getParameter(...) : String | C.java:33:23:33:26 | data |
-| C.java:38:17:38:44 | getParameter(...) : String | C.java:43:25:43:28 | data |
-| C.java:38:17:38:44 | getParameter(...) : String | C.java:46:3:46:4 | jr |
-| C.java:51:17:51:44 | getParameter(...) : String | C.java:53:3:53:3 | r |
-| C.java:51:17:51:44 | getParameter(...) : String | C.java:54:3:54:3 | r |
-| C.java:51:17:51:44 | getParameter(...) : String | C.java:55:3:55:3 | r |
-| C.java:60:18:60:45 | getParameter(...) : String | C.java:61:55:61:59 | bytes : byte[] |
-| C.java:60:18:60:45 | getParameter(...) : String | C.java:63:3:63:14 | hessianInput |
-| C.java:60:18:60:45 | getParameter(...) : String | C.java:64:3:64:14 | hessianInput |
-| C.java:61:30:61:60 | new ByteArrayInputStream(...) : ByteArrayInputStream | C.java:63:3:63:14 | hessianInput |
-| C.java:61:30:61:60 | new ByteArrayInputStream(...) : ByteArrayInputStream | C.java:64:3:64:14 | hessianInput |
-| C.java:61:55:61:59 | bytes : byte[] | C.java:61:30:61:60 | new ByteArrayInputStream(...) : ByteArrayInputStream |
-| C.java:69:18:69:45 | getParameter(...) : String | C.java:70:55:70:59 | bytes : byte[] |
-| C.java:69:18:69:45 | getParameter(...) : String | C.java:72:3:72:14 | hessianInput |
-| C.java:69:18:69:45 | getParameter(...) : String | C.java:73:3:73:14 | hessianInput |
-| C.java:70:30:70:60 | new ByteArrayInputStream(...) : ByteArrayInputStream | C.java:72:3:72:14 | hessianInput |
-| C.java:70:30:70:60 | new ByteArrayInputStream(...) : ByteArrayInputStream | C.java:73:3:73:14 | hessianInput |
-| C.java:70:55:70:59 | bytes : byte[] | C.java:70:30:70:60 | new ByteArrayInputStream(...) : ByteArrayInputStream |
-| C.java:79:43:79:70 | getParameter(...) : String | C.java:79:26:79:71 | new StringReader(...) |
-| C.java:84:27:84:54 | getParameter(...) : String | C.java:85:54:85:67 | serializedData : byte[] |
-| C.java:84:27:84:54 | getParameter(...) : String | C.java:87:3:87:13 | burlapInput |
-| C.java:84:27:84:54 | getParameter(...) : String | C.java:91:3:91:14 | burlapInput1 |
-| C.java:85:29:85:68 | new ByteArrayInputStream(...) : ByteArrayInputStream | C.java:87:3:87:13 | burlapInput |
-| C.java:85:29:85:68 | new ByteArrayInputStream(...) : ByteArrayInputStream | C.java:91:3:91:14 | burlapInput1 |
-| C.java:85:54:85:67 | serializedData : byte[] | C.java:85:29:85:68 | new ByteArrayInputStream(...) : ByteArrayInputStream |
-| TestMessageBodyReader.java:20:55:20:78 | entityStream : InputStream | TestMessageBodyReader.java:22:18:22:52 | new ObjectInputStream(...) |
-| TestMessageBodyReader.java:20:55:20:78 | entityStream : InputStream | TestMessageBodyReader.java:22:40:22:51 | entityStream : InputStream |
-| TestMessageBodyReader.java:22:40:22:51 | entityStream : InputStream | TestMessageBodyReader.java:22:18:22:52 | new ObjectInputStream(...) |
-nodes
-| A.java:13:31:13:51 | getInputStream(...) : InputStream | semmle.label | getInputStream(...) : InputStream |
-| A.java:14:28:14:61 | new ObjectInputStream(...) : ObjectInputStream | semmle.label | new ObjectInputStream(...) : ObjectInputStream |
-| A.java:14:50:14:60 | inputStream : InputStream | semmle.label | inputStream : InputStream |
-| A.java:15:12:15:13 | in | semmle.label | in |
-| A.java:19:31:19:51 | getInputStream(...) : InputStream | semmle.label | getInputStream(...) : InputStream |
-| A.java:20:28:20:61 | new ObjectInputStream(...) : ObjectInputStream | semmle.label | new ObjectInputStream(...) : ObjectInputStream |
-| A.java:20:50:20:60 | inputStream : InputStream | semmle.label | inputStream : InputStream |
-| A.java:21:12:21:13 | in | semmle.label | in |
-| A.java:25:31:25:51 | getInputStream(...) : InputStream | semmle.label | getInputStream(...) : InputStream |
-| A.java:26:20:26:46 | new XMLDecoder(...) : XMLDecoder | semmle.label | new XMLDecoder(...) : XMLDecoder |
-| A.java:26:35:26:45 | inputStream : InputStream | semmle.label | inputStream : InputStream |
-| A.java:27:12:27:12 | d | semmle.label | d |
-| A.java:32:31:32:51 | getInputStream(...) : InputStream | semmle.label | getInputStream(...) : InputStream |
-| A.java:33:21:33:54 | new InputStreamReader(...) : InputStreamReader | semmle.label | new InputStreamReader(...) : InputStreamReader |
-| A.java:33:43:33:53 | inputStream : InputStream | semmle.label | inputStream : InputStream |
-| A.java:34:23:34:28 | reader | semmle.label | reader |
-| A.java:39:19:39:50 | new Input(...) : Input | semmle.label | new Input(...) : Input |
-| A.java:39:29:39:49 | getInputStream(...) : InputStream | semmle.label | getInputStream(...) : InputStream |
-| A.java:40:28:40:32 | input | semmle.label | input |
-| A.java:41:34:41:38 | input | semmle.label | input |
-| A.java:42:40:42:44 | input | semmle.label | input |
-| A.java:60:25:60:45 | getInputStream(...) : InputStream | semmle.label | getInputStream(...) : InputStream |
-| A.java:61:26:61:30 | input | semmle.label | input |
-| A.java:62:30:62:34 | input | semmle.label | input |
-| A.java:63:28:63:55 | new InputStreamReader(...) | semmle.label | new InputStreamReader(...) |
-| A.java:63:50:63:54 | input : InputStream | semmle.label | input : InputStream |
-| A.java:64:24:64:28 | input | semmle.label | input |
-| A.java:65:24:65:51 | new InputStreamReader(...) | semmle.label | new InputStreamReader(...) |
-| A.java:65:46:65:50 | input : InputStream | semmle.label | input : InputStream |
-| A.java:70:25:70:45 | getInputStream(...) : InputStream | semmle.label | getInputStream(...) : InputStream |
-| A.java:71:26:71:30 | input | semmle.label | input |
-| A.java:72:30:72:34 | input | semmle.label | input |
-| A.java:73:28:73:55 | new InputStreamReader(...) | semmle.label | new InputStreamReader(...) |
-| A.java:73:50:73:54 | input : InputStream | semmle.label | input : InputStream |
-| A.java:74:24:74:28 | input | semmle.label | input |
-| A.java:75:24:75:51 | new InputStreamReader(...) | semmle.label | new InputStreamReader(...) |
-| A.java:75:46:75:50 | input : InputStream | semmle.label | input : InputStream |
-| A.java:90:25:90:45 | getInputStream(...) : InputStream | semmle.label | getInputStream(...) : InputStream |
-| A.java:91:26:91:30 | input | semmle.label | input |
-| A.java:92:30:92:34 | input | semmle.label | input |
-| A.java:93:28:93:55 | new InputStreamReader(...) | semmle.label | new InputStreamReader(...) |
-| A.java:93:50:93:54 | input : InputStream | semmle.label | input : InputStream |
-| A.java:94:24:94:28 | input | semmle.label | input |
-| A.java:95:24:95:51 | new InputStreamReader(...) | semmle.label | new InputStreamReader(...) |
-| A.java:95:46:95:50 | input : InputStream | semmle.label | input : InputStream |
-| B.java:7:31:7:51 | getInputStream(...) : InputStream | semmle.label | getInputStream(...) : InputStream |
-| B.java:8:29:8:39 | inputStream | semmle.label | inputStream |
-| B.java:12:31:12:51 | getInputStream(...) : InputStream | semmle.label | getInputStream(...) : InputStream |
-| B.java:14:5:14:15 | inputStream : InputStream | semmle.label | inputStream : InputStream |
-| B.java:14:22:14:26 | bytes [post update] : byte[] | semmle.label | bytes [post update] : byte[] |
-| B.java:15:23:15:27 | bytes | semmle.label | bytes |
-| B.java:19:31:19:51 | getInputStream(...) : InputStream | semmle.label | getInputStream(...) : InputStream |
-| B.java:21:5:21:15 | inputStream : InputStream | semmle.label | inputStream : InputStream |
-| B.java:21:22:21:26 | bytes [post update] : byte[] | semmle.label | bytes [post update] : byte[] |
-| B.java:23:29:23:29 | s | semmle.label | s |
-| B.java:27:31:27:51 | getInputStream(...) : InputStream | semmle.label | getInputStream(...) : InputStream |
-| B.java:29:5:29:15 | inputStream : InputStream | semmle.label | inputStream : InputStream |
-| B.java:29:22:29:26 | bytes [post update] : byte[] | semmle.label | bytes [post update] : byte[] |
-| B.java:31:23:31:23 | s | semmle.label | s |
-| C.java:23:17:23:44 | getParameter(...) : String | semmle.label | getParameter(...) : String |
-| C.java:24:13:24:16 | data | semmle.label | data |
-| C.java:25:19:25:22 | data | semmle.label | data |
-| C.java:26:25:26:28 | data | semmle.label | data |
-| C.java:27:17:27:20 | data | semmle.label | data |
-| C.java:30:19:30:22 | data | semmle.label | data |
-| C.java:31:25:31:28 | data | semmle.label | data |
-| C.java:32:31:32:34 | data | semmle.label | data |
-| C.java:33:23:33:26 | data | semmle.label | data |
-| C.java:38:17:38:44 | getParameter(...) : String | semmle.label | getParameter(...) : String |
-| C.java:43:25:43:28 | data | semmle.label | data |
-| C.java:46:3:46:4 | jr | semmle.label | jr |
-| C.java:51:17:51:44 | getParameter(...) : String | semmle.label | getParameter(...) : String |
-| C.java:53:3:53:3 | r | semmle.label | r |
-| C.java:54:3:54:3 | r | semmle.label | r |
-| C.java:55:3:55:3 | r | semmle.label | r |
-| C.java:60:18:60:45 | getParameter(...) : String | semmle.label | getParameter(...) : String |
-| C.java:61:30:61:60 | new ByteArrayInputStream(...) : ByteArrayInputStream | semmle.label | new ByteArrayInputStream(...) : ByteArrayInputStream |
-| C.java:61:55:61:59 | bytes : byte[] | semmle.label | bytes : byte[] |
-| C.java:63:3:63:14 | hessianInput | semmle.label | hessianInput |
-| C.java:64:3:64:14 | hessianInput | semmle.label | hessianInput |
-| C.java:69:18:69:45 | getParameter(...) : String | semmle.label | getParameter(...) : String |
-| C.java:70:30:70:60 | new ByteArrayInputStream(...) : ByteArrayInputStream | semmle.label | new ByteArrayInputStream(...) : ByteArrayInputStream |
-| C.java:70:55:70:59 | bytes : byte[] | semmle.label | bytes : byte[] |
-| C.java:72:3:72:14 | hessianInput | semmle.label | hessianInput |
-| C.java:73:3:73:14 | hessianInput | semmle.label | hessianInput |
-| C.java:79:26:79:71 | new StringReader(...) | semmle.label | new StringReader(...) |
-| C.java:79:43:79:70 | getParameter(...) : String | semmle.label | getParameter(...) : String |
-| C.java:84:27:84:54 | getParameter(...) : String | semmle.label | getParameter(...) : String |
-| C.java:85:29:85:68 | new ByteArrayInputStream(...) : ByteArrayInputStream | semmle.label | new ByteArrayInputStream(...) : ByteArrayInputStream |
-| C.java:85:54:85:67 | serializedData : byte[] | semmle.label | serializedData : byte[] |
-| C.java:87:3:87:13 | burlapInput | semmle.label | burlapInput |
-| C.java:91:3:91:14 | burlapInput1 | semmle.label | burlapInput1 |
-| TestMessageBodyReader.java:20:55:20:78 | entityStream : InputStream | semmle.label | entityStream : InputStream |
-| TestMessageBodyReader.java:22:18:22:52 | new ObjectInputStream(...) | semmle.label | new ObjectInputStream(...) |
-| TestMessageBodyReader.java:22:40:22:51 | entityStream : InputStream | semmle.label | entityStream : InputStream |
-#select
-| A.java:15:12:15:26 | readObject(...) | A.java:13:31:13:51 | getInputStream(...) : InputStream | A.java:15:12:15:13 | in | Unsafe deserialization of $@. | A.java:13:31:13:51 | getInputStream(...) | user input |
-| A.java:21:12:21:28 | readUnshared(...) | A.java:19:31:19:51 | getInputStream(...) : InputStream | A.java:21:12:21:13 | in | Unsafe deserialization of $@. | A.java:19:31:19:51 | getInputStream(...) | user input |
-| A.java:27:12:27:25 | readObject(...) | A.java:25:31:25:51 | getInputStream(...) : InputStream | A.java:27:12:27:12 | d | Unsafe deserialization of $@. | A.java:25:31:25:51 | getInputStream(...) | user input |
-| A.java:34:12:34:29 | fromXML(...) | A.java:32:31:32:51 | getInputStream(...) : InputStream | A.java:34:23:34:28 | reader | Unsafe deserialization of $@. | A.java:32:31:32:51 | getInputStream(...) | user input |
-| A.java:40:12:40:42 | readObject(...) | A.java:39:29:39:49 | getInputStream(...) : InputStream | A.java:40:28:40:32 | input | Unsafe deserialization of $@. | A.java:39:29:39:49 | getInputStream(...) | user input |
-| A.java:41:12:41:48 | readObjectOrNull(...) | A.java:39:29:39:49 | getInputStream(...) : InputStream | A.java:41:34:41:38 | input | Unsafe deserialization of $@. | A.java:39:29:39:49 | getInputStream(...) | user input |
-| A.java:42:16:42:45 | readClassAndObject(...) | A.java:39:29:39:49 | getInputStream(...) : InputStream | A.java:42:40:42:44 | input | Unsafe deserialization of $@. | A.java:39:29:39:49 | getInputStream(...) | user input |
-| A.java:61:16:61:31 | load(...) | A.java:60:25:60:45 | getInputStream(...) : InputStream | A.java:61:26:61:30 | input | Unsafe deserialization of $@. | A.java:60:25:60:45 | getInputStream(...) | user input |
-| A.java:62:17:62:35 | loadAll(...) | A.java:60:25:60:45 | getInputStream(...) : InputStream | A.java:62:30:62:34 | input | Unsafe deserialization of $@. | A.java:60:25:60:45 | getInputStream(...) | user input |
-| A.java:63:17:63:56 | parse(...) | A.java:60:25:60:45 | getInputStream(...) : InputStream | A.java:63:28:63:55 | new InputStreamReader(...) | Unsafe deserialization of $@. | A.java:60:25:60:45 | getInputStream(...) | user input |
-| A.java:64:12:64:38 | loadAs(...) | A.java:60:25:60:45 | getInputStream(...) : InputStream | A.java:64:24:64:28 | input | Unsafe deserialization of $@. | A.java:60:25:60:45 | getInputStream(...) | user input |
-| A.java:65:12:65:61 | loadAs(...) | A.java:60:25:60:45 | getInputStream(...) : InputStream | A.java:65:24:65:51 | new InputStreamReader(...) | Unsafe deserialization of $@. | A.java:60:25:60:45 | getInputStream(...) | user input |
-| A.java:71:16:71:31 | load(...) | A.java:70:25:70:45 | getInputStream(...) : InputStream | A.java:71:26:71:30 | input | Unsafe deserialization of $@. | A.java:70:25:70:45 | getInputStream(...) | user input |
-| A.java:72:17:72:35 | loadAll(...) | A.java:70:25:70:45 | getInputStream(...) : InputStream | A.java:72:30:72:34 | input | Unsafe deserialization of $@. | A.java:70:25:70:45 | getInputStream(...) | user input |
-| A.java:73:17:73:56 | parse(...) | A.java:70:25:70:45 | getInputStream(...) : InputStream | A.java:73:28:73:55 | new InputStreamReader(...) | Unsafe deserialization of $@. | A.java:70:25:70:45 | getInputStream(...) | user input |
-| A.java:74:12:74:38 | loadAs(...) | A.java:70:25:70:45 | getInputStream(...) : InputStream | A.java:74:24:74:28 | input | Unsafe deserialization of $@. | A.java:70:25:70:45 | getInputStream(...) | user input |
-| A.java:75:12:75:61 | loadAs(...) | A.java:70:25:70:45 | getInputStream(...) : InputStream | A.java:75:24:75:51 | new InputStreamReader(...) | Unsafe deserialization of $@. | A.java:70:25:70:45 | getInputStream(...) | user input |
-| A.java:91:16:91:31 | load(...) | A.java:90:25:90:45 | getInputStream(...) : InputStream | A.java:91:26:91:30 | input | Unsafe deserialization of $@. | A.java:90:25:90:45 | getInputStream(...) | user input |
-| A.java:92:17:92:35 | loadAll(...) | A.java:90:25:90:45 | getInputStream(...) : InputStream | A.java:92:30:92:34 | input | Unsafe deserialization of $@. | A.java:90:25:90:45 | getInputStream(...) | user input |
-| A.java:93:17:93:56 | parse(...) | A.java:90:25:90:45 | getInputStream(...) : InputStream | A.java:93:28:93:55 | new InputStreamReader(...) | Unsafe deserialization of $@. | A.java:90:25:90:45 | getInputStream(...) | user input |
-| A.java:94:12:94:38 | loadAs(...) | A.java:90:25:90:45 | getInputStream(...) : InputStream | A.java:94:24:94:28 | input | Unsafe deserialization of $@. | A.java:90:25:90:45 | getInputStream(...) | user input |
-| A.java:95:12:95:61 | loadAs(...) | A.java:90:25:90:45 | getInputStream(...) : InputStream | A.java:95:24:95:51 | new InputStreamReader(...) | Unsafe deserialization of $@. | A.java:90:25:90:45 | getInputStream(...) | user input |
-| B.java:8:12:8:46 | parseObject(...) | B.java:7:31:7:51 | getInputStream(...) : InputStream | B.java:8:29:8:39 | inputStream | Unsafe deserialization of $@. | B.java:7:31:7:51 | getInputStream(...) | user input |
-| B.java:15:12:15:28 | parse(...) | B.java:12:31:12:51 | getInputStream(...) : InputStream | B.java:15:23:15:27 | bytes | Unsafe deserialization of $@. | B.java:12:31:12:51 | getInputStream(...) | user input |
-| B.java:23:12:23:30 | parseObject(...) | B.java:19:31:19:51 | getInputStream(...) : InputStream | B.java:23:29:23:29 | s | Unsafe deserialization of $@. | B.java:19:31:19:51 | getInputStream(...) | user input |
-| B.java:31:12:31:24 | parse(...) | B.java:27:31:27:51 | getInputStream(...) : InputStream | B.java:31:23:31:23 | s | Unsafe deserialization of $@. | B.java:27:31:27:51 | getInputStream(...) | user input |
-| C.java:24:3:24:17 | load(...) | C.java:23:17:23:44 | getParameter(...) : String | C.java:24:13:24:16 | data | Unsafe deserialization of $@. | C.java:23:17:23:44 | getParameter(...) | user input |
-| C.java:25:3:25:23 | loadStream(...) | C.java:23:17:23:44 | getParameter(...) : String | C.java:25:19:25:22 | data | Unsafe deserialization of $@. | C.java:23:17:23:44 | getParameter(...) | user input |
-| C.java:26:3:26:43 | loadStreamOfType(...) | C.java:23:17:23:44 | getParameter(...) : String | C.java:26:25:26:28 | data | Unsafe deserialization of $@. | C.java:23:17:23:44 | getParameter(...) | user input |
-| C.java:27:3:27:35 | loadType(...) | C.java:23:17:23:44 | getParameter(...) : String | C.java:27:17:27:20 | data | Unsafe deserialization of $@. | C.java:23:17:23:44 | getParameter(...) | user input |
-| C.java:30:3:30:23 | load(...) | C.java:23:17:23:44 | getParameter(...) : String | C.java:30:19:30:22 | data | Unsafe deserialization of $@. | C.java:23:17:23:44 | getParameter(...) | user input |
-| C.java:31:3:31:29 | loadStream(...) | C.java:23:17:23:44 | getParameter(...) : String | C.java:31:25:31:28 | data | Unsafe deserialization of $@. | C.java:23:17:23:44 | getParameter(...) | user input |
-| C.java:32:3:32:49 | loadStreamOfType(...) | C.java:23:17:23:44 | getParameter(...) : String | C.java:32:31:32:34 | data | Unsafe deserialization of $@. | C.java:23:17:23:44 | getParameter(...) | user input |
-| C.java:33:3:33:41 | loadType(...) | C.java:23:17:23:44 | getParameter(...) : String | C.java:33:23:33:26 | data | Unsafe deserialization of $@. | C.java:23:17:23:44 | getParameter(...) | user input |
-| C.java:43:3:43:29 | jsonToJava(...) | C.java:38:17:38:44 | getParameter(...) : String | C.java:43:25:43:28 | data | Unsafe deserialization of $@. | C.java:38:17:38:44 | getParameter(...) | user input |
-| C.java:46:3:46:17 | readObject(...) | C.java:38:17:38:44 | getParameter(...) : String | C.java:46:3:46:4 | jr | Unsafe deserialization of $@. | C.java:38:17:38:44 | getParameter(...) | user input |
-| C.java:53:3:53:10 | read(...) | C.java:51:17:51:44 | getParameter(...) : String | C.java:53:3:53:3 | r | Unsafe deserialization of $@. | C.java:51:17:51:44 | getParameter(...) | user input |
-| C.java:54:3:54:22 | read(...) | C.java:51:17:51:44 | getParameter(...) : String | C.java:54:3:54:3 | r | Unsafe deserialization of $@. | C.java:51:17:51:44 | getParameter(...) | user input |
-| C.java:55:3:55:36 | read(...) | C.java:51:17:51:44 | getParameter(...) : String | C.java:55:3:55:3 | r | Unsafe deserialization of $@. | C.java:51:17:51:44 | getParameter(...) | user input |
-| C.java:63:3:63:27 | readObject(...) | C.java:60:18:60:45 | getParameter(...) : String | C.java:63:3:63:14 | hessianInput | Unsafe deserialization of $@. | C.java:60:18:60:45 | getParameter(...) | user input |
-| C.java:64:3:64:39 | readObject(...) | C.java:60:18:60:45 | getParameter(...) : String | C.java:64:3:64:14 | hessianInput | Unsafe deserialization of $@. | C.java:60:18:60:45 | getParameter(...) | user input |
-| C.java:72:3:72:27 | readObject(...) | C.java:69:18:69:45 | getParameter(...) : String | C.java:72:3:72:14 | hessianInput | Unsafe deserialization of $@. | C.java:69:18:69:45 | getParameter(...) | user input |
-| C.java:73:3:73:39 | readObject(...) | C.java:69:18:69:45 | getParameter(...) : String | C.java:73:3:73:14 | hessianInput | Unsafe deserialization of $@. | C.java:69:18:69:45 | getParameter(...) | user input |
-| C.java:79:3:79:72 | unmarshal(...) | C.java:79:43:79:70 | getParameter(...) : String | C.java:79:26:79:71 | new StringReader(...) | Unsafe deserialization of $@. | C.java:79:43:79:70 | getParameter(...) | user input |
-| C.java:87:3:87:26 | readObject(...) | C.java:84:27:84:54 | getParameter(...) : String | C.java:87:3:87:13 | burlapInput | Unsafe deserialization of $@. | C.java:84:27:84:54 | getParameter(...) | user input |
-| C.java:91:3:91:27 | readObject(...) | C.java:84:27:84:54 | getParameter(...) : String | C.java:91:3:91:14 | burlapInput1 | Unsafe deserialization of $@. | C.java:84:27:84:54 | getParameter(...) | user input |
-| TestMessageBodyReader.java:22:18:22:65 | readObject(...) | TestMessageBodyReader.java:20:55:20:78 | entityStream : InputStream | TestMessageBodyReader.java:22:18:22:52 | new ObjectInputStream(...) | Unsafe deserialization of $@. | TestMessageBodyReader.java:20:55:20:78 | entityStream | user input |
diff --git a/java/ql/test/query-tests/security/CWE-502/UnsafeDeserialization.ql b/java/ql/test/query-tests/security/CWE-502/UnsafeDeserialization.ql
new file mode 100644
index 000000000000..a2ba654a5409
--- /dev/null
+++ b/java/ql/test/query-tests/security/CWE-502/UnsafeDeserialization.ql
@@ -0,0 +1,18 @@
+import java
+import semmle.code.java.security.UnsafeDeserializationQuery
+import TestUtilities.InlineExpectationsTest
+
+class UnsafeDeserializationTest extends InlineExpectationsTest {
+ UnsafeDeserializationTest() { this = "UnsafeDeserializationTest" }
+
+ override string getARelevantTag() { result = "unsafeDeserialization" }
+
+ override predicate hasActualResult(Location location, string element, string tag, string value) {
+ tag = "unsafeDeserialization" and
+ exists(DataFlow::Node sink, UnsafeDeserializationConfig conf | conf.hasFlowTo(sink) |
+ sink.getLocation() = location and
+ element = sink.toString() and
+ value = ""
+ )
+ }
+}
diff --git a/java/ql/test/query-tests/security/CWE-502/UnsafeDeserialization.qlref b/java/ql/test/query-tests/security/CWE-502/UnsafeDeserialization.qlref
deleted file mode 100644
index 30612dac5a58..000000000000
--- a/java/ql/test/query-tests/security/CWE-502/UnsafeDeserialization.qlref
+++ /dev/null
@@ -1 +0,0 @@
-Security/CWE/CWE-502/UnsafeDeserialization.ql
diff --git a/java/ql/test/query-tests/security/CWE-502/options b/java/ql/test/query-tests/security/CWE-502/options
index 03027487dce3..fc5cac9e843d 100644
--- a/java/ql/test/query-tests/security/CWE-502/options
+++ b/java/ql/test/query-tests/security/CWE-502/options
@@ -1 +1 @@
-//semmle-extractor-options: --javac-args -cp ${testdir}/../../../stubs/snakeyaml-1.21:${testdir}/../../../stubs/xstream-1.4.10:${testdir}/../../../stubs/kryo-4.0.2:${testdir}/../../../stubs/jsr311-api-1.1.1:${testdir}/../../../stubs/fastjson-1.2.74:${testdir}/../../../stubs/springframework-5.3.8:${testdir}/../../../stubs/servlet-api-2.4:${testdir}/../../../stubs/jyaml-1.3:${testdir}/../../../stubs/json-io-4.10.0:${testdir}/../../../stubs/yamlbeans-1.09:${testdir}/../../../stubs/hessian-4.0.38:${testdir}/../../../stubs/castor-1.4.1
+//semmle-extractor-options: --javac-args -cp ${testdir}/../../../stubs/snakeyaml-1.21:${testdir}/../../../stubs/xstream-1.4.10:${testdir}/../../../stubs/kryo-4.0.2:${testdir}/../../../stubs/jsr311-api-1.1.1:${testdir}/../../../stubs/fastjson-1.2.74:${testdir}/../../../stubs/springframework-5.3.8:${testdir}/../../../stubs/servlet-api-2.4:${testdir}/../../../stubs/jyaml-1.3:${testdir}/../../../stubs/json-io-4.10.0:${testdir}/../../../stubs/yamlbeans-1.09:${testdir}/../../../stubs/hessian-4.0.38:${testdir}/../../../stubs/castor-1.4.1:${testdir}/../../../stubs/jackson-databind-2.10
diff --git a/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/annotation/JsonTypeInfo.java b/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/annotation/JsonTypeInfo.java
new file mode 100644
index 000000000000..fb6733f40192
--- /dev/null
+++ b/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/annotation/JsonTypeInfo.java
@@ -0,0 +1,21 @@
+package com.fasterxml.jackson.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target({ElementType.ANNOTATION_TYPE, ElementType.TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface JsonTypeInfo {
+ JsonTypeInfo.Id use();
+
+ public static enum Id {
+ CLASS("@class"),
+ MINIMAL_CLASS("@c");
+
+ private Id(String defProp) { }
+
+ public String getDefaultPropertyName() { return null; }
+ }
+}
diff --git a/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/core/JsonFactory.java b/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/core/JsonFactory.java
index 06f71ab187d6..12696cd4397e 100644
--- a/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/core/JsonFactory.java
+++ b/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/core/JsonFactory.java
@@ -9,4 +9,8 @@ public JsonFactory() {
public JsonGenerator createGenerator(Writer writer) {
return new JsonGenerator();
}
+
+ public JsonParser createParser(String content) {
+ return null;
+ }
}
diff --git a/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/core/JsonParser.java b/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/core/JsonParser.java
new file mode 100644
index 000000000000..2c5527d50ab7
--- /dev/null
+++ b/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/core/JsonParser.java
@@ -0,0 +1,3 @@
+package com.fasterxml.jackson.core;
+
+public abstract class JsonParser {}
diff --git a/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/core/TreeNode.java b/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/core/TreeNode.java
new file mode 100644
index 000000000000..0d89838457a6
--- /dev/null
+++ b/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/core/TreeNode.java
@@ -0,0 +1,3 @@
+package com.fasterxml.jackson.core;
+
+public interface TreeNode {}
diff --git a/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/JavaType.java b/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/JavaType.java
new file mode 100644
index 000000000000..51fe62ca4b15
--- /dev/null
+++ b/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/JavaType.java
@@ -0,0 +1,3 @@
+package com.fasterxml.jackson.databind;
+
+public class JavaType {}
\ No newline at end of file
diff --git a/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/JsonNode.java b/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/JsonNode.java
index b04572cd4da1..06602e943f5a 100644
--- a/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/JsonNode.java
+++ b/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/JsonNode.java
@@ -1,8 +1,8 @@
package com.fasterxml.jackson.databind;
import java.util.*;
+import com.fasterxml.jackson.core.TreeNode;
-public abstract class JsonNode implements Iterable {
- public JsonNode() {
- }
+public abstract class JsonNode implements TreeNode, Iterable {
+ public JsonNode() {}
}
diff --git a/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/MappingIterator.java b/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/MappingIterator.java
index ac427ef01c9d..929676e64568 100644
--- a/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/MappingIterator.java
+++ b/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/MappingIterator.java
@@ -25,4 +25,8 @@ public void remove() {
public void close() throws IOException {
}
+
+ public List readAll() {
+ return null;
+ }
}
diff --git a/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/ObjectMapper.java b/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/ObjectMapper.java
index 71dc99a351da..754fcc43144b 100644
--- a/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/ObjectMapper.java
+++ b/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/ObjectMapper.java
@@ -1,5 +1,9 @@
package com.fasterxml.jackson.databind;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.TreeNode;
+import com.fasterxml.jackson.databind.jsontype.PolymorphicTypeValidator;
+import java.lang.reflect.Type;
import java.io.*;
import java.util.*;
@@ -38,4 +42,36 @@ public T valueToTree(Object fromValue) throws IllegalArgume
public T convertValue(Object fromValue, Class toValueType) throws IllegalArgumentException {
return null;
}
+
+ public ObjectMapper setPolymorphicTypeValidator(PolymorphicTypeValidator ptv) {
+ return null;
+ }
+
+ public ObjectMapper enableDefaultTyping() {
+ return null;
+ }
+
+ public T readValue(String content, Class valueType) {
+ return null;
+ }
+
+ public T readValue(String content, JavaType valueType) {
+ return null;
+ }
+
+ public MappingIterator readValues(JsonParser p, Class valueType) {
+ return null;
+ }
+
+ public T treeToValue(TreeNode n, Class valueType) {
+ return null;
+ }
+
+ public JsonNode readTree(String content) {
+ return null;
+ }
+
+ public JavaType constructType(Type t) {
+ return null;
+ }
}
diff --git a/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/cfg/MapperBuilder.java b/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/cfg/MapperBuilder.java
new file mode 100644
index 000000000000..db2c24c43628
--- /dev/null
+++ b/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/cfg/MapperBuilder.java
@@ -0,0 +1,9 @@
+package com.fasterxml.jackson.databind.cfg;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.jsontype.PolymorphicTypeValidator;
+
+public abstract class MapperBuilder> {
+ public M build() { return null; }
+ public B polymorphicTypeValidator(PolymorphicTypeValidator ptv) { return null; }
+}
diff --git a/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/json/JsonMapper.java b/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/json/JsonMapper.java
new file mode 100644
index 000000000000..adec92a9210a
--- /dev/null
+++ b/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/json/JsonMapper.java
@@ -0,0 +1,9 @@
+package com.fasterxml.jackson.databind.json;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.cfg.MapperBuilder;
+
+public class JsonMapper extends ObjectMapper {
+ public static JsonMapper.Builder builder() { return null; }
+ public static class Builder extends MapperBuilder {}
+}
\ No newline at end of file
diff --git a/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/jsontype/BasicPolymorphicTypeValidator.java b/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/jsontype/BasicPolymorphicTypeValidator.java
new file mode 100644
index 000000000000..243cd467b4b4
--- /dev/null
+++ b/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/jsontype/BasicPolymorphicTypeValidator.java
@@ -0,0 +1,10 @@
+package com.fasterxml.jackson.databind.jsontype;
+
+public class BasicPolymorphicTypeValidator extends PolymorphicTypeValidator {
+ public static BasicPolymorphicTypeValidator.Builder builder() { return null; }
+
+ public static class Builder {
+ public BasicPolymorphicTypeValidator.Builder allowIfSubType(final String prefixForSubType) { return null; }
+ public BasicPolymorphicTypeValidator build() { return null; }
+ }
+}
diff --git a/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/jsontype/PolymorphicTypeValidator.java b/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/jsontype/PolymorphicTypeValidator.java
new file mode 100644
index 000000000000..37e68d2c429f
--- /dev/null
+++ b/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/jsontype/PolymorphicTypeValidator.java
@@ -0,0 +1,3 @@
+package com.fasterxml.jackson.databind.jsontype;
+
+public abstract class PolymorphicTypeValidator {}