From 808af286182c622960da61bc86eb94fd93feb926 Mon Sep 17 00:00:00 2001 From: Porcupiney Hairs Date: Sat, 15 Jun 2024 20:53:00 +0530 Subject: [PATCH 1/4] Python : Arbitrary codde execution due to Js2Py Js2Py is a Javascript to Python translation library written in Python. It allows users to invoke JavaScript code directly from Python. The Js2Py interpreter by default exposes the entire standard library to it's users. This can lead to security issues if a malicious input were directly. This PR includes a CodeQL query along with a qhelp and testcases to detect cases where an untrusted input flows to an Js2Py eval call. This query successfully detects CVE-2023-0297 in `pyload/pyload`along with it's fix. The databases can be downloaded from the links bellow. ``` https://file.io/qrMEjSJJoTq1 https://filetransfer.io/data-package/a02eab7V#link ``` --- .../experimental/Security/CWE-094/Js2Py.qhelp | 24 +++++++++++++ .../experimental/Security/CWE-094/Js2Py.ql | 36 +++++++++++++++++++ .../experimental/Security/CWE-094/Js2pyBad.py | 4 +++ .../Security/CWE-094/Js2pyGood.py | 6 ++++ .../Security/CWE-094/Js2Py.expected | 10 ++++++ .../query-tests/Security/CWE-094/Js2Py.qlref | 1 + .../query-tests/Security/CWE-094/Js2PyTest.py | 10 ++++++ 7 files changed, 91 insertions(+) create mode 100644 python/ql/src/experimental/Security/CWE-094/Js2Py.qhelp create mode 100644 python/ql/src/experimental/Security/CWE-094/Js2Py.ql create mode 100644 python/ql/src/experimental/Security/CWE-094/Js2pyBad.py create mode 100644 python/ql/src/experimental/Security/CWE-094/Js2pyGood.py create mode 100644 python/ql/test/experimental/query-tests/Security/CWE-094/Js2Py.expected create mode 100644 python/ql/test/experimental/query-tests/Security/CWE-094/Js2Py.qlref create mode 100644 python/ql/test/experimental/query-tests/Security/CWE-094/Js2PyTest.py diff --git a/python/ql/src/experimental/Security/CWE-094/Js2Py.qhelp b/python/ql/src/experimental/Security/CWE-094/Js2Py.qhelp new file mode 100644 index 000000000000..f1fed6c38f6d --- /dev/null +++ b/python/ql/src/experimental/Security/CWE-094/Js2Py.qhelp @@ -0,0 +1,24 @@ + + + +

+ Passing untrusted inputs to a JavaScript interpreter like `Js2Py` can lead to arbitrary + code execution. +

+
+ +

This vulnerability can be prevented either by preventing an untrusted user input to flow + to an eval_js call. Or, the impact of this vulnerability can be + significantly reduced by disabling imports from the interepreted code (note that in a + comment the author of the library highlights that Js2Py is still insecure with this + option).

+
+ +

In the example below, the Javascript code being evaluated is controlled by the user and + hence leads to arbitrary code execution.

+ +

