Skip to content

Commit

Permalink
Merge pull request #16771 from porcupineyhairs/js2py
Browse files Browse the repository at this point in the history
Python : Arbitrary code execution due to Js2Py
  • Loading branch information
RasmusWL authored Jul 11, 2024
2 parents 8152ec7 + 5ecde38 commit f41d2a8
Show file tree
Hide file tree
Showing 7 changed files with 93 additions and 0 deletions.
24 changes: 24 additions & 0 deletions python/ql/src/experimental/Security/CWE-094/Js2Py.qhelp
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<!DOCTYPE qhelp SYSTEM "qhelp.dtd">
<qhelp>
<overview>
<p>
Passing untrusted inputs to a JavaScript interpreter like `Js2Py` can lead to arbitrary
code execution.
</p>
</overview>
<recommendation>
<p> This vulnerability can be prevented either by preventing an untrusted user input to flow
to an <code>eval_js</code> call. Or, the impact of this vulnerability can be
significantly reduced by disabling imports from the interepreted code (note that in a <a
href="https://github.com/PiotrDabkowski/Js2Py/issues/45#issuecomment-258724406">
comment</a> the author of the library highlights that Js2Py is still insecure with this
option).</p>
</recommendation>
<example>
<p>In the example below, the Javascript code being evaluated is controlled by the user and
hence leads to arbitrary code execution.</p>
<sample src="Js2pyBad.py" />
<p>This can be fixed by disabling imports before evaluating the user passed buffer.</p>
<sample src="Js2pyGood.py" />
</example>
</qhelp>
38 changes: 38 additions & 0 deletions python/ql/src/experimental/Security/CWE-094/Js2Py.ql
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* @name JavaScript code execution.
* @description Passing user supplied arguments to a Javascript to Python translation engine such as Js2Py can lead to remote code execution.
* @problem.severity error
* @security-severity 9.3
* @precision high
* @kind path-problem
* @id py/js2py-rce
* @tags security
* experimental
* external/cwe/cwe-94
*/

import python
import semmle.python.ApiGraphs
import semmle.python.dataflow.new.TaintTracking
import semmle.python.dataflow.new.RemoteFlowSources
import semmle.python.Concepts

module Js2PyFlowConfig implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node node) { node instanceof RemoteFlowSource }

predicate isSink(DataFlow::Node node) {
API::moduleImport("js2py").getMember(["eval_js", "eval_js6", "EvalJs"]).getACall().getArg(_) =
node
}
}

module Js2PyFlow = TaintTracking::Global<Js2PyFlowConfig>;

import Js2PyFlow::PathGraph

from Js2PyFlow::PathNode source, Js2PyFlow::PathNode sink
where
Js2PyFlow::flowPath(source, sink) and
not exists(API::moduleImport("js2py").getMember("disable_pyimport").getACall())
select sink, source, sink, "This input to Js2Py depends on a $@.", source.getNode(),
"user-provided value"
4 changes: 4 additions & 0 deletions python/ql/src/experimental/Security/CWE-094/Js2pyBad.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
@bp.route("/bad")
def bad():
jk = flask.request.form["jk"]
jk = eval_js(f"{jk} f()")
6 changes: 6 additions & 0 deletions python/ql/src/experimental/Security/CWE-094/Js2pyGood.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
@bp.route("/good")
def good():
# disable python imports to prevent execution of malicious code
js2py.disable_pyimport()
jk = flask.request.form["jk"]
jk = eval_js(f"{jk} f()")
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
edges
| Js2PyTest.py:9:5:9:6 | ControlFlowNode for jk | Js2PyTest.py:10:18:10:28 | ControlFlowNode for Fstring | provenance | |
| Js2PyTest.py:9:10:9:22 | ControlFlowNode for Attribute | Js2PyTest.py:9:5:9:6 | ControlFlowNode for jk | provenance | AdditionalTaintStep |
nodes
| Js2PyTest.py:9:5:9:6 | ControlFlowNode for jk | semmle.label | ControlFlowNode for jk |
| Js2PyTest.py:9:10:9:22 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
| Js2PyTest.py:10:18:10:28 | ControlFlowNode for Fstring | semmle.label | ControlFlowNode for Fstring |
subpaths
#select
| Js2PyTest.py:10:18:10:28 | ControlFlowNode for Fstring | Js2PyTest.py:9:10:9:22 | ControlFlowNode for Attribute | Js2PyTest.py:10:18:10:28 | ControlFlowNode for Fstring | This input to Js2Py depends on a $@. | Js2PyTest.py:9:10:9:22 | ControlFlowNode for Attribute | user-provided value |
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
experimental/Security/CWE-094/Js2Py.ql
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@

import flask
from js2py import eval_js, disable_pyimport

bp = flask.Blueprint("app", __name__, url_prefix="/")

@bp.route("/bad")
def bad():
jk = flask.request.form["jk"]
jk = eval_js(f"{jk} f()")

0 comments on commit f41d2a8

Please sign in to comment.