Skip to content

Commit b0b5338

Browse files
committed
Rhino code injection
1 parent 9deaace commit b0b5338

File tree

14 files changed

+1589
-1
lines changed

14 files changed

+1589
-1
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
public class RhinoInjection extends HttpServlet {
2+
3+
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
4+
response.setContentType("text/plain");
5+
String code = request.getParameter("code");
6+
Context ctx = Context.enter();
7+
try {
8+
{
9+
// BAD: allow arbitrary Java and JavaScript code to be executed
10+
Scriptable scope = ctx.initStandardObjects();
11+
}
12+
13+
{
14+
// GOOD: enable the safe mode
15+
Scriptable scope = ctx.initSafeStandardObjects();
16+
}
17+
18+
{
19+
// GOOD: enforce a constraint on allowed classes
20+
Scriptable scope = ctx.initStandardObjects();
21+
ctx.setClassShutter(new ClassShutter() {
22+
public boolean visibleToScripts(String className) {
23+
if(className.startsWith("com.example.")) {
24+
return true;
25+
}
26+
return false;
27+
}
28+
});
29+
}
30+
31+
Object result = ctx.evaluateString(scope, code, "<code>", 1, null);
32+
response.getWriter().print(Context.toString(result));
33+
} catch(RhinoException ex) {
34+
response.getWriter().println(ex.getMessage());
35+
} finally {
36+
Context.exit();
37+
}
38+
}
39+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<!DOCTYPE qhelp PUBLIC "-//Semmle//qhelp//EN" "qhelp.dtd">
2+
<qhelp>
3+
4+
<overview>
5+
<p>
6+
Rhino is a JavaScript engine written fully in Java and managed by the Mozilla Foundation.
7+
It serves as an embedded scripting engine inside Java applications which allows
8+
Java-to-JavaScript interoperability and provides a seamless integration between the two
9+
languages. If an expression is built using attacker-controlled data, and then evaluated in
10+
a powerful context, it may allow the attacker to run arbitrary code.
11+
</p>
12+
<p>
13+
Typically an expression is evaluated in the powerful context initialized with
14+
<code>initStandardObjects</code> that allows an expression of arbitrary Java code to
15+
execute in the JVM.
16+
</p>
17+
</overview>
18+
19+
<recommendation>
20+
<p>
21+
In general, including user input in a Rhino expression should be avoided.
22+
If user input must be included in the expression, it should be then evaluated in a safe
23+
context that doesn't allow arbitrary code invocation.
24+
</p>
25+
</recommendation>
26+
27+
<example>
28+
<p>
29+
The following example shows two ways of using Rhino expression. In the 'BAD' case,
30+
an unsafe context is initialized with <code>initStandardObjects</code>. In the 'GOOD' case,
31+
a safe context is initialized with <code>initSafeStandardObjects</code> or
32+
<code>setClassShutter</code>.
33+
</p>
34+
<sample src="RhinoInjection.java" />
35+
</example>
36+
37+
<references>
38+
<li>
39+
Mozilla Rhino:
40+
<a href="https://github.com/mozilla/rhino">Rhino: JavaScript in Java</a>
41+
</li>
42+
<li>
43+
Rhino Sandbox:
44+
<a href="https://github.com/javadelight/delight-rhino-sandbox">A sandbox to execute JavaScript code with Rhino in Java.</a>
45+
</li>
46+
<li>
47+
GuardRails:
48+
<a href="https://docs.guardrails.io/docs/en/vulnerabilities/java/insecure_use_of_dangerous_function">Code Injection</a>
49+
</li>
50+
</references>
51+
</qhelp>
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/**
2+
* @name Injection in Mozilla Rhino JavaScript Engine
3+
* @description Evaluation of a user-controlled JavaScript or Java expression in Rhino
4+
* JavaScript Engine may lead to remote code execution.
5+
* @kind path-problem
6+
* @id java/rhino-injection
7+
* @tags security
8+
* external/cwe/cwe-094
9+
*/
10+
11+
import java
12+
import RhinoInjection
13+
import DataFlow::PathGraph
14+
15+
from DataFlow::PathNode source, DataFlow::PathNode sink, RhinoInjectionConfig conf
16+
where conf.hasFlowPath(source, sink)
17+
select sink.getNode(), source, sink, "Rhino injection from $@.", source.getNode(), " user input"
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import java
2+
import semmle.code.java.dataflow.FlowSources
3+
import semmle.code.java.dataflow.TaintTracking
4+
5+
/** The class `org.mozilla.javascript.Context`. */
6+
class Context extends RefType {
7+
Context() { this.hasQualifiedName("org.mozilla.javascript", "Context") }
8+
}
9+
10+
/**
11+
* A method that evaluates a Rhino expression.
12+
*/
13+
class EvaluateExpressionMethod extends Method {
14+
EvaluateExpressionMethod() {
15+
this.getDeclaringType().getAnAncestor*() instanceof Context and
16+
(
17+
hasName("evaluateString") or
18+
hasName("evaluateReader")
19+
)
20+
}
21+
}
22+
23+
/**
24+
* A taint-tracking configuration for unsafe user input that is used to evaluate
25+
* a Rhino expression.
26+
*/
27+
class RhinoInjectionConfig extends TaintTracking::Configuration {
28+
RhinoInjectionConfig() { this = "RhinoInjectionConfig" }
29+
30+
override predicate isSource(DataFlow::Node source) {
31+
source instanceof RemoteFlowSource
32+
or
33+
source instanceof LocalUserInput
34+
}
35+
36+
override predicate isSink(DataFlow::Node sink) { sink instanceof EvaluateExpressionSink }
37+
}
38+
39+
/**
40+
* A sink for Rhino code injection vulnerabilities.
41+
*/
42+
class EvaluateExpressionSink extends DataFlow::ExprNode {
43+
EvaluateExpressionSink() {
44+
exists(MethodAccess ea, EvaluateExpressionMethod m | m = ea.getMethod() |
45+
this.asExpr() = ea.getArgument(1) and // The second argument is the JavaScript or Java input
46+
not exists(MethodAccess ca |
47+
(
48+
ca.getMethod().hasName("initSafeStandardObjects") // safe mode
49+
or
50+
ca.getMethod().hasName("setClassShutter") // `ClassShutter` constraint is enforced
51+
) and
52+
ea.getQualifier() = ca.getQualifier().(VarAccess).getVariable().getAnAccess()
53+
)
54+
)
55+
}
56+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
edges
2+
| RhinoServlet.java:25:23:25:50 | getParameter(...) : String | RhinoServlet.java:29:55:29:58 | code |
3+
nodes
4+
| RhinoServlet.java:25:23:25:50 | getParameter(...) : String | semmle.label | getParameter(...) : String |
5+
| RhinoServlet.java:29:55:29:58 | code | semmle.label | code |
6+
#select
7+
| RhinoServlet.java:29:55:29:58 | code | RhinoServlet.java:25:23:25:50 | getParameter(...) : String | RhinoServlet.java:29:55:29:58 | code | Rhino injection from $@. | RhinoServlet.java:25:23:25:50 | getParameter(...) | user input |
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
experimental/Security/CWE/CWE-094/RhinoInjection.ql
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import java.io.IOException;
2+
import javax.servlet.ServletException;
3+
import javax.servlet.http.HttpServlet;
4+
import javax.servlet.http.HttpServletRequest;
5+
import javax.servlet.http.HttpServletResponse;
6+
7+
import org.mozilla.javascript.ClassShutter;
8+
import org.mozilla.javascript.Context;
9+
import org.mozilla.javascript.Scriptable;
10+
import org.mozilla.javascript.RhinoException;
11+
12+
/**
13+
* Servlet implementation class RhinoServlet
14+
*/
15+
public class RhinoServlet extends HttpServlet {
16+
private static final long serialVersionUID = 1L;
17+
18+
public RhinoServlet() {
19+
super();
20+
}
21+
22+
// BAD: allow arbitrary Java and JavaScript code to be executed
23+
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
24+
response.setContentType("text/plain");
25+
String code = request.getParameter("code");
26+
Context ctx = Context.enter();
27+
try {
28+
Scriptable scope = ctx.initStandardObjects();
29+
Object result = ctx.evaluateString(scope, code, "<code>", 1, null);
30+
response.getWriter().print(Context.toString(result));
31+
} catch(RhinoException ex) {
32+
response.getWriter().println(ex.getMessage());
33+
} finally {
34+
Context.exit();
35+
}
36+
}
37+
38+
// GOOD: enable the safe mode
39+
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
40+
response.setContentType("text/plain");
41+
String code = request.getParameter("code");
42+
Context ctx = Context.enter();
43+
try {
44+
Scriptable scope = ctx.initSafeStandardObjects();
45+
Object result = ctx.evaluateString(scope, code, "<code>", 1, null);
46+
response.getWriter().print(Context.toString(result));
47+
} catch(RhinoException ex) {
48+
response.getWriter().println(ex.getMessage());
49+
} finally {
50+
Context.exit();
51+
}
52+
}
53+
54+
// GOOD: enforce a constraint on allowed classes
55+
protected void doPut(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
56+
response.setContentType("text/plain");
57+
String code = request.getParameter("code");
58+
Context ctx = Context.enter();
59+
try {
60+
Scriptable scope = ctx.initStandardObjects();
61+
ctx.setClassShutter(new ClassShutter() {
62+
public boolean visibleToScripts(String className) {
63+
if(className.startsWith("com.example.")) {
64+
return true;
65+
}
66+
return false;
67+
}
68+
});
69+
70+
Object result = ctx.evaluateString(scope, code, "<code>", 1, null);
71+
response.getWriter().print(Context.toString(result));
72+
} catch(RhinoException ex) {
73+
response.getWriter().println(ex.getMessage());
74+
} finally {
75+
Context.exit();
76+
}
77+
}
78+
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
//semmle-extractor-options: --javac-args -cp ${testdir}/../../../../stubs/springframework-5.2.3:${testdir}/../../../../stubs/mvel2-2.4.7:${testdir}/../../../../stubs/jsr223-api:${testdir}/../../../../stubs/apache-commons-jexl-2.1.1:${testdir}/../../../../stubs/apache-commons-jexl-3.1:${testdir}/../../../../stubs/scriptengine:${testdir}/../../../../stubs/java-ee-el:${testdir}/../../../../stubs/juel-2.2:${testdir}/../../../stubs/groovy-all-3.0.7:${testdir}/../../../../stubs/servlet-api-2.4:${testdir}/../../../../stubs/jython-2.7.2
1+
//semmle-extractor-options: --javac-args -cp ${testdir}/../../../../stubs/springframework-5.2.3:${testdir}/../../../../stubs/mvel2-2.4.7:${testdir}/../../../../stubs/jsr223-api:${testdir}/../../../../stubs/apache-commons-jexl-2.1.1:${testdir}/../../../../stubs/apache-commons-jexl-3.1:${testdir}/../../../../stubs/scriptengine:${testdir}/../../../../stubs/java-ee-el:${testdir}/../../../../stubs/juel-2.2:${testdir}/../../../stubs/groovy-all-3.0.7:${testdir}/../../../../stubs/servlet-api-2.4:${testdir}/../../../../stubs/jython-2.7.2:${testdir}/../../../../experimental/stubs/rhino-1.7.13
22

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/* -*- Mode: java; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
2+
*
3+
* This Source Code Form is subject to the terms of the Mozilla Public
4+
* License, v. 2.0. If a copy of the MPL was not distributed with this
5+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6+
7+
// API class
8+
9+
package org.mozilla.javascript;
10+
11+
/**
12+
Embeddings that wish to filter Java classes that are visible to scripts
13+
through the LiveConnect, should implement this interface.
14+
15+
@see Context#setClassShutter(ClassShutter)
16+
@since 1.5 Release 4
17+
@author Norris Boyd
18+
*/
19+
20+
public interface ClassShutter {
21+
22+
/**
23+
* Return true iff the Java class with the given name should be exposed
24+
* to scripts.
25+
* <p>
26+
* An embedding may filter which Java classes are exposed through
27+
* LiveConnect to JavaScript scripts.
28+
* <p>
29+
* Due to the fact that there is no package reflection in Java,
30+
* this method will also be called with package names. There
31+
* is no way for Rhino to tell if "Packages.a.b" is a package name
32+
* or a class that doesn't exist. What Rhino does is attempt
33+
* to load each segment of "Packages.a.b.c": It first attempts to
34+
* load class "a", then attempts to load class "a.b", then
35+
* finally attempts to load class "a.b.c". On a Rhino installation
36+
* without any ClassShutter set, and without any of the
37+
* above classes, the expression "Packages.a.b.c" will result in
38+
* a [JavaPackage a.b.c] and not an error.
39+
* <p>
40+
* With ClassShutter supplied, Rhino will first call
41+
* visibleToScripts before attempting to look up the class name. If
42+
* visibleToScripts returns false, the class name lookup is not
43+
* performed and subsequent Rhino execution assumes the class is
44+
* not present. So for "java.lang.System.out.println" the lookup
45+
* of "java.lang.System" is skipped and thus Rhino assumes that
46+
* "java.lang.System" doesn't exist. So then for "java.lang.System.out",
47+
* Rhino attempts to load the class "java.lang.System.out" because
48+
* it assumes that "java.lang.System" is a package name.
49+
* <p>
50+
* @param fullClassName the full name of the class (including the package
51+
* name, with '.' as a delimiter). For example the
52+
* standard string class is "java.lang.String"
53+
* @return whether or not to reveal this class to scripts
54+
*/
55+
public boolean visibleToScripts(String fullClassName);
56+
}

0 commit comments

Comments
 (0)