mirror of
https://github.com/github/codeql.git
synced 2025-12-21 11:16:30 +01:00
Merge branch 'main' into atorralba/promote-groovy-injection
This commit is contained in:
@@ -17,7 +17,7 @@ from InstanceOfExpr ioe, RefType t, RefType ct
|
||||
where
|
||||
ioe.getExpr() instanceof ThisAccess and
|
||||
t = ioe.getExpr().getType() and
|
||||
ct = ioe.getTypeName().getType() and
|
||||
ct = ioe.getCheckedType() and
|
||||
ct.getASupertype*() = t
|
||||
select ioe,
|
||||
"Testing whether 'this' is an instance of $@ in $@ introduces a dependency cycle between the two types.",
|
||||
|
||||
@@ -15,7 +15,7 @@ import java
|
||||
from InstanceOfExpr ioe, RefType t, RefType ct
|
||||
where
|
||||
t = ioe.getExpr().getType() and
|
||||
ct = ioe.getTypeName().getType() and
|
||||
ct = ioe.getCheckedType() and
|
||||
ct = t.getASupertype+()
|
||||
select ioe,
|
||||
"There is no need to test whether an instance of $@ is also an instance of $@ - it always is.", t,
|
||||
|
||||
@@ -17,8 +17,8 @@ predicate instanceofInEquals(EqualsMethod m, InstanceOfExpr e) {
|
||||
m.fromSource() and
|
||||
e.getEnclosingCallable() = m and
|
||||
e.getExpr().(VarAccess).getVariable() = m.getParameter() and
|
||||
exists(Class instanceofType |
|
||||
instanceofType = e.getTypeName().getType() and
|
||||
exists(RefType instanceofType |
|
||||
instanceofType = e.getCheckedType() and
|
||||
not instanceofType.isFinal()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ class LockObjectField extends Field {
|
||||
class ValidSynchStmt extends Stmt {
|
||||
ValidSynchStmt() {
|
||||
// It's OK to lock the enclosing class.
|
||||
this.(SynchronizedStmt).getExpr().(TypeLiteral).getTypeName().getType() =
|
||||
this.(SynchronizedStmt).getExpr().(TypeLiteral).getReferencedType() =
|
||||
this.getEnclosingCallable().getDeclaringType()
|
||||
or
|
||||
// It's OK to lock on a "lock object field".
|
||||
|
||||
@@ -24,7 +24,7 @@ import java
|
||||
predicate isSynchronizedByBlock(Method m) {
|
||||
exists(SynchronizedStmt sync, Expr on | sync = m.getBody().getAChild*() and on = sync.getExpr() |
|
||||
if m.isStatic()
|
||||
then on.(TypeLiteral).getTypeName().getType() = m.getDeclaringType()
|
||||
then on.(TypeLiteral).getReferencedType() = m.getDeclaringType()
|
||||
else on.(ThisAccess).getType().(RefType).getSourceDeclaration() = m.getDeclaringType()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import semmle.code.java.dataflow.SSA
|
||||
/** `ioe` is of the form `va instanceof t`. */
|
||||
predicate instanceOfCheck(InstanceOfExpr ioe, VarAccess va, RefType t) {
|
||||
ioe.getExpr() = va and
|
||||
ioe.getTypeName().getType().(RefType).getSourceDeclaration() = t
|
||||
ioe.getCheckedType().getSourceDeclaration() = t
|
||||
}
|
||||
|
||||
/** Expression `e` assumes that `va` could be of type `t`. */
|
||||
|
||||
@@ -127,7 +127,9 @@ predicate potentiallyStatic(InnerClass c) {
|
||||
forall(InnerClass superOfNested | superOfNested = nested.getASourceSupertype+() |
|
||||
potentiallyStatic(superOfNested)
|
||||
)
|
||||
)
|
||||
) and
|
||||
// JUnit Nested test classes are required to be non-static.
|
||||
not c.hasAnnotation("org.junit.jupiter.api", "Nested")
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
public void evaluate(Socket socket) throws IOException {
|
||||
try (BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(socket.getInputStream()))) {
|
||||
|
||||
String expression = reader.readLine();
|
||||
// BAD: the user-provided expression is directly evaluated
|
||||
MVEL.eval(expression);
|
||||
}
|
||||
}
|
||||
|
||||
public void safeEvaluate(Socket socket) throws IOException {
|
||||
try (BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(socket.getInputStream()))) {
|
||||
|
||||
String expression = reader.readLine();
|
||||
// GOOD: the user-provided expression is validated before evaluation
|
||||
validateExpression(expression);
|
||||
MVEL.eval(expression);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateExpression(String expression) {
|
||||
// Validate that the expression does not contain unexpected code.
|
||||
// For instance, this can be done with allow-lists or deny-lists of code patterns.
|
||||
}
|
||||
@@ -3,11 +3,11 @@
|
||||
|
||||
<overview>
|
||||
<p>
|
||||
MVEL is an expression language based on Java-syntax.
|
||||
The language offers many features
|
||||
MVEL is an expression language based on Java-syntax,
|
||||
which offers many features
|
||||
including invocation of methods available in the JVM.
|
||||
If a MVEL expression is built using attacker-controlled data,
|
||||
and then evaluated, then it may allow the attacker to run arbitrary code.
|
||||
and then evaluated, then it may allow attackers to run arbitrary code.
|
||||
</p>
|
||||
</overview>
|
||||
|
||||
@@ -19,10 +19,12 @@ Including user input in a MVEL expression should be avoided.
|
||||
|
||||
<example>
|
||||
<p>
|
||||
The following example uses untrusted data to build a MVEL expression
|
||||
and then runs it in the default powerfull context.
|
||||
In the following sample, the first example uses untrusted data to build a MVEL expression
|
||||
and then runs it in the default context. In the second example, the untrusted data is
|
||||
validated with a custom method that checks that the expression does not contain unexpected code
|
||||
before evaluating it.
|
||||
</p>
|
||||
<sample src="UnsafeMvelExpressionEvaluation.java" />
|
||||
<sample src="MvelExpressionEvaluation.java" />
|
||||
</example>
|
||||
|
||||
<references>
|
||||
@@ -35,4 +37,4 @@ and then runs it in the default powerfull context.
|
||||
<a href="https://owasp.org/www-community/vulnerabilities/Expression_Language_Injection">Expression Language Injection</a>.
|
||||
</li>
|
||||
</references>
|
||||
</qhelp>
|
||||
</qhelp>
|
||||
@@ -11,9 +11,9 @@
|
||||
*/
|
||||
|
||||
import java
|
||||
import MvelInjectionLib
|
||||
import semmle.code.java.security.MvelInjectionQuery
|
||||
import DataFlow::PathGraph
|
||||
|
||||
from DataFlow::PathNode source, DataFlow::PathNode sink, MvelInjectionConfig conf
|
||||
from DataFlow::PathNode source, DataFlow::PathNode sink, MvelInjectionFlowConfig conf
|
||||
where conf.hasFlowPath(source, sink)
|
||||
select sink.getNode(), source, sink, "MVEL injection from $@.", source.getNode(), "this user input"
|
||||
@@ -14,8 +14,8 @@ may have unforeseen effects, such as the execution of arbitrary code.
|
||||
</p>
|
||||
<p>
|
||||
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 <code>ObjectInputStream</code>/<code>ObjectOutputStream</code>.
|
||||
supports Kryo, XmlDecoder, XStream, SnakeYaml, JYaml, JsonIO, YAMLBeans, HessianBurlap, Castor, Burlap,
|
||||
Jackson and Java IO serialization through <code>ObjectInputStream</code>/<code>ObjectOutputStream</code>.
|
||||
</p>
|
||||
</overview>
|
||||
|
||||
@@ -91,6 +91,15 @@ Remote code execution in JYaml library:
|
||||
JsonIO deserialization vulnerabilities:
|
||||
<a href="https://klezvirus.github.io/Advanced-Web-Hacking/Serialisation/">JsonIO deserialization</a>.
|
||||
</li>
|
||||
<li>
|
||||
Research by Moritz Bechler:
|
||||
<a href="https://www.github.com/mbechler/marshalsec/blob/master/marshalsec.pdf?raw=true">Java Unmarshaller Security - Turning your data into code execution</a>
|
||||
</li>
|
||||
<li>
|
||||
Blog posts by the developer of Jackson libraries:
|
||||
<a href="https://cowtowncoder.medium.com/on-jackson-cves-dont-panic-here-is-what-you-need-to-know-54cd0d6e8062">On Jackson CVEs: Don’t Panic — Here is what you need to know</a>
|
||||
<a href="https://cowtowncoder.medium.com/jackson-2-10-safe-default-typing-2d018f0ce2ba">Jackson 2.10: Safe Default Typing</a>
|
||||
</li>
|
||||
</references>
|
||||
|
||||
</qhelp>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -16,9 +16,9 @@ import java
|
||||
import semmle.code.java.Collections
|
||||
|
||||
predicate guardedByInstanceOf(VarAccess e, RefType t) {
|
||||
exists(IfStmt s, InstanceOfExpr instanceCheck, Type checkType |
|
||||
exists(IfStmt s, InstanceOfExpr instanceCheck, RefType checkType |
|
||||
s.getCondition() = instanceCheck and
|
||||
instanceCheck.getTypeName().getType() = checkType and
|
||||
instanceCheck.getCheckedType() = checkType and
|
||||
// The same variable appears as the subject of the `instanceof`.
|
||||
instanceCheck.getExpr() = e.getVariable().getAnAccess() and
|
||||
// The checked type is either the type itself, or a raw version. For example, it is usually
|
||||
|
||||
@@ -1,367 +0,0 @@
|
||||
import java
|
||||
import semmle.code.java.dataflow.FlowSources
|
||||
import semmle.code.java.dataflow.TaintTracking
|
||||
|
||||
/**
|
||||
* A taint-tracking configuration for unsafe user input
|
||||
* that is used to construct and evaluate a MVEL expression.
|
||||
*/
|
||||
class MvelInjectionConfig extends TaintTracking::Configuration {
|
||||
MvelInjectionConfig() { this = "MvelInjectionConfig" }
|
||||
|
||||
override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
|
||||
|
||||
override predicate isSink(DataFlow::Node sink) { sink instanceof MvelEvaluationSink }
|
||||
|
||||
override predicate isAdditionalTaintStep(DataFlow::Node node1, DataFlow::Node node2) {
|
||||
expressionCompilationStep(node1, node2) or
|
||||
createExpressionCompilerStep(node1, node2) or
|
||||
expressionCompilerCompileStep(node1, node2) or
|
||||
createCompiledAccExpressionStep(node1, node2) or
|
||||
scriptCompileStep(node1, node2) or
|
||||
createMvelCompiledScriptStep(node1, node2) or
|
||||
templateCompileStep(node1, node2) or
|
||||
createTemplateCompilerStep(node1, node2)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A sink for EL injection vulnerabilities via MVEL,
|
||||
* i.e. methods that run evaluation of a MVEL expression.
|
||||
*/
|
||||
class MvelEvaluationSink extends DataFlow::ExprNode {
|
||||
MvelEvaluationSink() {
|
||||
exists(StaticMethodAccess ma, Method m | m = ma.getMethod() |
|
||||
(
|
||||
m instanceof MvelEvalMethod or
|
||||
m instanceof TemplateRuntimeEvaluationMethod
|
||||
) and
|
||||
ma.getArgument(0) = asExpr()
|
||||
)
|
||||
or
|
||||
exists(MethodAccess ma, Method m | m = ma.getMethod() |
|
||||
m instanceof MvelScriptEngineEvaluationMethod and
|
||||
ma.getArgument(0) = asExpr()
|
||||
)
|
||||
or
|
||||
exists(MethodAccess ma, Method m | m = ma.getMethod() |
|
||||
(
|
||||
m instanceof ExecutableStatementEvaluationMethod or
|
||||
m instanceof CompiledExpressionEvaluationMethod or
|
||||
m instanceof CompiledAccExpressionEvaluationMethod or
|
||||
m instanceof AccessorEvaluationMethod or
|
||||
m instanceof CompiledScriptEvaluationMethod or
|
||||
m instanceof MvelCompiledScriptEvaluationMethod
|
||||
) and
|
||||
ma.getQualifier() = asExpr()
|
||||
)
|
||||
or
|
||||
exists(StaticMethodAccess ma, Method m | m = ma.getMethod() |
|
||||
m instanceof MvelRuntimeEvaluationMethod and
|
||||
ma.getArgument(1) = asExpr()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `node1` to `node2` is a dataflow step that compiles a MVEL expression
|
||||
* by callilng `MVEL.compileExpression(tainted)`.
|
||||
*/
|
||||
predicate expressionCompilationStep(DataFlow::Node node1, DataFlow::Node node2) {
|
||||
exists(StaticMethodAccess ma, Method m | ma.getMethod() = m |
|
||||
m.getDeclaringType() instanceof MVEL and
|
||||
m.hasName("compileExpression") and
|
||||
ma.getAnArgument() = node1.asExpr() and
|
||||
node2.asExpr() = ma
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `node1` to `node2` is a dataflow step creates `ExpressionCompiler`,
|
||||
* i.e. `new ExpressionCompiler(tainted)`.
|
||||
*/
|
||||
predicate createExpressionCompilerStep(DataFlow::Node node1, DataFlow::Node node2) {
|
||||
exists(ConstructorCall cc |
|
||||
cc.getConstructedType() instanceof ExpressionCompiler and
|
||||
cc = node2.asExpr() and
|
||||
cc.getArgument(0) = node1.asExpr()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `node1` to `node2` is a dataflow step creates `CompiledAccExpression`,
|
||||
* i.e. `new CompiledAccExpression(tainted, ...)`.
|
||||
*/
|
||||
predicate createCompiledAccExpressionStep(DataFlow::Node node1, DataFlow::Node node2) {
|
||||
exists(ConstructorCall cc |
|
||||
cc.getConstructedType() instanceof CompiledAccExpression and
|
||||
cc = node2.asExpr() and
|
||||
cc.getArgument(0) = node1.asExpr()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `node1` to `node2` is a dataflow step that compiles a MVEL expression
|
||||
* by calling `ExpressionCompiler.compile()`.
|
||||
*/
|
||||
predicate expressionCompilerCompileStep(DataFlow::Node node1, DataFlow::Node node2) {
|
||||
exists(MethodAccess ma, Method m | ma.getMethod() = m |
|
||||
m.getDeclaringType() instanceof ExpressionCompiler and
|
||||
m.hasName("compile") and
|
||||
ma = node2.asExpr() and
|
||||
ma.getQualifier() = node1.asExpr()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `node1` to `node2` is a dataflow step that compiles a script via `MvelScriptEngine`,
|
||||
* i.e. `engine.compile(tainted)` or `engine.compiledScript(tainted)`.
|
||||
*/
|
||||
predicate scriptCompileStep(DataFlow::Node node1, DataFlow::Node node2) {
|
||||
exists(MethodAccess ma, Method m | ma.getMethod() = m |
|
||||
m instanceof MvelScriptEngineCompilationMethod and
|
||||
ma = node2.asExpr() and
|
||||
ma.getArgument(0) = node1.asExpr()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `node1` to `node2` is a dataflow step creates `MvelCompiledScript`,
|
||||
* i.e. `new MvelCompiledScript(engine, tainted)`.
|
||||
*/
|
||||
predicate createMvelCompiledScriptStep(DataFlow::Node node1, DataFlow::Node node2) {
|
||||
exists(ConstructorCall cc |
|
||||
cc.getConstructedType() instanceof MvelCompiledScript and
|
||||
cc = node2.asExpr() and
|
||||
cc.getArgument(1) = node1.asExpr()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `node1` to `node2` is a dataflow step creates `TemplateCompiler`,
|
||||
* i.e. `new TemplateCompiler(tainted)`.
|
||||
*/
|
||||
predicate createTemplateCompilerStep(DataFlow::Node node1, DataFlow::Node node2) {
|
||||
exists(ConstructorCall cc |
|
||||
cc.getConstructedType() instanceof TemplateCompiler and
|
||||
cc = node2.asExpr() and
|
||||
cc.getArgument(0) = node1.asExpr()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `node1` to `node2` is a dataflow step that compiles a script via `TemplateCompiler`,
|
||||
* i.e. `compiler.compile()` or `TemplateCompiler.compileTemplate(tainted)`.
|
||||
*/
|
||||
predicate templateCompileStep(DataFlow::Node node1, DataFlow::Node node2) {
|
||||
exists(MethodAccess ma, Method m | ma.getMethod() = m |
|
||||
m instanceof TemplateCompilerCompileMethod and
|
||||
ma.getQualifier() = node1.asExpr() and
|
||||
ma = node2.asExpr()
|
||||
)
|
||||
or
|
||||
exists(StaticMethodAccess ma, Method m | ma.getMethod() = m |
|
||||
m instanceof TemplateCompilerCompileTemplateMethod and
|
||||
ma = node2.asExpr() and
|
||||
ma.getArgument(0) = node1.asExpr()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Methods in the MVEL class that evaluate a MVEL expression.
|
||||
*/
|
||||
class MvelEvalMethod extends Method {
|
||||
MvelEvalMethod() {
|
||||
getDeclaringType() instanceof MVEL and
|
||||
(
|
||||
hasName("eval") or
|
||||
hasName("executeExpression") or
|
||||
hasName("evalToBoolean") or
|
||||
hasName("evalToString") or
|
||||
hasName("executeAllExpression") or
|
||||
hasName("executeSetExpression")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Methods in `MVEL` class that compile a MVEL expression.
|
||||
*/
|
||||
class MvelCompileExpressionMethod extends Method {
|
||||
MvelCompileExpressionMethod() {
|
||||
getDeclaringType() instanceof MVEL and
|
||||
(
|
||||
hasName("compileExpression") or
|
||||
hasName("compileGetExpression") or
|
||||
hasName("compileSetExpression")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Methods in `ExecutableStatement` that evaluate a MVEL expression.
|
||||
*/
|
||||
class ExecutableStatementEvaluationMethod extends Method {
|
||||
ExecutableStatementEvaluationMethod() {
|
||||
getDeclaringType() instanceof ExecutableStatement and
|
||||
hasName("getValue")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Methods in `CompiledExpression` that evaluate a MVEL expression.
|
||||
*/
|
||||
class CompiledExpressionEvaluationMethod extends Method {
|
||||
CompiledExpressionEvaluationMethod() {
|
||||
getDeclaringType() instanceof CompiledExpression and
|
||||
hasName("getDirectValue")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Methods in `CompiledAccExpression` that evaluate a MVEL expression.
|
||||
*/
|
||||
class CompiledAccExpressionEvaluationMethod extends Method {
|
||||
CompiledAccExpressionEvaluationMethod() {
|
||||
getDeclaringType() instanceof CompiledAccExpression and
|
||||
hasName("getValue")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Methods in `Accessor` that evaluate a MVEL expression.
|
||||
*/
|
||||
class AccessorEvaluationMethod extends Method {
|
||||
AccessorEvaluationMethod() {
|
||||
getDeclaringType() instanceof Accessor and
|
||||
hasName("getValue")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Methods in `MvelScriptEngine` that evaluate a MVEL expression.
|
||||
*/
|
||||
class MvelScriptEngineEvaluationMethod extends Method {
|
||||
MvelScriptEngineEvaluationMethod() {
|
||||
getDeclaringType() instanceof MvelScriptEngine and
|
||||
(hasName("eval") or hasName("evaluate"))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Methods in `MvelScriptEngine` that compile a MVEL expression.
|
||||
*/
|
||||
class MvelScriptEngineCompilationMethod extends Method {
|
||||
MvelScriptEngineCompilationMethod() {
|
||||
getDeclaringType() instanceof MvelScriptEngine and
|
||||
(hasName("compile") or hasName("compiledScript"))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Methods in `CompiledScript` that evaluate a MVEL expression.
|
||||
*/
|
||||
class CompiledScriptEvaluationMethod extends Method {
|
||||
CompiledScriptEvaluationMethod() {
|
||||
getDeclaringType() instanceof CompiledScript and
|
||||
hasName("eval")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Methods in `TemplateRuntime` that evaluate a MVEL template.
|
||||
*/
|
||||
class TemplateRuntimeEvaluationMethod extends Method {
|
||||
TemplateRuntimeEvaluationMethod() {
|
||||
getDeclaringType() instanceof TemplateRuntime and
|
||||
(hasName("eval") or hasName("execute"))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* `TemplateCompiler.compile()` method compiles a MVEL template.
|
||||
*/
|
||||
class TemplateCompilerCompileMethod extends Method {
|
||||
TemplateCompilerCompileMethod() {
|
||||
getDeclaringType() instanceof TemplateCompiler and
|
||||
hasName("compile")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* `TemplateCompiler.compileTemplate(tainted)` static method compiles a MVEL template.
|
||||
*/
|
||||
class TemplateCompilerCompileTemplateMethod extends Method {
|
||||
TemplateCompilerCompileTemplateMethod() {
|
||||
getDeclaringType() instanceof TemplateCompiler and
|
||||
hasName("compileTemplate")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Methods in `MvelCompiledScript` that evaluate a MVEL expression.
|
||||
*/
|
||||
class MvelCompiledScriptEvaluationMethod extends Method {
|
||||
MvelCompiledScriptEvaluationMethod() {
|
||||
getDeclaringType() instanceof MvelCompiledScript and
|
||||
hasName("eval")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Methods in `MVELRuntime` that evaluate a MVEL expression.
|
||||
*/
|
||||
class MvelRuntimeEvaluationMethod extends Method {
|
||||
MvelRuntimeEvaluationMethod() {
|
||||
getDeclaringType() instanceof MVELRuntime and
|
||||
hasName("execute")
|
||||
}
|
||||
}
|
||||
|
||||
class MVEL extends RefType {
|
||||
MVEL() { hasQualifiedName("org.mvel2", "MVEL") }
|
||||
}
|
||||
|
||||
class ExpressionCompiler extends RefType {
|
||||
ExpressionCompiler() { hasQualifiedName("org.mvel2.compiler", "ExpressionCompiler") }
|
||||
}
|
||||
|
||||
class ExecutableStatement extends RefType {
|
||||
ExecutableStatement() { hasQualifiedName("org.mvel2.compiler", "ExecutableStatement") }
|
||||
}
|
||||
|
||||
class CompiledExpression extends RefType {
|
||||
CompiledExpression() { hasQualifiedName("org.mvel2.compiler", "CompiledExpression") }
|
||||
}
|
||||
|
||||
class CompiledAccExpression extends RefType {
|
||||
CompiledAccExpression() { hasQualifiedName("org.mvel2.compiler", "CompiledAccExpression") }
|
||||
}
|
||||
|
||||
class Accessor extends RefType {
|
||||
Accessor() { hasQualifiedName("org.mvel2.compiler", "Accessor") }
|
||||
}
|
||||
|
||||
class CompiledScript extends RefType {
|
||||
CompiledScript() { hasQualifiedName("javax.script", "CompiledScript") }
|
||||
}
|
||||
|
||||
class MvelScriptEngine extends RefType {
|
||||
MvelScriptEngine() { hasQualifiedName("org.mvel2.jsr223", "MvelScriptEngine") }
|
||||
}
|
||||
|
||||
class MvelCompiledScript extends RefType {
|
||||
MvelCompiledScript() { hasQualifiedName("org.mvel2.jsr223", "MvelCompiledScript") }
|
||||
}
|
||||
|
||||
class TemplateRuntime extends RefType {
|
||||
TemplateRuntime() { hasQualifiedName("org.mvel2.templates", "TemplateRuntime") }
|
||||
}
|
||||
|
||||
class TemplateCompiler extends RefType {
|
||||
TemplateCompiler() { hasQualifiedName("org.mvel2.templates", "TemplateCompiler") }
|
||||
}
|
||||
|
||||
class MVELRuntime extends RefType {
|
||||
MVELRuntime() { hasQualifiedName("org.mvel2", "MVELRuntime") }
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
public void evaluate(Socket socket) throws IOException {
|
||||
try (BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(socket.getInputStream()))) {
|
||||
|
||||
String expression = reader.readLine();
|
||||
MVEL.eval(expression);
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ all certificate errors are ignored. In the 'GOOD' case, certificate errors are r
|
||||
|
||||
<references>
|
||||
<li>Teamdev:
|
||||
<a href="https://jxbrowser.support.teamdev.com/support/discussions/topics/9000051708">
|
||||
<a href="https://jxbrowser-support.teamdev.com/release-notes/2019/v6-24.html">
|
||||
Changelog of JxBrowser 6.24</a>.</li>
|
||||
</references>
|
||||
</qhelp>
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
@Controller
|
||||
public class UnsafeReflection {
|
||||
|
||||
@RequestMapping(value = {"/service/{beanIdOrClassName}/{methodName}"}, method = {RequestMethod.POST}, consumes = {"application/json"}, produces = {"application/json"})
|
||||
public Object bad1(@PathVariable("beanIdOrClassName") String beanIdOrClassName, @PathVariable("methodName") String methodName, @RequestBody Map<String, Object> body) throws Exception {
|
||||
List<Object> rawData = null;
|
||||
try {
|
||||
rawData = (List<Object>)body.get("methodInput");
|
||||
} catch (Exception e) {
|
||||
return e;
|
||||
}
|
||||
return invokeService(beanIdOrClassName, methodName, null, rawData);
|
||||
}
|
||||
|
||||
@GetMapping(value = "uf1")
|
||||
public void good1(HttpServletRequest request) throws Exception {
|
||||
HashSet<String> hashSet = new HashSet<>();
|
||||
hashSet.add("com.example.test1");
|
||||
hashSet.add("com.example.test2");
|
||||
String className = request.getParameter("className");
|
||||
String parameterValue = request.getParameter("parameterValue");
|
||||
if (!hashSet.contains(className)){
|
||||
throw new Exception("Class not valid: " + className);
|
||||
}
|
||||
try {
|
||||
Class clazz = Class.forName(className);
|
||||
Object object = clazz.getDeclaredConstructors()[0].newInstance(parameterValue); //good
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping(value = "uf2")
|
||||
public void good2(HttpServletRequest request) throws Exception {
|
||||
String className = request.getParameter("className");
|
||||
String parameterValue = request.getParameter("parameterValue");
|
||||
if (!"com.example.test1".equals(className)){
|
||||
throw new Exception("Class not valid: " + className);
|
||||
}
|
||||
try {
|
||||
Class clazz = Class.forName(className);
|
||||
Object object = clazz.getDeclaredConstructors()[0].newInstance(parameterValue); //good
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private Object invokeService(String beanIdOrClassName, String methodName, MultipartFile[] files, List<Object> data) throws Exception {
|
||||
BeanFactory beanFactory = new BeanFactory();
|
||||
try {
|
||||
Object bean = null;
|
||||
Class<?> beanClass = Class.forName(beanIdOrClassName);
|
||||
bean = beanFactory.getBean(beanClass);
|
||||
byte b;
|
||||
int i;
|
||||
Method[] arrayOfMethod;
|
||||
for (i = (arrayOfMethod = bean.getClass().getMethods()).length, b = 0; b < i; ) {
|
||||
Method method = arrayOfMethod[b];
|
||||
if (!method.getName().equals(methodName)) {
|
||||
b++;
|
||||
continue;
|
||||
}
|
||||
Object result = method.invoke(bean, data);
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
return map;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
return e;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class BeanFactory {
|
||||
|
||||
private static HashMap<String, Object> classNameMap = new HashMap<>();
|
||||
|
||||
private static HashMap<Class<?>, Object> classMap = new HashMap<>();
|
||||
|
||||
static {
|
||||
classNameMap.put("xxxx", Runtime.getRuntime());
|
||||
classMap.put(Runtime.class, Runtime.getRuntime());
|
||||
}
|
||||
|
||||
public Object getBean(Class<?> clzz) {
|
||||
return classMap.get(clzz);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<!DOCTYPE qhelp PUBLIC "-//Semmle//qhelp//EN" "qhelp.dtd">
|
||||
<qhelp>
|
||||
|
||||
<overview>
|
||||
<p>
|
||||
Allowing users to freely choose the name of a class to instantiate could provide means to attack a vulnerable appplication.
|
||||
</p>
|
||||
</overview>
|
||||
|
||||
<recommendation>
|
||||
<p>
|
||||
Create a list of classes that are allowed to load reflectively and strictly verify the input to ensure that
|
||||
users can only instantiate classes or execute methods that ought to be allowed.
|
||||
</p>
|
||||
</recommendation>
|
||||
|
||||
<example>
|
||||
<p>
|
||||
The <code>bad</code> method shown below illustrate class loading with <code>Class.forName</code> without any check on the particular class being instantiated.
|
||||
The <code>good</code> methods illustrate some different ways to restrict which classes can be instantiated.
|
||||
</p>
|
||||
<sample src="UnsafeReflection.java" />
|
||||
|
||||
</example>
|
||||
|
||||
<references>
|
||||
|
||||
<li>
|
||||
Unsafe use of Reflection | OWASP:
|
||||
<a href="https://owasp.org/www-community/vulnerabilities/Unsafe_use_of_Reflection">Unsafe use of Reflection</a>.
|
||||
</li>
|
||||
<li>
|
||||
Java owasp: Classes should not be loaded dynamically:
|
||||
<a href="https://rules.sonarsource.com/java/tag/owasp/RSPEC-2658">Classes should not be loaded dynamically</a>.
|
||||
</li>
|
||||
</references>
|
||||
|
||||
</qhelp>
|
||||
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* @name Use of externally-controlled input to select classes or code ('unsafe reflection')
|
||||
* @description Use external input with reflection function to select the class or code to
|
||||
* be used, which brings serious security risks.
|
||||
* @kind path-problem
|
||||
* @problem.severity error
|
||||
* @precision high
|
||||
* @id java/unsafe-reflection
|
||||
* @tags security
|
||||
* external/cwe/cwe-470
|
||||
*/
|
||||
|
||||
import java
|
||||
import DataFlow
|
||||
import UnsafeReflectionLib
|
||||
import semmle.code.java.dataflow.DataFlow
|
||||
import semmle.code.java.dataflow.FlowSources
|
||||
import DataFlow::PathGraph
|
||||
|
||||
private class ContainsSanitizer extends DataFlow::BarrierGuard {
|
||||
ContainsSanitizer() { this.(MethodAccess).getMethod().hasName("contains") }
|
||||
|
||||
override predicate checks(Expr e, boolean branch) {
|
||||
e = this.(MethodAccess).getArgument(0) and branch = true
|
||||
}
|
||||
}
|
||||
|
||||
private class EqualsSanitizer extends DataFlow::BarrierGuard {
|
||||
EqualsSanitizer() { this.(MethodAccess).getMethod().hasName("equals") }
|
||||
|
||||
override predicate checks(Expr e, boolean branch) {
|
||||
e = [this.(MethodAccess).getArgument(0), this.(MethodAccess).getQualifier()] and
|
||||
branch = true
|
||||
}
|
||||
}
|
||||
|
||||
class UnsafeReflectionConfig extends TaintTracking::Configuration {
|
||||
UnsafeReflectionConfig() { this = "UnsafeReflectionConfig" }
|
||||
|
||||
override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
|
||||
|
||||
override predicate isSink(DataFlow::Node sink) { sink instanceof UnsafeReflectionSink }
|
||||
|
||||
override predicate isAdditionalTaintStep(DataFlow::Node pred, DataFlow::Node succ) {
|
||||
// Argument -> return of Class.forName, ClassLoader.loadClass
|
||||
exists(ReflectiveClassIdentifierMethodAccess rcimac |
|
||||
rcimac.getArgument(0) = pred.asExpr() and rcimac = succ.asExpr()
|
||||
)
|
||||
or
|
||||
// Qualifier -> return of Class.getDeclaredConstructors/Methods and similar
|
||||
exists(MethodAccess ma |
|
||||
(
|
||||
ma instanceof ReflectiveConstructorsAccess or
|
||||
ma instanceof ReflectiveMethodsAccess
|
||||
) and
|
||||
ma.getQualifier() = pred.asExpr() and
|
||||
ma = succ.asExpr()
|
||||
)
|
||||
or
|
||||
// Qualifier -> return of Object.getClass
|
||||
exists(MethodAccess ma |
|
||||
ma.getMethod().hasName("getClass") and
|
||||
ma.getMethod().getDeclaringType().hasQualifiedName("java.lang", "Object") and
|
||||
ma.getQualifier() = pred.asExpr() and
|
||||
ma = succ.asExpr()
|
||||
)
|
||||
or
|
||||
// Argument -> return of methods that look like Class.forName
|
||||
looksLikeResolveClassStep(pred, succ)
|
||||
or
|
||||
// Argument -> return of methods that look like `Object getInstance(Class c)`
|
||||
looksLikeInstantiateClassStep(pred, succ)
|
||||
or
|
||||
// Qualifier -> return of Constructor.newInstance, Class.newInstance
|
||||
exists(NewInstance ni |
|
||||
ni.getQualifier() = pred.asExpr() and
|
||||
ni = succ.asExpr()
|
||||
)
|
||||
}
|
||||
|
||||
override predicate isSanitizerGuard(DataFlow::BarrierGuard guard) {
|
||||
guard instanceof ContainsSanitizer or guard instanceof EqualsSanitizer
|
||||
}
|
||||
}
|
||||
|
||||
private Expr getAMethodArgument(MethodAccess reflectiveCall) {
|
||||
result = reflectiveCall.(NewInstance).getAnArgument()
|
||||
or
|
||||
result = reflectiveCall.(MethodInvokeCall).getAnArgument()
|
||||
}
|
||||
|
||||
from
|
||||
DataFlow::PathNode source, DataFlow::PathNode sink, UnsafeReflectionConfig conf,
|
||||
MethodAccess reflectiveCall
|
||||
where
|
||||
conf.hasFlowPath(source, sink) and
|
||||
sink.getNode().asExpr() = reflectiveCall.getQualifier() and
|
||||
conf.hasFlowToExpr(getAMethodArgument(reflectiveCall))
|
||||
select sink.getNode(), source, sink, "Unsafe reflection of $@.", source.getNode(), "user input"
|
||||
@@ -0,0 +1,60 @@
|
||||
import java
|
||||
import DataFlow
|
||||
import semmle.code.java.Reflection
|
||||
import semmle.code.java.dataflow.FlowSources
|
||||
|
||||
/**
|
||||
* A call to `java.lang.reflect.Method.invoke`.
|
||||
*/
|
||||
class MethodInvokeCall extends MethodAccess {
|
||||
MethodInvokeCall() { this.getMethod().hasQualifiedName("java.lang.reflect", "Method", "invoke") }
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsafe reflection sink (the qualifier or method arguments to `Constructor.newInstance(...)` or `Method.invoke(...)`)
|
||||
*/
|
||||
class UnsafeReflectionSink extends DataFlow::ExprNode {
|
||||
UnsafeReflectionSink() {
|
||||
exists(MethodAccess ma |
|
||||
(
|
||||
ma.getMethod().hasQualifiedName("java.lang.reflect", "Constructor<>", "newInstance") or
|
||||
ma instanceof MethodInvokeCall
|
||||
) and
|
||||
this.asExpr() = [ma.getQualifier(), ma.getAnArgument()]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 Class
|
||||
* and its name contains "resolve", "load", etc.
|
||||
*/
|
||||
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)
|
||||
|
|
||||
m.getReturnType() instanceof TypeClass and
|
||||
m.getName().toLowerCase().regexpMatch("resolve|load|class|type") and
|
||||
arg.getType() instanceof TypeString and
|
||||
arg = fromNode.asExpr() and
|
||||
ma = toNode.asExpr()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `fromNode` to `toNode` is a dataflow step that looks like instantiating a class.
|
||||
* A method probably instantiates a class if it is external, takes a Class, returns an Object
|
||||
* and its name contains "instantiate" or similar terms.
|
||||
*/
|
||||
predicate looksLikeInstantiateClassStep(DataFlow::Node fromNode, DataFlow::Node toNode) {
|
||||
exists(MethodAccess ma, Method m, int i, Expr arg |
|
||||
m = ma.getMethod() and arg = ma.getArgument(i)
|
||||
|
|
||||
m.getReturnType() instanceof TypeObject and
|
||||
m.getName().toLowerCase().regexpMatch("instantiate|instance|create|make|getbean") and
|
||||
arg.getType() instanceof TypeClass and
|
||||
arg = fromNode.asExpr() and
|
||||
ma = toNode.asExpr()
|
||||
)
|
||||
}
|
||||
@@ -34,6 +34,29 @@ public class SpringUrlRedirect {
|
||||
}
|
||||
|
||||
@GetMapping("url5")
|
||||
public ResponseEntity<Void> bad5(String redirectUrl) {
|
||||
return ResponseEntity.status(HttpStatus.FOUND)
|
||||
.location(URI.create(redirectUrl))
|
||||
.build();
|
||||
}
|
||||
|
||||
@GetMapping("url6")
|
||||
public ResponseEntity<Void> bad6(String redirectUrl) {
|
||||
HttpHeaders httpHeaders = new HttpHeaders();
|
||||
httpHeaders.setLocation(URI.create(redirectUrl));
|
||||
|
||||
return new ResponseEntity<>(httpHeaders, HttpStatus.SEE_OTHER);
|
||||
}
|
||||
|
||||
@GetMapping("url7")
|
||||
public ResponseEntity<Void> bad7(String redirectUrl) {
|
||||
HttpHeaders httpHeaders = new HttpHeaders();
|
||||
httpHeaders.add("Location", redirectUrl);
|
||||
|
||||
return ResponseEntity.status(HttpStatus.SEE_OTHER).headers(httpHeaders).build();
|
||||
}
|
||||
|
||||
@GetMapping("url8")
|
||||
public RedirectView good1(String redirectUrl) {
|
||||
RedirectView rv = new RedirectView();
|
||||
if (redirectUrl.startsWith(VALID_REDIRECT)){
|
||||
|
||||
@@ -21,10 +21,10 @@ redirects on the server; then choose from that list based on the user input prov
|
||||
<example>
|
||||
|
||||
<p>The following examples show the bad case and the good case respectively.
|
||||
In <code>bad1</code> method and <code>bad2</code> method and <code>bad3</code> method and
|
||||
<code>bad4</code> method, shows an HTTP request parameter being used directly in a URL redirect
|
||||
without validating the input, which facilitates phishing attacks. In <code>good1</code> method,
|
||||
shows how to solve this problem by verifying whether the user input is a known fixed string beginning.
|
||||
The <code>bad</code> methods show an HTTP request parameter being used directly
|
||||
in a URL redirect without validating the input, which facilitates phishing attacks.
|
||||
In the <code>good1</code> method, it is shown how to solve this problem by verifying whether
|
||||
the user input is a known fixed string beginning.
|
||||
</p>
|
||||
|
||||
<sample src="SpringUrlRedirect.java" />
|
||||
@@ -33,5 +33,6 @@ shows how to solve this problem by verifying whether the user input is a known f
|
||||
<references>
|
||||
<li>A Guide To Spring Redirects: <a href="https://www.baeldung.com/spring-redirect-and-forward">Spring Redirects</a>.</li>
|
||||
<li>Url redirection - attack and defense: <a href="https://www.virtuesecurity.com/kb/url-redirection-attack-and-defense/">Url Redirection</a>.</li>
|
||||
<li>How to redirect to an external URL from Spring Boot REST Controller (Post/Redirect/Get pattern)?: <a href="https://fullstackdeveloper.guru/2021/03/12/how-to-redirect-to-an-external-url-from-spring-boot-rest-controller/">ResponseEntity Redirection</a>.</li>
|
||||
</references>
|
||||
</qhelp>
|
||||
|
||||
@@ -34,6 +34,10 @@ class SpringUrlRedirectFlowConfig extends TaintTracking::Configuration {
|
||||
|
||||
override predicate isSink(DataFlow::Node sink) { sink instanceof SpringUrlRedirectSink }
|
||||
|
||||
override predicate isAdditionalTaintStep(DataFlow::Node fromNode, DataFlow::Node toNode) {
|
||||
springUrlRedirectTaintStep(fromNode, toNode)
|
||||
}
|
||||
|
||||
override predicate isSanitizerGuard(DataFlow::BarrierGuard guard) {
|
||||
guard instanceof StartsWithSanitizer
|
||||
}
|
||||
@@ -57,6 +61,8 @@ class SpringUrlRedirectFlowConfig extends TaintTracking::Configuration {
|
||||
not ma.getArgument(0).(CompileTimeConstantExpr).getStringValue().regexpMatch("^%s.*")
|
||||
)
|
||||
)
|
||||
or
|
||||
nonLocationHeaderSanitizer(node)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,8 +35,13 @@ class RedirectAppendCall extends MethodAccess {
|
||||
}
|
||||
|
||||
/** A URL redirection sink from spring controller method. */
|
||||
class SpringUrlRedirectSink extends DataFlow::Node {
|
||||
SpringUrlRedirectSink() {
|
||||
abstract class SpringUrlRedirectSink extends DataFlow::Node { }
|
||||
|
||||
/**
|
||||
* A sink for URL Redirection via the Spring View classes.
|
||||
*/
|
||||
private class SpringViewUrlRedirectSink extends SpringUrlRedirectSink {
|
||||
SpringViewUrlRedirectSink() {
|
||||
exists(RedirectBuilderExpr rbe |
|
||||
rbe.getRightOperand() = this.asExpr() and
|
||||
any(SpringRequestMappingMethod sqmm).polyCalls*(this.getEnclosingCallable())
|
||||
@@ -71,3 +76,64 @@ class SpringUrlRedirectSink extends DataFlow::Node {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A sink for URL Redirection via the `ResponseEntity` class.
|
||||
*/
|
||||
private class SpringResponseEntityUrlRedirectSink extends SpringUrlRedirectSink {
|
||||
SpringResponseEntityUrlRedirectSink() {
|
||||
// Find `new ResponseEntity(httpHeaders, ...)` or
|
||||
// `new ResponseEntity(..., httpHeaders, ...)` sinks
|
||||
exists(ClassInstanceExpr cie, Argument argument |
|
||||
cie.getConstructedType() instanceof SpringResponseEntity and
|
||||
argument.getType() instanceof SpringHttpHeaders and
|
||||
argument = cie.getArgument([0, 1]) and
|
||||
this.asExpr() = argument
|
||||
)
|
||||
or
|
||||
// Find `ResponseEntity.status(...).headers(taintHeaders).build()` or
|
||||
// `ResponseEntity.status(...).location(URI.create(taintURL)).build()` sinks
|
||||
exists(MethodAccess ma |
|
||||
ma.getMethod()
|
||||
.getDeclaringType()
|
||||
.hasQualifiedName("org.springframework.http", "ResponseEntity$HeadersBuilder<BodyBuilder>") and
|
||||
ma.getMethod().getName() in ["headers", "location"] and
|
||||
this.asExpr() = ma.getArgument(0)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class HttpHeadersMethodAccess extends MethodAccess {
|
||||
HttpHeadersMethodAccess() { this.getMethod().getDeclaringType() instanceof SpringHttpHeaders }
|
||||
}
|
||||
|
||||
private class HttpHeadersAddSetMethodAccess extends HttpHeadersMethodAccess {
|
||||
HttpHeadersAddSetMethodAccess() { this.getMethod().getName() in ["add", "set"] }
|
||||
}
|
||||
|
||||
private class HttpHeadersSetLocationMethodAccess extends HttpHeadersMethodAccess {
|
||||
HttpHeadersSetLocationMethodAccess() { this.getMethod().hasName("setLocation") }
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `fromNode` to `toNode` is a dataflow step from a tainted argument to
|
||||
* a `HttpHeaders` instance qualifier, i.e. `httpHeaders.setLocation(tainted)`.
|
||||
*/
|
||||
predicate springUrlRedirectTaintStep(DataFlow::Node fromNode, DataFlow::Node toNode) {
|
||||
exists(HttpHeadersSetLocationMethodAccess ma |
|
||||
fromNode.asExpr() = ma.getArgument(0) and
|
||||
toNode.asExpr() = ma.getQualifier()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A sanitizer to exclude the cases where the `HttpHeaders.add` or `HttpHeaders.set`
|
||||
* methods are called with a HTTP header other than "Location".
|
||||
* E.g: `httpHeaders.add("X-Some-Header", taintedUrlString)`
|
||||
*/
|
||||
predicate nonLocationHeaderSanitizer(DataFlow::Node node) {
|
||||
exists(HttpHeadersAddSetMethodAccess ma, Argument firstArg | node.asExpr() = ma.getArgument(1) |
|
||||
firstArg = ma.getArgument(0) and
|
||||
not firstArg.(CompileTimeConstantExpr).getStringValue().matches("Location")
|
||||
)
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ predicate instantiates(RefType t, GenericType g, int i, RefType arg) {
|
||||
* - a class `MyIntMap<V> extends HashMap<Integer, V>` instantiates `Map` (among others)
|
||||
* with the `0`-th type parameter being `Integer` and the `1`-th type parameter being `V`.
|
||||
*/
|
||||
pragma[nomagic]
|
||||
predicate indirectlyInstantiates(RefType t, GenericType g, int i, RefType arg) {
|
||||
instantiates(t, g, i, arg)
|
||||
or
|
||||
|
||||
@@ -62,7 +62,7 @@ predicate depends(RefType t, RefType dep) {
|
||||
or
|
||||
// the type of a type literal accessed in `t`,
|
||||
exists(TypeLiteral l | l.getEnclosingCallable().getDeclaringType() = t |
|
||||
usesType(l.getTypeName().getType(), dep)
|
||||
usesType(l.getReferencedType(), dep)
|
||||
)
|
||||
or
|
||||
// the type of an annotation (or one of its element values) that annotates `t` or one of its members,
|
||||
@@ -76,7 +76,7 @@ predicate depends(RefType t, RefType dep) {
|
||||
or
|
||||
// the type accessed in an `instanceof` expression in `t`.
|
||||
exists(InstanceOfExpr ioe | t = ioe.getEnclosingCallable().getDeclaringType() |
|
||||
usesType(ioe.getTypeName().getType(), dep)
|
||||
usesType(ioe.getCheckedType(), dep)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ predicate numDepends(RefType t, RefType dep, int value) {
|
||||
elem = l and
|
||||
l.getEnclosingCallable().getDeclaringType() = t
|
||||
|
|
||||
usesType(l.getTypeName().getType(), dep)
|
||||
usesType(l.getReferencedType(), dep)
|
||||
)
|
||||
or
|
||||
// the type of an annotation (or one of its element values) that annotates `t` or one of its members,
|
||||
@@ -100,7 +100,7 @@ predicate numDepends(RefType t, RefType dep, int value) {
|
||||
elem = ioe and
|
||||
t = ioe.getEnclosingCallable().getDeclaringType()
|
||||
|
|
||||
usesType(ioe.getTypeName().getType(), dep)
|
||||
usesType(ioe.getCheckedType(), dep)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -46,17 +46,6 @@ class Expr extends ExprParent, @expr {
|
||||
*/
|
||||
int getKind() { exprs(this, result, _, _, _) }
|
||||
|
||||
/**
|
||||
* DEPRECATED: This is no longer necessary. See `Expr.isParenthesized()`.
|
||||
*
|
||||
* Gets this expression with any surrounding parentheses removed.
|
||||
*/
|
||||
deprecated Expr getProperExpr() {
|
||||
result = this.(ParExpr).getExpr().getProperExpr()
|
||||
or
|
||||
result = this and not this instanceof ParExpr
|
||||
}
|
||||
|
||||
/** Gets the statement containing this expression, if any. */
|
||||
Stmt getEnclosingStmt() { statementEnclosingExpr(this, result) }
|
||||
|
||||
@@ -1318,19 +1307,6 @@ class SwitchExpr extends Expr, StmtParent, @switchexpr {
|
||||
override string getAPrimaryQlClass() { result = "SwitchExpr" }
|
||||
}
|
||||
|
||||
/**
|
||||
* DEPRECATED: Use `Expr.isParenthesized()` instead.
|
||||
*
|
||||
* A parenthesised expression.
|
||||
*/
|
||||
deprecated class ParExpr extends Expr, @parexpr {
|
||||
/** Gets the expression inside the parentheses. */
|
||||
deprecated Expr getExpr() { result.getParent() = this }
|
||||
|
||||
/** Gets a printable representation of this expression. */
|
||||
override string toString() { result = "(...)" }
|
||||
}
|
||||
|
||||
/** An `instanceof` expression. */
|
||||
class InstanceOfExpr extends Expr, @instanceofexpr {
|
||||
/** Gets the expression on the left-hand side of the `instanceof` operator. */
|
||||
@@ -1357,6 +1333,9 @@ class InstanceOfExpr extends Expr, @instanceofexpr {
|
||||
/** Gets the access to the type on the right-hand side of the `instanceof` operator. */
|
||||
Expr getTypeName() { result.isNthChildOf(this, 1) }
|
||||
|
||||
/** Gets the type this `instanceof` expression checks for. */
|
||||
RefType getCheckedType() { result = getTypeName().getType() }
|
||||
|
||||
/** Gets a printable representation of this expression. */
|
||||
override string toString() { result = "...instanceof..." }
|
||||
|
||||
@@ -1449,6 +1428,12 @@ class TypeLiteral extends Expr, @typeliteral {
|
||||
/** Gets the access to the type whose class is accessed. */
|
||||
Expr getTypeName() { result.getParent() = this }
|
||||
|
||||
/**
|
||||
* Gets the type this type literal refers to. For example for `String.class` the
|
||||
* result is the type representing `String`.
|
||||
*/
|
||||
Type getReferencedType() { result = getTypeName().getType() }
|
||||
|
||||
/** Gets a printable representation of this expression. */
|
||||
override string toString() { result = this.getTypeName().toString() + ".class" }
|
||||
|
||||
|
||||
@@ -47,19 +47,22 @@ private XMLElement elementReferencingType(RefType rt) {
|
||||
}
|
||||
|
||||
abstract private class ReflectiveClassIdentifier extends Expr {
|
||||
/**
|
||||
* Gets the type of a class identified by this expression.
|
||||
*/
|
||||
abstract RefType getReflectivelyIdentifiedClass();
|
||||
}
|
||||
|
||||
private class ReflectiveClassIdentifierLiteral extends ReflectiveClassIdentifier, TypeLiteral {
|
||||
override RefType getReflectivelyIdentifiedClass() {
|
||||
result = getTypeName().getType().(RefType).getSourceDeclaration()
|
||||
result = getReferencedType().(RefType).getSourceDeclaration()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A call to a Java standard library method which constructs or returns a `Class<T>` from a `String`.
|
||||
*/
|
||||
library class ReflectiveClassIdentifierMethodAccess extends ReflectiveClassIdentifier, MethodAccess {
|
||||
class ReflectiveClassIdentifierMethodAccess extends ReflectiveClassIdentifier, MethodAccess {
|
||||
ReflectiveClassIdentifierMethodAccess() {
|
||||
// A call to `Class.forName(...)`, from which we can infer `T` in the returned type `Class<T>`.
|
||||
getCallee().getDeclaringType() instanceof TypeClass and getCallee().hasName("forName")
|
||||
@@ -314,6 +317,26 @@ class ClassMethodAccess extends MethodAccess {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A call to `Class.getConstructors(..)` or `Class.getDeclaredConstructors(..)`.
|
||||
*/
|
||||
class ReflectiveConstructorsAccess extends ClassMethodAccess {
|
||||
ReflectiveConstructorsAccess() {
|
||||
this.getCallee().hasName("getConstructors") or
|
||||
this.getCallee().hasName("getDeclaredConstructors")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A call to `Class.getMethods(..)` or `Class.getDeclaredMethods(..)`.
|
||||
*/
|
||||
class ReflectiveMethodsAccess extends ClassMethodAccess {
|
||||
ReflectiveMethodsAccess() {
|
||||
this.getCallee().hasName("getMethods") or
|
||||
this.getCallee().hasName("getDeclaredMethods")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A call to `Class.getMethod(..)` or `Class.getDeclaredMethod(..)`.
|
||||
*/
|
||||
|
||||
@@ -168,7 +168,7 @@ class TestNGTestMethod extends Method {
|
||||
or
|
||||
// Or the data provider class should be declared
|
||||
result.getDeclaringType() =
|
||||
testAnnotation.getValue("dataProviderClass").(TypeLiteral).getTypeName().getType()
|
||||
testAnnotation.getValue("dataProviderClass").(TypeLiteral).getReferencedType()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -227,7 +227,7 @@ class TestNGListenersAnnotation extends TestNGAnnotation {
|
||||
* Gets a listener defined in this annotation.
|
||||
*/
|
||||
TestNGListenerImpl getAListener() {
|
||||
result = getAValue("value").(TypeLiteral).getTypeName().getType()
|
||||
result = getAValue("value").(TypeLiteral).getReferencedType()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -303,7 +303,7 @@ class JUnitCategoryAnnotation extends Annotation {
|
||||
literal = value.(ArrayCreationExpr).getInit().getAnInit()
|
||||
)
|
||||
|
|
||||
result = literal.getTypeName().getType()
|
||||
result = literal.getReferencedType()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,6 +99,7 @@ private module Frameworks {
|
||||
private import semmle.code.java.security.GroovyInjection
|
||||
private import semmle.code.java.security.JexlInjectionSinkModels
|
||||
private import semmle.code.java.security.LdapInjection
|
||||
private import semmle.code.java.security.MvelInjection
|
||||
private import semmle.code.java.security.XPath
|
||||
private import semmle.code.java.frameworks.android.SQLite
|
||||
private import semmle.code.java.frameworks.Jdbc
|
||||
|
||||
@@ -25,8 +25,8 @@ Expr enumConstEquality(Expr e, boolean polarity, EnumConstant c) {
|
||||
}
|
||||
|
||||
/** Gets an instanceof expression of `v` with type `type` */
|
||||
InstanceOfExpr instanceofExpr(SsaVariable v, Type type) {
|
||||
result.getTypeName().getType() = type and
|
||||
InstanceOfExpr instanceofExpr(SsaVariable v, RefType type) {
|
||||
result.getCheckedType() = type and
|
||||
result.getExpr() = v.getAUse()
|
||||
}
|
||||
|
||||
|
||||
@@ -504,7 +504,7 @@ private predicate correlatedConditions(
|
||||
inverted = pol1.booleanXor(pol2)
|
||||
)
|
||||
or
|
||||
exists(SsaVariable v, Type type |
|
||||
exists(SsaVariable v, RefType type |
|
||||
cond1.getCondition() = instanceofExpr(v, type) and
|
||||
cond2.getCondition() = instanceofExpr(v, type) and
|
||||
inverted = false
|
||||
|
||||
@@ -299,7 +299,7 @@ private predicate downcastSuccessor(VarAccess va, RefType t) {
|
||||
private predicate instanceOfGuarded(VarAccess va, RefType t) {
|
||||
exists(InstanceOfExpr ioe, BaseSsaVariable v |
|
||||
ioe.getExpr() = v.getAUse() and
|
||||
t = ioe.getTypeName().getType() and
|
||||
t = ioe.getCheckedType() and
|
||||
va = v.getAUse() and
|
||||
guardControls_v1(ioe, va.getBasicBlock(), true)
|
||||
)
|
||||
@@ -311,7 +311,7 @@ private predicate instanceOfGuarded(VarAccess va, RefType t) {
|
||||
predicate arrayInstanceOfGuarded(ArrayAccess aa, RefType t) {
|
||||
exists(InstanceOfExpr ioe, BaseSsaVariable v1, BaseSsaVariable v2, ArrayAccess aa1 |
|
||||
ioe.getExpr() = aa1 and
|
||||
t = ioe.getTypeName().getType() and
|
||||
t = ioe.getCheckedType() and
|
||||
aa1.getArray() = v1.getAUse() and
|
||||
aa1.getIndexExpr() = v2.getAUse() and
|
||||
aa.getArray() = v1.getAUse() and
|
||||
|
||||
@@ -368,7 +368,44 @@ private class ContainerFlowSummaries extends SummaryModelCsv {
|
||||
"java.util;Collections;false;copy;(List,List);;Element of Argument[1];Element of Argument[0];value",
|
||||
"java.util;Collections;false;fill;(List,Object);;Argument[1];Element of Argument[0];value",
|
||||
"java.util;Arrays;false;asList;;;ArrayElement of Argument[0];Element of ReturnValue;value",
|
||||
"java.util;Collections;false;addAll;(Collection,Object[]);;ArrayElement of Argument[1];Element of Argument[0];value"
|
||||
"java.util;Collections;false;addAll;(Collection,Object[]);;ArrayElement of Argument[1];Element of Argument[0];value",
|
||||
"java.util;AbstractMap$SimpleEntry;false;SimpleEntry;(Object,Object);;Argument[0];MapKey of Argument[-1];value",
|
||||
"java.util;AbstractMap$SimpleEntry;false;SimpleEntry;(Object,Object);;Argument[1];MapValue of Argument[-1];value",
|
||||
"java.util;AbstractMap$SimpleEntry;false;SimpleEntry;(Entry);;MapKey of Argument[0];MapKey of Argument[-1];value",
|
||||
"java.util;AbstractMap$SimpleEntry;false;SimpleEntry;(Entry);;MapValue of Argument[0];MapValue of Argument[-1];value",
|
||||
"java.util;AbstractMap$SimpleImmutableEntry;false;SimpleImmutableEntry;(Object,Object);;Argument[0];MapKey of Argument[-1];value",
|
||||
"java.util;AbstractMap$SimpleImmutableEntry;false;SimpleImmutableEntry;(Object,Object);;Argument[1];MapValue of Argument[-1];value",
|
||||
"java.util;AbstractMap$SimpleImmutableEntry;false;SimpleImmutableEntry;(Entry);;MapKey of Argument[0];MapKey of Argument[-1];value",
|
||||
"java.util;AbstractMap$SimpleImmutableEntry;false;SimpleImmutableEntry;(Entry);;MapValue of Argument[0];MapValue of Argument[-1];value",
|
||||
"java.util;ArrayDeque;false;ArrayDeque;(Collection);;Element of Argument[0];Element of Argument[-1];value",
|
||||
"java.util;ArrayList;false;ArrayList;(Collection);;Element of Argument[0];Element of Argument[-1];value",
|
||||
"java.util;EnumMap;false;EnumMap;(Map);;MapKey of Argument[0];MapKey of Argument[-1];value",
|
||||
"java.util;EnumMap;false;EnumMap;(Map);;MapValue of Argument[0];MapValue of Argument[-1];value",
|
||||
"java.util;EnumMap;false;EnumMap;(EnumMap);;MapKey of Argument[0];MapKey of Argument[-1];value",
|
||||
"java.util;EnumMap;false;EnumMap;(EnumMap);;MapValue of Argument[0];MapValue of Argument[-1];value",
|
||||
"java.util;HashMap;false;HashMap;(Map);;MapKey of Argument[0];MapKey of Argument[-1];value",
|
||||
"java.util;HashMap;false;HashMap;(Map);;MapValue of Argument[0];MapValue of Argument[-1];value",
|
||||
"java.util;HashSet;false;HashSet;(Collection);;Element of Argument[0];Element of Argument[-1];value",
|
||||
"java.util;Hashtable;false;Hashtable;(Map);;MapKey of Argument[0];MapKey of Argument[-1];value",
|
||||
"java.util;Hashtable;false;Hashtable;(Map);;MapValue of Argument[0];MapValue of Argument[-1];value",
|
||||
"java.util;IdentityHashMap;false;IdentityHashMap;(Map);;MapKey of Argument[0];MapKey of Argument[-1];value",
|
||||
"java.util;IdentityHashMap;false;IdentityHashMap;(Map);;MapValue of Argument[0];MapValue of Argument[-1];value",
|
||||
"java.util;LinkedHashMap;false;LinkedHashMap;(Map);;MapKey of Argument[0];MapKey of Argument[-1];value",
|
||||
"java.util;LinkedHashMap;false;LinkedHashMap;(Map);;MapValue of Argument[0];MapValue of Argument[-1];value",
|
||||
"java.util;LinkedHashSet;false;LinkedHashSet;(Collection);;Element of Argument[0];Element of Argument[-1];value",
|
||||
"java.util;LinkedList;false;LinkedList;(Collection);;Element of Argument[0];Element of Argument[-1];value",
|
||||
"java.util;PriorityQueue;false;PriorityQueue;(Collection);;Element of Argument[0];Element of Argument[-1];value",
|
||||
"java.util;PriorityQueue;false;PriorityQueue;(PriorityQueue);;Element of Argument[0];Element of Argument[-1];value",
|
||||
"java.util;PriorityQueue;false;PriorityQueue;(SortedSet);;Element of Argument[0];Element of Argument[-1];value",
|
||||
"java.util;TreeMap;false;TreeMap;(Map);;MapKey of Argument[0];MapKey of Argument[-1];value",
|
||||
"java.util;TreeMap;false;TreeMap;(Map);;MapValue of Argument[0];MapValue of Argument[-1];value",
|
||||
"java.util;TreeMap;false;TreeMap;(SortedMap);;MapKey of Argument[0];MapKey of Argument[-1];value",
|
||||
"java.util;TreeMap;false;TreeMap;(SortedMap);;MapValue of Argument[0];MapValue of Argument[-1];value",
|
||||
"java.util;TreeSet;false;TreeSet;(Collection);;Element of Argument[0];Element of Argument[-1];value",
|
||||
"java.util;TreeSet;false;TreeSet;(SortedSet);;Element of Argument[0];Element of Argument[-1];value",
|
||||
"java.util;Vector;false;Vector;(Collection);;Element of Argument[0];Element of Argument[-1];value",
|
||||
"java.util;WeakHashMap;false;WeakHashMap;(Map);;MapKey of Argument[0];MapKey of Argument[-1];value",
|
||||
"java.util;WeakHashMap;false;WeakHashMap;(Map);;MapValue of Argument[0];MapValue of Argument[-1];value"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ private module DispatchImpl {
|
||||
* restricted to those `ma`s for which a context might make a difference.
|
||||
*/
|
||||
Method viableImplInCallContext(MethodAccess ma, Call ctx) {
|
||||
result = VirtualDispatch::viableImpl(ma) and
|
||||
result = viableCallable(ma) and
|
||||
exists(int i, Callable c, Method def, RefType t, boolean exact |
|
||||
mayBenefitFromCallContext(ma, c, i) and
|
||||
c = viableCallable(ctx) and
|
||||
@@ -115,6 +115,8 @@ private module DispatchImpl {
|
||||
result = VirtualDispatch::viableMethodImpl(def, t.getSourceDeclaration(), t2) and
|
||||
not failsUnification(t, t2)
|
||||
)
|
||||
or
|
||||
result = def and def instanceof SummarizedCallable
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -176,7 +176,7 @@ private module Dispatch {
|
||||
v.getAUse() = q and
|
||||
guardControls_v1(ioe, q.getBasicBlock(), false) and
|
||||
ioe.getExpr() = v.getAUse() and
|
||||
ioe.getTypeName().getType().getErasure() = t and
|
||||
ioe.getCheckedType().getErasure() = t and
|
||||
tgt.getDeclaringType().getSourceDeclaration().getASourceSupertype*() = t
|
||||
)
|
||||
}
|
||||
|
||||
@@ -64,5 +64,5 @@ class RunWithAnnotation extends Annotation {
|
||||
/**
|
||||
* Gets the runner that will be used.
|
||||
*/
|
||||
Type getRunner() { result = getValue("value").(TypeLiteral).getTypeName().getType() }
|
||||
Type getRunner() { result = getValue("value").(TypeLiteral).getReferencedType() }
|
||||
}
|
||||
|
||||
174
java/ql/src/semmle/code/java/frameworks/Jackson.qll
Normal file
174
java/ql/src/semmle/code/java/frameworks/Jackson.qll
Normal file
@@ -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<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") }
|
||||
}
|
||||
|
||||
/** 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()
|
||||
)
|
||||
}
|
||||
@@ -33,7 +33,7 @@ class HttpResponseParseAsDeserializableField extends DeserializableField {
|
||||
HttpResponseParseAsDeserializableField() {
|
||||
exists(RefType decltype, TypeLiteralToParseAsFlowConfiguration conf |
|
||||
decltype = getDeclaringType() and
|
||||
conf.getSourceWithFlowToParseAs().getTypeName().getType() = decltype and
|
||||
conf.getSourceWithFlowToParseAs().getReferencedType() = decltype and
|
||||
decltype.fromSource()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -112,7 +112,7 @@ private class TypeLiteralToJacksonDatabindFlowConfiguration extends DataFlow5::C
|
||||
private class ExplicitlyReadJacksonDeserializableType extends JacksonDeserializableType {
|
||||
ExplicitlyReadJacksonDeserializableType() {
|
||||
exists(TypeLiteralToJacksonDatabindFlowConfiguration conf |
|
||||
usesType(conf.getSourceWithFlowToJacksonDatabind().getTypeName().getType(), this)
|
||||
usesType(conf.getSourceWithFlowToJacksonDatabind().getReferencedType(), this)
|
||||
)
|
||||
or
|
||||
exists(MethodAccess ma |
|
||||
|
||||
@@ -199,7 +199,7 @@ abstract class EjbInterfaceAnnotation extends Annotation {
|
||||
// within the "value" element of this annotation.
|
||||
// Uses `getAChildExpr*()` since the "value" element can have type `Class` or `Class[]`.
|
||||
exists(TypeLiteral tl | tl = getValue("value").getAChildExpr*() |
|
||||
exists(TypeAccess ta | ta = tl.getTypeName() | result = ta.getType())
|
||||
result = tl.getReferencedType()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ class SpringComponentScan extends Annotation {
|
||||
// Base package classes are type literals whose package should be considered a base package.
|
||||
typeLiteral = getAValue("basePackageClasses")
|
||||
|
|
||||
result = typeLiteral.getTypeName().getType().(RefType).getPackage().getName()
|
||||
result = typeLiteral.getReferencedType().(RefType).getPackage().getName()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
233
java/ql/src/semmle/code/java/security/MvelInjection.qll
Normal file
233
java/ql/src/semmle/code/java/security/MvelInjection.qll
Normal file
@@ -0,0 +1,233 @@
|
||||
/** Provides classes to reason about MVEL injection attacks. */
|
||||
|
||||
import java
|
||||
private import semmle.code.java.dataflow.DataFlow
|
||||
private import semmle.code.java.dataflow.ExternalFlow
|
||||
|
||||
/** A data flow sink for unvalidated user input that is used to construct MVEL expressions. */
|
||||
abstract class MvelEvaluationSink extends DataFlow::Node { }
|
||||
|
||||
/** A sanitizer that prevents MVEL injection attacks. */
|
||||
abstract class MvelInjectionSanitizer extends DataFlow::Node { }
|
||||
|
||||
/**
|
||||
* A unit class for adding additional taint steps.
|
||||
*
|
||||
* Extend this class to add additional taint steps that should apply to the `MvelInjectionFlowConfig`.
|
||||
*/
|
||||
class MvelInjectionAdditionalTaintStep extends Unit {
|
||||
/**
|
||||
* Holds if the step from `node1` to `node2` should be considered a taint
|
||||
* step for the `MvelInjectionFlowConfig` configuration.
|
||||
*/
|
||||
abstract predicate step(DataFlow::Node n1, DataFlow::Node n2);
|
||||
}
|
||||
|
||||
/** Default sink for MVEL injection vulnerabilities. */
|
||||
private class DefaultMvelEvaluationSink extends MvelEvaluationSink {
|
||||
DefaultMvelEvaluationSink() { sinkNode(this, "mvel") }
|
||||
}
|
||||
|
||||
private class DefaulMvelEvaluationSinkModel extends SinkModelCsv {
|
||||
override predicate row(string row) {
|
||||
row =
|
||||
[
|
||||
"javax.script;CompiledScript;false;eval;;;Argument[-1];mvel",
|
||||
"org.mvel2;MVEL;false;eval;;;Argument[0];mvel",
|
||||
"org.mvel2;MVEL;false;executeExpression;;;Argument[0];mvel",
|
||||
"org.mvel2;MVEL;false;evalToBoolean;;;Argument[0];mvel",
|
||||
"org.mvel2;MVEL;false;evalToString;;;Argument[0];mvel",
|
||||
"org.mvel2;MVEL;false;executeAllExpression;;;Argument[0];mvel",
|
||||
"org.mvel2;MVEL;false;executeSetExpression;;;Argument[0];mvel",
|
||||
"org.mvel2;MVELRuntime;false;execute;;;Argument[1];mvel",
|
||||
"org.mvel2.templates;TemplateRuntime;false;eval;;;Argument[0];mvel",
|
||||
"org.mvel2.templates;TemplateRuntime;false;execute;;;Argument[0];mvel",
|
||||
"org.mvel2.jsr223;MvelScriptEngine;false;eval;;;Argument[0];mvel",
|
||||
"org.mvel2.jsr223;MvelScriptEngine;false;evaluate;;;Argument[0];mvel",
|
||||
"org.mvel2.jsr223;MvelCompiledScript;false;eval;;;Argument[-1];mvel",
|
||||
"org.mvel2.compiler;ExecutableStatement;false;getValue;;;Argument[-1];mvel",
|
||||
"org.mvel2.compiler;CompiledExpression;false;getDirectValue;;;Argument[-1];mvel",
|
||||
"org.mvel2.compiler;CompiledAccExpression;false;getValue;;;Argument[-1];mvel",
|
||||
"org.mvel2.compiler;Accessor;false;getValue;;;Argument[-1];mvel"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
/** A default sanitizer that considers numeric and boolean typed data safe for building MVEL expressions */
|
||||
private class DefaultMvelInjectionSanitizer extends MvelInjectionSanitizer {
|
||||
DefaultMvelInjectionSanitizer() {
|
||||
this.getType() instanceof NumericType or this.getType() instanceof BooleanType
|
||||
}
|
||||
}
|
||||
|
||||
/** A set of additional taint steps to consider when taint tracking MVEL related data flows. */
|
||||
private class DefaultMvelInjectionAdditionalTaintStep extends MvelInjectionAdditionalTaintStep {
|
||||
override predicate step(DataFlow::Node node1, DataFlow::Node node2) {
|
||||
expressionCompilationStep(node1, node2) or
|
||||
createExpressionCompilerStep(node1, node2) or
|
||||
expressionCompilerCompileStep(node1, node2) or
|
||||
createCompiledAccExpressionStep(node1, node2) or
|
||||
scriptCompileStep(node1, node2) or
|
||||
createMvelCompiledScriptStep(node1, node2) or
|
||||
templateCompileStep(node1, node2) or
|
||||
createTemplateCompilerStep(node1, node2)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `node1` to `node2` is a dataflow step that compiles a MVEL expression
|
||||
* by callilng `MVEL.compileExpression(tainted)`.
|
||||
*/
|
||||
private predicate expressionCompilationStep(DataFlow::Node node1, DataFlow::Node node2) {
|
||||
exists(StaticMethodAccess ma, Method m | ma.getMethod() = m |
|
||||
m.getDeclaringType() instanceof MVEL and
|
||||
m.hasName("compileExpression") and
|
||||
ma.getAnArgument() = node1.asExpr() and
|
||||
node2.asExpr() = ma
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `node1` to `node2` is a dataflow step that creates `ExpressionCompiler`
|
||||
* by calling `new ExpressionCompiler(tainted)`.
|
||||
*/
|
||||
private predicate createExpressionCompilerStep(DataFlow::Node node1, DataFlow::Node node2) {
|
||||
exists(ConstructorCall cc |
|
||||
cc.getConstructedType() instanceof ExpressionCompiler and
|
||||
cc = node2.asExpr() and
|
||||
cc.getArgument(0) = node1.asExpr()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `node1` to `node2` is a dataflow step creates `CompiledAccExpression`
|
||||
* by calling `new CompiledAccExpression(tainted, ...)`.
|
||||
*/
|
||||
private predicate createCompiledAccExpressionStep(DataFlow::Node node1, DataFlow::Node node2) {
|
||||
exists(ConstructorCall cc |
|
||||
cc.getConstructedType() instanceof CompiledAccExpression and
|
||||
cc = node2.asExpr() and
|
||||
cc.getArgument(0) = node1.asExpr()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `node1` to `node2` is a dataflow step that compiles a MVEL expression
|
||||
* by calling `ExpressionCompiler.compile()`.
|
||||
*/
|
||||
private predicate expressionCompilerCompileStep(DataFlow::Node node1, DataFlow::Node node2) {
|
||||
exists(MethodAccess ma, Method m | ma.getMethod() = m |
|
||||
m.getDeclaringType() instanceof ExpressionCompiler and
|
||||
m.hasName("compile") and
|
||||
ma = node2.asExpr() and
|
||||
ma.getQualifier() = node1.asExpr()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `node1` to `node2` is a dataflow step that compiles a script via `MvelScriptEngine`
|
||||
* by calling `engine.compile(tainted)` or `engine.compiledScript(tainted)`.
|
||||
*/
|
||||
private predicate scriptCompileStep(DataFlow::Node node1, DataFlow::Node node2) {
|
||||
exists(MethodAccess ma, Method m | ma.getMethod() = m |
|
||||
m instanceof MvelScriptEngineCompilationMethod and
|
||||
ma = node2.asExpr() and
|
||||
ma.getArgument(0) = node1.asExpr()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `node1` to `node2` is a dataflow step that creates `MvelCompiledScript`
|
||||
* by calling `new MvelCompiledScript(engine, tainted)`.
|
||||
*/
|
||||
private predicate createMvelCompiledScriptStep(DataFlow::Node node1, DataFlow::Node node2) {
|
||||
exists(ConstructorCall cc |
|
||||
cc.getConstructedType() instanceof MvelCompiledScript and
|
||||
cc = node2.asExpr() and
|
||||
cc.getArgument(1) = node1.asExpr()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `node1` to `node2` is a dataflow step creates `TemplateCompiler`
|
||||
* by calling `new TemplateCompiler(tainted)`.
|
||||
*/
|
||||
private predicate createTemplateCompilerStep(DataFlow::Node node1, DataFlow::Node node2) {
|
||||
exists(ConstructorCall cc |
|
||||
cc.getConstructedType() instanceof TemplateCompiler and
|
||||
cc = node2.asExpr() and
|
||||
cc.getArgument(0) = node1.asExpr()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `node1` to `node2` is a dataflow step that compiles a script via `TemplateCompiler`
|
||||
* by calling `compiler.compile()` or `TemplateCompiler.compileTemplate(tainted)`.
|
||||
*/
|
||||
private predicate templateCompileStep(DataFlow::Node node1, DataFlow::Node node2) {
|
||||
exists(MethodAccess ma, Method m | ma.getMethod() = m |
|
||||
m instanceof TemplateCompilerCompileMethod and
|
||||
ma.getQualifier() = node1.asExpr() and
|
||||
ma = node2.asExpr()
|
||||
)
|
||||
or
|
||||
exists(StaticMethodAccess ma, Method m | ma.getMethod() = m |
|
||||
m instanceof TemplateCompilerCompileTemplateMethod and
|
||||
ma = node2.asExpr() and
|
||||
ma.getArgument(0) = node1.asExpr()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Methods in `MvelScriptEngine` that compile a MVEL expression.
|
||||
*/
|
||||
private class MvelScriptEngineCompilationMethod extends Method {
|
||||
MvelScriptEngineCompilationMethod() {
|
||||
getDeclaringType() instanceof MvelScriptEngine and
|
||||
hasName(["compile", "compiledScript"])
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* `TemplateCompiler.compile()` method that compiles a MVEL template.
|
||||
*/
|
||||
private class TemplateCompilerCompileMethod extends Method {
|
||||
TemplateCompilerCompileMethod() {
|
||||
getDeclaringType() instanceof TemplateCompiler and
|
||||
hasName("compile")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* `TemplateCompiler.compileTemplate(tainted)` static method that compiles a MVEL template.
|
||||
*/
|
||||
private class TemplateCompilerCompileTemplateMethod extends Method {
|
||||
TemplateCompilerCompileTemplateMethod() {
|
||||
getDeclaringType() instanceof TemplateCompiler and
|
||||
hasName("compileTemplate")
|
||||
}
|
||||
}
|
||||
|
||||
private class MVEL extends RefType {
|
||||
MVEL() { hasQualifiedName("org.mvel2", "MVEL") }
|
||||
}
|
||||
|
||||
private class ExpressionCompiler extends RefType {
|
||||
ExpressionCompiler() { hasQualifiedName("org.mvel2.compiler", "ExpressionCompiler") }
|
||||
}
|
||||
|
||||
private class CompiledAccExpression extends RefType {
|
||||
CompiledAccExpression() { hasQualifiedName("org.mvel2.compiler", "CompiledAccExpression") }
|
||||
}
|
||||
|
||||
private class MvelScriptEngine extends RefType {
|
||||
MvelScriptEngine() { hasQualifiedName("org.mvel2.jsr223", "MvelScriptEngine") }
|
||||
}
|
||||
|
||||
private class MvelCompiledScript extends RefType {
|
||||
MvelCompiledScript() { hasQualifiedName("org.mvel2.jsr223", "MvelCompiledScript") }
|
||||
}
|
||||
|
||||
private class TemplateCompiler extends RefType {
|
||||
TemplateCompiler() { hasQualifiedName("org.mvel2.templates", "TemplateCompiler") }
|
||||
}
|
||||
26
java/ql/src/semmle/code/java/security/MvelInjectionQuery.qll
Normal file
26
java/ql/src/semmle/code/java/security/MvelInjectionQuery.qll
Normal file
@@ -0,0 +1,26 @@
|
||||
/** Provides taint tracking configurations to be used in MVEL injection related queries. */
|
||||
|
||||
import java
|
||||
import semmle.code.java.dataflow.FlowSources
|
||||
import semmle.code.java.dataflow.TaintTracking
|
||||
import semmle.code.java.security.MvelInjection
|
||||
|
||||
/**
|
||||
* A taint-tracking configuration for unsafe user input
|
||||
* that is used to construct and evaluate a MVEL expression.
|
||||
*/
|
||||
class MvelInjectionFlowConfig extends TaintTracking::Configuration {
|
||||
MvelInjectionFlowConfig() { this = "MvelInjectionFlowConfig" }
|
||||
|
||||
override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
|
||||
|
||||
override predicate isSink(DataFlow::Node sink) { sink instanceof MvelEvaluationSink }
|
||||
|
||||
override predicate isSanitizer(DataFlow::Node sanitizer) {
|
||||
sanitizer instanceof MvelInjectionSanitizer
|
||||
}
|
||||
|
||||
override predicate isAdditionalTaintStep(DataFlow::Node node1, DataFlow::Node node2) {
|
||||
any(MvelInjectionAdditionalTaintStep c).step(node1, node2)
|
||||
}
|
||||
}
|
||||
@@ -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()) }
|
||||
}
|
||||
@@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -12,8 +12,8 @@ import sys
|
||||
import tempfile
|
||||
|
||||
if any(s == "--help" for s in sys.argv):
|
||||
print("""Usage:
|
||||
GenerateFlowTestCase.py specsToTest.csv projectPom.xml outdir
|
||||
print("""Usage:
|
||||
GenerateFlowTestCase.py specsToTest.csv projectPom.xml outdir [--force]
|
||||
|
||||
This generates test cases exercising function model specifications found in specsToTest.csv
|
||||
producing files Test.java, test.ql and test.expected in outdir.
|
||||
@@ -22,32 +22,41 @@ projectPom.xml should be a Maven pom sufficient to resolve the classes named in
|
||||
Typically this means supplying a skeleton POM <dependencies> section that retrieves whatever jars
|
||||
contain the needed classes.
|
||||
|
||||
If --force is present, existing files may be overwritten.
|
||||
|
||||
Requirements: `mvn` and `codeql` should both appear on your path.
|
||||
|
||||
After test generation completes, any lines in specsToTest.csv that didn't produce tests are output.
|
||||
If this happens, check the spelling of class and method names, and the syntax of input and output specifications.
|
||||
""")
|
||||
sys.exit(0)
|
||||
sys.exit(0)
|
||||
|
||||
force = False
|
||||
if "--force" in sys.argv:
|
||||
sys.argv.remove("--force")
|
||||
force = True
|
||||
|
||||
if len(sys.argv) != 4:
|
||||
print("Usage: GenerateFlowTestCase.py specsToTest.csv projectPom.xml outdir", file=sys.stderr)
|
||||
print("specsToTest.csv should contain CSV rows describing method taint-propagation specifications to test", file=sys.stderr)
|
||||
print("projectPom.xml should import dependencies sufficient to resolve the types used in specsToTest.csv", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
print(
|
||||
"Usage: GenerateFlowTestCase.py specsToTest.csv projectPom.xml outdir [--force]", file=sys.stderr)
|
||||
print("specsToTest.csv should contain CSV rows describing method taint-propagation specifications to test", file=sys.stderr)
|
||||
print("projectPom.xml should import dependencies sufficient to resolve the types used in specsToTest.csv", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
os.makedirs(sys.argv[3])
|
||||
os.makedirs(sys.argv[3])
|
||||
except Exception as e:
|
||||
if e.errno != errno.EEXIST:
|
||||
print("Failed to create output directory %s: %s" % (sys.argv[3], e))
|
||||
sys.exit(1)
|
||||
if e.errno != errno.EEXIST:
|
||||
print("Failed to create output directory %s: %s" % (sys.argv[3], e))
|
||||
sys.exit(1)
|
||||
|
||||
resultJava = os.path.join(sys.argv[3], "Test.java")
|
||||
resultQl = os.path.join(sys.argv[3], "test.ql")
|
||||
|
||||
if os.path.exists(resultJava) or os.path.exists(resultQl):
|
||||
print("Won't overwrite existing files '%s' or '%s'" % (resultJava, resultQl), file = sys.stderr)
|
||||
sys.exit(1)
|
||||
if not force and (os.path.exists(resultJava) or os.path.exists(resultQl)):
|
||||
print("Won't overwrite existing files '%s' or '%s'" %
|
||||
(resultJava, resultQl), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
workDir = tempfile.mkdtemp()
|
||||
|
||||
@@ -57,129 +66,159 @@ projectDir = os.path.join(workDir, "mavenProject")
|
||||
os.makedirs(projectDir)
|
||||
|
||||
try:
|
||||
shutil.copyfile(sys.argv[2], os.path.join(projectDir, "pom.xml"))
|
||||
shutil.copyfile(sys.argv[2], os.path.join(projectDir, "pom.xml"))
|
||||
except Exception as e:
|
||||
print("Failed to read project POM %s: %s" % (sys.argv[2], e), file = sys.stderr)
|
||||
sys.exit(1)
|
||||
print("Failed to read project POM %s: %s" %
|
||||
(sys.argv[2], e), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
commentRegex = re.compile("^\s*(//|#)")
|
||||
|
||||
|
||||
def isComment(s):
|
||||
return commentRegex.match(s) is not None
|
||||
return commentRegex.match(s) is not None
|
||||
|
||||
|
||||
try:
|
||||
with open(sys.argv[1], "r") as f:
|
||||
specs = [l for l in f if not isComment(l)]
|
||||
with open(sys.argv[1], "r") as f:
|
||||
specs = [l for l in f if not isComment(l)]
|
||||
except Exception as e:
|
||||
print("Failed to open %s: %s\n" % (sys.argv[1], e))
|
||||
sys.exit(1)
|
||||
print("Failed to open %s: %s\n" % (sys.argv[1], e))
|
||||
sys.exit(1)
|
||||
|
||||
projectTestPkgDir = os.path.join(projectDir, "src", "main", "java", "test")
|
||||
projectTestFile = os.path.join(projectTestPkgDir, "Test.java")
|
||||
|
||||
os.makedirs(projectTestPkgDir)
|
||||
|
||||
|
||||
def qualifiedOuterNameFromCsvRow(row):
|
||||
cells = row.split(";")
|
||||
if len(cells) < 2:
|
||||
return None
|
||||
return cells[0] + "." + cells[1].replace("$", ".")
|
||||
cells = row.split(";")
|
||||
if len(cells) < 2:
|
||||
return None
|
||||
return cells[0] + "." + cells[1].replace("$", ".")
|
||||
|
||||
|
||||
with open(projectTestFile, "w") as testJava:
|
||||
testJava.write("package test;\n\npublic class Test {\n\n")
|
||||
testJava.write("package test;\n\npublic class Test {\n\n")
|
||||
|
||||
for i, spec in enumerate(specs):
|
||||
outerName = qualifiedOuterNameFromCsvRow(spec)
|
||||
if outerName is None:
|
||||
print("A taint specification has the wrong format: should be 'package;classname;methodname....'", file = sys.stderr)
|
||||
print("Mis-formatted row: " + spec, file = sys.stderr)
|
||||
sys.exit(1)
|
||||
testJava.write("\t%s obj%d = null;\n" % (outerName, i))
|
||||
for i, spec in enumerate(specs):
|
||||
outerName = qualifiedOuterNameFromCsvRow(spec)
|
||||
if outerName is None:
|
||||
print("A taint specification has the wrong format: should be 'package;classname;methodname....'", file=sys.stderr)
|
||||
print("Mis-formatted row: " + spec, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
testJava.write("\t%s obj%d = null;\n" % (outerName, i))
|
||||
|
||||
testJava.write("}")
|
||||
testJava.write("}")
|
||||
|
||||
print("Creating project database")
|
||||
cmd = ["codeql", "database", "create", "--language=java", "db"]
|
||||
ret = subprocess.call(cmd, cwd = projectDir)
|
||||
ret = subprocess.call(cmd, cwd=projectDir)
|
||||
if ret != 0:
|
||||
print("Failed to create project database. Check that '%s' is a valid POM that pulls in all necessary dependencies, and '%s' specifies valid classes and methods." % (sys.argv[2], sys.argv[1]), file = sys.stderr)
|
||||
print("Failed command was: %s (cwd: %s)" % (shlex.join(cmd), projectDir), file = sys.stderr)
|
||||
sys.exit(1)
|
||||
print("Failed to create project database. Check that '%s' is a valid POM that pulls in all necessary dependencies, and '%s' specifies valid classes and methods." % (
|
||||
sys.argv[2], sys.argv[1]), file=sys.stderr)
|
||||
print("Failed command was: %s (cwd: %s)" %
|
||||
(shlex.join(cmd), projectDir), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print("Creating test-generation query")
|
||||
queryDir = os.path.join(workDir, "query")
|
||||
os.makedirs(queryDir)
|
||||
qlFile = os.path.join(queryDir, "gen.ql")
|
||||
with open(os.path.join(queryDir, "qlpack.yml"), "w") as f:
|
||||
f.write("name: test-generation-query\nversion: 0.0.0\nlibraryPathDependencies: codeql-java")
|
||||
f.write("name: test-generation-query\nversion: 0.0.0\nlibraryPathDependencies: codeql-java")
|
||||
with open(qlFile, "w") as f:
|
||||
f.write("import java\nimport utils.GenerateFlowTestCase\n\nclass GenRow extends TargetSummaryModelCsv {\n\n\toverride predicate row(string r) {\n\t\tr = [\n")
|
||||
f.write(",\n".join('\t\t\t"%s"' % spec.strip() for spec in specs))
|
||||
f.write("\n\t\t]\n\t}\n}\n")
|
||||
f.write(
|
||||
"import java\nimport utils.GenerateFlowTestCase\n\nclass GenRow extends TargetSummaryModelCsv {\n\n\toverride predicate row(string r) {\n\t\tr = [\n")
|
||||
f.write(",\n".join('\t\t\t"%s"' % spec.strip() for spec in specs))
|
||||
f.write("\n\t\t]\n\t}\n}\n")
|
||||
|
||||
print("Generating tests")
|
||||
generatedBqrs = os.path.join(queryDir, "out.bqrs")
|
||||
cmd = ['codeql', 'query', 'run', qlFile, '--database', os.path.join(projectDir, "db"), '--output', generatedBqrs]
|
||||
cmd = ['codeql', 'query', 'run', qlFile, '--database',
|
||||
os.path.join(projectDir, "db"), '--output', generatedBqrs]
|
||||
ret = subprocess.call(cmd)
|
||||
if ret != 0:
|
||||
print("Failed to generate tests. Failed command was: " + shlex.join(cmd))
|
||||
sys.exit(1)
|
||||
print("Failed to generate tests. Failed command was: " + shlex.join(cmd))
|
||||
sys.exit(1)
|
||||
|
||||
generatedJson = os.path.join(queryDir, "out.json")
|
||||
cmd = ['codeql', 'bqrs', 'decode', generatedBqrs, '--format=json', '--output', generatedJson]
|
||||
cmd = ['codeql', 'bqrs', 'decode', generatedBqrs,
|
||||
'--format=json', '--output', generatedJson]
|
||||
ret = subprocess.call(cmd)
|
||||
if ret != 0:
|
||||
print("Failed to decode BQRS. Failed command was: " + shlex.join(cmd))
|
||||
sys.exit(1)
|
||||
print("Failed to decode BQRS. Failed command was: " + shlex.join(cmd))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def getTuples(queryName, jsonResult, fname):
|
||||
if queryName not in jsonResult or "tuples" not in jsonResult[queryName]:
|
||||
print("Failed to read generated tests: expected key '%s' with a 'tuples' subkey in file '%s'" % (queryName, fname), file = sys.stderr)
|
||||
sys.exit(1)
|
||||
return jsonResult[queryName]["tuples"]
|
||||
if queryName not in jsonResult or "tuples" not in jsonResult[queryName]:
|
||||
print("Failed to read generated tests: expected key '%s' with a 'tuples' subkey in file '%s'" % (
|
||||
queryName, fname), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
return jsonResult[queryName]["tuples"]
|
||||
|
||||
|
||||
with open(generatedJson, "r") as f:
|
||||
generateOutput = json.load(f)
|
||||
expectedTables = ("getTestCase", "getASupportMethodModel", "missingSummaryModelCsv", "getAParseFailure")
|
||||
generateOutput = json.load(f)
|
||||
expectedTables = ("getTestCase", "getASupportMethodModel",
|
||||
"missingSummaryModelCsv", "getAParseFailure", "noTestCaseGenerated")
|
||||
|
||||
testCaseRows, supportModelRows, missingSummaryModelCsvRows, parseFailureRows = \
|
||||
tuple([getTuples(k, generateOutput, generatedJson) for k in expectedTables])
|
||||
testCaseRows, supportModelRows, missingSummaryModelCsvRows, parseFailureRows, noTestCaseGeneratedRows = \
|
||||
tuple([getTuples(k, generateOutput, generatedJson)
|
||||
for k in expectedTables])
|
||||
|
||||
if len(testCaseRows) != 1 or len(testCaseRows[0]) != 1:
|
||||
print("Expected exactly one getTestCase result with one column (got: %s)" % json.dumps(testCaseRows), file = sys.stderr)
|
||||
if any(len(row) != 1 for row in supportModelRows):
|
||||
print("Expected exactly one column in getASupportMethodModel relation (got: %s)" % json.dumps(supportModelRows), file = sys.stderr)
|
||||
if any(len(row) != 2 for row in parseFailureRows):
|
||||
print("Expected exactly two columns in parseFailureRows relation (got: %s)" % json.dumps(parseFailureRows), file = sys.stderr)
|
||||
if len(testCaseRows) != 1 or len(testCaseRows[0]) != 1:
|
||||
print("Expected exactly one getTestCase result with one column (got: %s)" %
|
||||
json.dumps(testCaseRows), file=sys.stderr)
|
||||
if any(len(row) != 1 for row in supportModelRows):
|
||||
print("Expected exactly one column in getASupportMethodModel relation (got: %s)" %
|
||||
json.dumps(supportModelRows), file=sys.stderr)
|
||||
if any(len(row) != 2 for row in parseFailureRows):
|
||||
print("Expected exactly two columns in parseFailureRows relation (got: %s)" %
|
||||
json.dumps(parseFailureRows), file=sys.stderr)
|
||||
if any(len(row) != 1 for row in noTestCaseGeneratedRows):
|
||||
print("Expected exactly one column in noTestCaseGenerated relation (got: %s)" %
|
||||
json.dumps(noTestCaseGeneratedRows), file=sys.stderr)
|
||||
|
||||
if len(missingSummaryModelCsvRows) != 0:
|
||||
print("Tests for some CSV rows were requested that were not in scope (SummaryModelCsv.row does not hold):\n" + "\n".join(r[0] for r in missingSummaryModelCsvRows))
|
||||
sys.exit(1)
|
||||
if len(parseFailureRows) != 0:
|
||||
print("The following rows failed to generate any test case. Check package, class and method name spelling, and argument and result specifications:\n%s" % "\n".join(r[0] + ": " + r[1] for r in parseFailureRows), file = sys.stderr)
|
||||
sys.exit(1)
|
||||
if len(missingSummaryModelCsvRows) != 0:
|
||||
print("Tests for some CSV rows were requested that were not in scope (SummaryModelCsv.row does not hold):\n" +
|
||||
"\n".join(r[0] for r in missingSummaryModelCsvRows))
|
||||
sys.exit(1)
|
||||
if len(parseFailureRows) != 0:
|
||||
print("The following rows failed to generate any test case. Check package, class and method name spelling, and argument and result specifications:\n%s" %
|
||||
"\n".join(r[0] + ": " + r[1] for r in parseFailureRows), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
if len(noTestCaseGeneratedRows) != 0:
|
||||
print("The following CSV rows failed to generate any test case due to a limitation of the query. Other test cases will still be generated:\n" +
|
||||
"\n".join(r[0] for r in noTestCaseGeneratedRows))
|
||||
|
||||
with open(resultJava, "w") as f:
|
||||
f.write(generateOutput["getTestCase"]["tuples"][0][0])
|
||||
f.write(generateOutput["getTestCase"]["tuples"][0][0])
|
||||
|
||||
scriptPath = os.path.dirname(sys.argv[0])
|
||||
|
||||
|
||||
def copyfile(fromName, toFileHandle):
|
||||
with open(os.path.join(scriptPath, fromName), "r") as fromFileHandle:
|
||||
shutil.copyfileobj(fromFileHandle, toFileHandle)
|
||||
with open(os.path.join(scriptPath, fromName), "r") as fromFileHandle:
|
||||
shutil.copyfileobj(fromFileHandle, toFileHandle)
|
||||
|
||||
|
||||
with open(resultQl, "w") as f:
|
||||
copyfile("testHeader.qlfrag", f)
|
||||
if len(supportModelRows) != 0:
|
||||
copyfile("testModelsHeader.qlfrag", f)
|
||||
f.write(", ".join('"%s"' % modelSpecRow[0].strip() for modelSpecRow in supportModelRows))
|
||||
copyfile("testModelsFooter.qlfrag", f)
|
||||
copyfile("testFooter.qlfrag", f)
|
||||
copyfile("testHeader.qlfrag", f)
|
||||
if len(supportModelRows) != 0:
|
||||
copyfile("testModelsHeader.qlfrag", f)
|
||||
f.write(", ".join('"%s"' %
|
||||
modelSpecRow[0].strip() for modelSpecRow in supportModelRows))
|
||||
copyfile("testModelsFooter.qlfrag", f)
|
||||
copyfile("testFooter.qlfrag", f)
|
||||
|
||||
# Make an empty .expected file, since this is an inline-exectations test
|
||||
with open(os.path.join(sys.argv[3], "test.expected"), "w"):
|
||||
pass
|
||||
pass
|
||||
|
||||
cmd = ['codeql', 'query', 'format', '-qq', '-i', resultQl]
|
||||
subprocess.call(cmd)
|
||||
|
||||
shutil.rmtree(workDir)
|
||||
shutil.rmtree(workDir)
|
||||
|
||||
@@ -37,6 +37,9 @@ query string getAParseFailure(string reason) {
|
||||
any(TargetSummaryModelCsv target).row(result) and
|
||||
any(SummaryModelCsv model).row(result) and
|
||||
(
|
||||
not summaryModel(_, _, _, _, _, _, _, _, _, result) and
|
||||
reason = "row could not be parsed"
|
||||
or
|
||||
exists(
|
||||
string namespace, string type, boolean subtypes, string name, string signature, string ext
|
||||
|
|
||||
@@ -59,13 +62,26 @@ query string getAParseFailure(string reason) {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a CSV row for which a test was requested and was correctly parsed,
|
||||
* but for which no test case could be generated due to a limitation of the query.
|
||||
*/
|
||||
query string noTestCaseGenerated() {
|
||||
any(TargetSummaryModelCsv target).row(result) and
|
||||
any(SummaryModelCsv model).row(result) and
|
||||
not exists(getAParseFailure(_)) and
|
||||
not exists(any(TestCase tc).getATestSnippetForRow(result))
|
||||
}
|
||||
|
||||
private class CallableToTest extends Callable {
|
||||
CallableToTest() {
|
||||
exists(
|
||||
string namespace, string type, boolean subtypes, string name, string signature, string ext
|
||||
|
|
||||
summaryModel(namespace, type, subtypes, name, signature, ext, _, _, _) and
|
||||
this = interpretElement(namespace, type, subtypes, name, signature, ext)
|
||||
this = interpretElement(namespace, type, subtypes, name, signature, ext) and
|
||||
this.isPublic() and
|
||||
getRootType(this.getDeclaringType()).isPublic()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -124,6 +140,21 @@ string getFieldToken(FieldContent fc) {
|
||||
fc.getField().getName()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a valid Java token naming the synthetic field `fc`,
|
||||
* assuming that the name of that field consists only of characters valid in a Java identifier and `.`.
|
||||
*/
|
||||
string getSyntheticFieldToken(SyntheticFieldContent fc) {
|
||||
exists(string name, int parts |
|
||||
name = fc.getField() and
|
||||
parts = count(name.splitAt("."))
|
||||
|
|
||||
if parts = 1
|
||||
then result = name
|
||||
else result = name.splitAt(".", parts - 2) + "_" + name.splitAt(".", parts - 1)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a token suitable for incorporation into a Java method name describing content `c`.
|
||||
*/
|
||||
@@ -137,6 +168,8 @@ string contentToken(Content c) {
|
||||
c instanceof MapValueContent and result = "MapValue"
|
||||
or
|
||||
result = getFieldToken(c)
|
||||
or
|
||||
result = getSyntheticFieldToken(c)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -145,7 +178,10 @@ string contentToken(Content c) {
|
||||
RefType getRootType(RefType t) {
|
||||
if t instanceof NestedType
|
||||
then result = getRootType(t.(NestedType).getEnclosingType())
|
||||
else result = t
|
||||
else
|
||||
if t instanceof Array
|
||||
then result = getRootType(t.(Array).getElementType())
|
||||
else result = t
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -406,6 +442,8 @@ class TestCase extends TTestCase {
|
||||
content instanceof CollectionContent and result = "Element"
|
||||
or
|
||||
result = "Field[" + content.(FieldContent).getField().getQualifiedName() + "]"
|
||||
or
|
||||
result = "SyntheticField[" + content.(SyntheticFieldContent).getField() + "]"
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -482,18 +520,22 @@ predicate isImportable(Type t) {
|
||||
* if we cannot import it due to a name clash.
|
||||
*/
|
||||
string getShortNameIfPossible(Type t) {
|
||||
getRootSourceDeclaration(t) = any(TestCase tc).getADesiredImport() and
|
||||
if t instanceof RefType
|
||||
then
|
||||
exists(RefType replaced, string nestedName |
|
||||
replaced = replaceTypeVariable(t).getSourceDeclaration() and
|
||||
nestedName = replaced.nestedName().replaceAll("$", ".")
|
||||
|
|
||||
if isImportable(getRootSourceDeclaration(t))
|
||||
then result = nestedName
|
||||
else result = replaced.getPackage().getName() + "." + nestedName
|
||||
)
|
||||
else result = t.getName()
|
||||
if t instanceof Array
|
||||
then result = getShortNameIfPossible(t.(Array).getComponentType()) + "[]"
|
||||
else (
|
||||
getRootSourceDeclaration(t) = any(TestCase tc).getADesiredImport() and
|
||||
if t instanceof RefType
|
||||
then
|
||||
exists(RefType replaced, string nestedName |
|
||||
replaced = replaceTypeVariable(t).getSourceDeclaration() and
|
||||
nestedName = replaced.nestedName().replaceAll("$", ".")
|
||||
|
|
||||
if isImportable(getRootSourceDeclaration(t))
|
||||
then result = nestedName
|
||||
else result = replaced.getPackage().getName() + "." + nestedName
|
||||
)
|
||||
else result = t.getName()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -530,7 +572,7 @@ query string getTestCase() {
|
||||
result =
|
||||
"package generatedtest;\n\n" + concat(getAnImportStatement() + "\n") +
|
||||
"\n// Test case generated by GenerateFlowTestCase.ql\npublic class Test {\n\n" +
|
||||
concat("\t" + getASupportMethod() + "\n") + "\n\tpublic void test() {\n\n" +
|
||||
concat("\t" + getASupportMethod() + "\n") + "\n\tpublic void test() throws Exception {\n\n" +
|
||||
concat(string row, string snippet |
|
||||
snippet = any(TestCase tc).getATestSnippetForRow(row)
|
||||
|
|
||||
|
||||
Reference in New Issue
Block a user