Skip to content

Python : Arbitrary code execution due to Js2Py #16771

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jul 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()")
Loading