This can be fixed by disabling imports before evaluating the user passed buffer. + + + \ No newline at end of file diff --git a/python/ql/src/experimental/Security/CWE-094/Js2Py.ql b/python/ql/src/experimental/Security/CWE-094/Js2Py.ql new file mode 100644 index 000000000000..0dc0145a1e7d --- /dev/null +++ b/python/ql/src/experimental/Security/CWE-094/Js2Py.ql @@ -0,0 +1,36 @@ +/** + * @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. + * @severity 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; + +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" diff --git a/python/ql/src/experimental/Security/CWE-094/Js2pyBad.py b/python/ql/src/experimental/Security/CWE-094/Js2pyBad.py new file mode 100644 index 000000000000..69791a424628 --- /dev/null +++ b/python/ql/src/experimental/Security/CWE-094/Js2pyBad.py @@ -0,0 +1,4 @@ +@bp.route("/bad") +def bad(): + jk = flask.request.form["jk"] + jk = eval_js(f"{jk} f()") diff --git a/python/ql/src/experimental/Security/CWE-094/Js2pyGood.py b/python/ql/src/experimental/Security/CWE-094/Js2pyGood.py new file mode 100644 index 000000000000..dd034d48bb30 --- /dev/null +++ b/python/ql/src/experimental/Security/CWE-094/Js2pyGood.py @@ -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()") diff --git a/python/ql/test/experimental/query-tests/Security/CWE-094/Js2Py.expected b/python/ql/test/experimental/query-tests/Security/CWE-094/Js2Py.expected new file mode 100644 index 000000000000..2d4542b92ec9 --- /dev/null +++ b/python/ql/test/experimental/query-tests/Security/CWE-094/Js2Py.expected @@ -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 can lead to arbitrary code execution | diff --git a/python/ql/test/experimental/query-tests/Security/CWE-094/Js2Py.qlref b/python/ql/test/experimental/query-tests/Security/CWE-094/Js2Py.qlref new file mode 100644 index 000000000000..457bfe2aacca --- /dev/null +++ b/python/ql/test/experimental/query-tests/Security/CWE-094/Js2Py.qlref @@ -0,0 +1 @@ +experimental/Security/CWE-094/Js2Py.ql diff --git a/python/ql/test/experimental/query-tests/Security/CWE-094/Js2PyTest.py b/python/ql/test/experimental/query-tests/Security/CWE-094/Js2PyTest.py new file mode 100644 index 000000000000..f7aae16a9eed --- /dev/null +++ b/python/ql/test/experimental/query-tests/Security/CWE-094/Js2PyTest.py @@ -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()") \ No newline at end of file From 8d1113cdafa0f22e0dffbffe53f965d3082e8e94 Mon Sep 17 00:00:00 2001 From: Rasmus Wriedt Larsen Date: Thu, 4 Jul 2024 14:01:30 +0200 Subject: [PATCH 2/4] Python: Fixup qhelp --- python/ql/src/experimental/Security/CWE-094/Js2Py.qhelp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/python/ql/src/experimental/Security/CWE-094/Js2Py.qhelp b/python/ql/src/experimental/Security/CWE-094/Js2Py.qhelp index f1fed6c38f6d..6be0b43d1a1f 100644 --- a/python/ql/src/experimental/Security/CWE-094/Js2Py.qhelp +++ b/python/ql/src/experimental/Security/CWE-094/Js2Py.qhelp @@ -17,8 +17,8 @@

In the example below, the Javascript code being evaluated is controlled by the user and hence leads to arbitrary code execution.

- -

This can be fixed by disabling imports before evaluating the user passed buffer. - + +

This can be fixed by disabling imports before evaluating the user passed buffer.

+
-
\ No newline at end of file + From 0a32f9fed67b76a602c0731bc79c397b775a8f7a Mon Sep 17 00:00:00 2001 From: Rasmus Wriedt Larsen Date: Thu, 4 Jul 2024 14:09:37 +0200 Subject: [PATCH 3/4] Python: Update query metadata --- python/ql/src/experimental/Security/CWE-094/Js2Py.ql | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/python/ql/src/experimental/Security/CWE-094/Js2Py.ql b/python/ql/src/experimental/Security/CWE-094/Js2Py.ql index 0dc0145a1e7d..5dc160077873 100644 --- a/python/ql/src/experimental/Security/CWE-094/Js2Py.ql +++ b/python/ql/src/experimental/Security/CWE-094/Js2Py.ql @@ -1,7 +1,9 @@ /** * @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. - * @severity high + * @problem.severity error + * @security-severity 9.3 + * @precision high * @kind path-problem * @id py/js2py-rce * @tags security From 5ecde387afc76ebe4520ab0d9f6ffadb4fb428c6 Mon Sep 17 00:00:00 2001 From: Rasmus Wriedt Larsen Date: Thu, 11 Jul 2024 14:42:26 +0200 Subject: [PATCH 4/4] Python: Fix `.expected` --- .../experimental/query-tests/Security/CWE-094/Js2Py.expected | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/ql/test/experimental/query-tests/Security/CWE-094/Js2Py.expected b/python/ql/test/experimental/query-tests/Security/CWE-094/Js2Py.expected index 2d4542b92ec9..7798cdda143c 100644 --- a/python/ql/test/experimental/query-tests/Security/CWE-094/Js2Py.expected +++ b/python/ql/test/experimental/query-tests/Security/CWE-094/Js2Py.expected @@ -7,4 +7,4 @@ nodes | 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 can lead to arbitrary code execution | +| 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 |