Merge branch 'main' into atorralba/promote-groovy-injection

This commit is contained in:
Tony Torralba
2021-08-03 09:53:46 +02:00
committed by GitHub
211 changed files with 5560 additions and 2147 deletions

View File

@@ -1,38 +0,0 @@
<!DOCTYPE qhelp PUBLIC "-//Semmle//qhelp//EN" "qhelp.dtd">
<qhelp>
<overview>
<p>
MVEL is an expression language based on Java-syntax.
The language 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.
</p>
</overview>
<recommendation>
<p>
Including user input in a MVEL expression should be avoided.
</p>
</recommendation>
<example>
<p>
The following example uses untrusted data to build a MVEL expression
and then runs it in the default powerfull context.
</p>
<sample src="UnsafeMvelExpressionEvaluation.java" />
</example>
<references>
<li>
MVEL Documentation:
<a href="http://mvel.documentnode.com/">Language Guide for 2.0</a>.
</li>
<li>
OWASP:
<a href="https://owasp.org/www-community/vulnerabilities/Expression_Language_Injection">Expression Language Injection</a>.
</li>
</references>
</qhelp>

View File

@@ -1,19 +0,0 @@
/**
* @name Expression language injection (MVEL)
* @description Evaluation of a user-controlled MVEL expression
* may lead to remote code execution.
* @kind path-problem
* @problem.severity error
* @precision high
* @id java/mvel-expression-injection
* @tags security
* external/cwe/cwe-094
*/
import java
import MvelInjectionLib
import DataFlow::PathGraph
from DataFlow::PathNode source, DataFlow::PathNode sink, MvelInjectionConfig conf
where conf.hasFlowPath(source, sink)
select sink.getNode(), source, sink, "MVEL injection from $@.", source.getNode(), "this user input"

View File

@@ -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") }
}

View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -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"

View File

@@ -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()
)
}

View File

@@ -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)){

View File

@@ -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>

View File

@@ -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)
}
}

View File

@@ -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")
)
}