-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Java: Unsafe deserialization with Jackson #5900
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 19 commits
3eb2af1
97fca62
ea0991c
aefd210
24e4b68
704cc77
e9731cd
476843a
c98f1a4
09ae779
1b3516a
6d7cb48
3856527
c367c7e
e025307
035f7ac
47e4cf4
158a75e
7fec575
50497eb
893f84f
83a9b0e
1d3eb57
a4b0041
7959e76
6c973b5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
lgtm,codescanning | ||
* The "Deserialization of user-controlled data" (`java/unsafe-deserialization`) query | ||
now recognizes `Jackson` deserialization. |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,177 @@ | ||||||
/** | ||||||
* Provides classes and predicates for working with the Jackson serialization framework. | ||||||
*/ | ||||||
|
||||||
import java | ||||||
import semmle.code.java.Reflection | ||||||
import semmle.code.java.dataflow.FlowSources | ||||||
import semmle.code.java.dataflow.TaintTracking2 | ||||||
artem-smotrakov marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
|
||||||
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<JsonMapper,Builder>") | ||||||
} | ||||||
} | ||||||
|
||||||
private class JsonFactory extends RefType { | ||||||
JsonFactory() { hasQualifiedName("com.fasterxml.jackson.core", "JsonFactory") } | ||||||
} | ||||||
|
||||||
private class JsonParser extends RefType { | ||||||
JsonParser() { hasQualifiedName("com.fasterxml.jackson.core", "JsonParser") } | ||||||
} | ||||||
|
||||||
/** Type descriptors in Jackson libraries. */ | ||||||
artem-smotrakov marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
class JacksonTypeDescriptorType extends RefType { | ||||||
JacksonTypeDescriptorType() { | ||||||
this instanceof TypeClass or | ||||||
hasQualifiedName("com.fasterxml.jackson.databind", "JavaType") or | ||||||
hasQualifiedName("com.fasterxml.jackson.core.type", "TypeReference") | ||||||
} | ||||||
} | ||||||
|
||||||
/** Methods in `ObjectMapper` that deserialize data. */ | ||||||
artem-smotrakov marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
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. s */ | ||||||
artem-smotrakov marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
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 | ||||||
artem-smotrakov marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
* 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 | ||||||
hasFieldWithJsonTypeAnnotation(type.getAField().getType()) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this line be deleted? The predicate There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, it looks like the tests actually use this case? But then the restriction to only one or two
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, you're right. I've updated the query. |
||||||
} | ||||||
|
||||||
/** | ||||||
* 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 is external, takes a string, returns a type descriptor | ||||||
artem-smotrakov marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
* 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, int i, Expr arg | | ||||||
m = ma.getMethod() and arg = ma.getArgument(i) | ||||||
artem-smotrakov marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
| | ||||||
m.getReturnType() instanceof JacksonTypeDescriptorType and | ||||||
m.getName().toLowerCase().regexpMatch("resolve|load|class|type") and | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This doesn't quite do what you want, since There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yup, I'll update the regex. Thanks! |
||||||
arg.getType() instanceof TypeString and | ||||||
arg = fromNode.asExpr() and | ||||||
ma = toNode.asExpr() | ||||||
) | ||||||
} |
Uh oh!
There was an error while loading. Please reload this page.