-
Notifications
You must be signed in to change notification settings - Fork 1.7k
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
Conversation
Hello porcupineyhairs 👋 In the meantime, feel free to make changes to the pull request. If you'd like to maximize payout for your this and future submissions, here are a few general guidelines, that we might take into consideration when reviewing a submission.
Please note that these are guidelines, not rules. Since we have a lot of different types of submissions, the guidelines might vary for each submission. Happy hacking! |
@github/codeql-python 👋 This submission is ready for review. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for your contribution 💪
I've made a few suggestions. If you accept the one for the alert message, you will also need to update the .expected files.
I had a brief look to understand what js2py.disable_pyimport()
actually does (src), and found an interesting comment highlighting that even when using this, you should still consider using the library with untrusted input as insecure. I think this is important to highlight in the qhelp.
</p> | ||
</overview> | ||
<recommendation> | ||
<p> This vulnerability can be prevented either by preventing an untrusted user input to lfow |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
<p> This vulnerability can be prevented either by preventing an untrusted user input to lfow | |
<p> This vulnerability can be prevented either by preventing an untrusted user input to flow |
</overview> | ||
<recommendation> | ||
<p> This vulnerability can be prevented either by preventing an untrusted user input to lfow | ||
to an <code>eval_js</code> call. Or, thee impact of this vulnerability can be |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
to an <code>eval_js</code> call. Or, thee impact of this vulnerability can be | |
to an <code>eval_js</code> call. Or, the impact of this vulnerability can be |
* @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 python/js2py-rce |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
convention is to use py/
as the prefix
* @id python/js2py-rce | |
* @id py/js2py-rce |
private API::Node js2py() { result = API::moduleImport("js2py") } | ||
|
||
private API::Node js2evaljs() { result = js2py().getMember(["eval_js", "eval_js6"]) } | ||
|
||
private class DisablePyImportCall extends API::CallNode, DataFlow::CallCfgNode { | ||
DisablePyImportCall() { this = js2py().getMember("disable_pyimport").getACall() } | ||
} | ||
|
||
class EvalJsCall extends API::CallNode, DataFlow::CallCfgNode { | ||
EvalJsCall() { this = js2evaljs().getACall() } | ||
} | ||
|
||
module Js2PyFlowConfiguration implements DataFlow::ConfigSig { | ||
predicate isSource(DataFlow::Node node) { node instanceof RemoteFlowSource } | ||
|
||
predicate isSink(DataFlow::Node node) { exists(EvalJsCall e | e.getArg(_) = node) } | ||
} | ||
|
||
module Js2PyFlow = TaintTracking::Global<Js2PyFlowConfiguration>; | ||
|
||
import Js2PyFlow::PathGraph | ||
|
||
from Js2PyFlow::PathNode source, Js2PyFlow::PathNode sink | ||
where | ||
Js2PyFlow::flowPath(source, sink) and | ||
not exists(DisablePyImportCall c) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the following looks cleaner, although it does the same.
NOTE: I've also added support for EvalJs
method call.
private API::Node js2py() { result = API::moduleImport("js2py") } | |
private API::Node js2evaljs() { result = js2py().getMember(["eval_js", "eval_js6"]) } | |
private class DisablePyImportCall extends API::CallNode, DataFlow::CallCfgNode { | |
DisablePyImportCall() { this = js2py().getMember("disable_pyimport").getACall() } | |
} | |
class EvalJsCall extends API::CallNode, DataFlow::CallCfgNode { | |
EvalJsCall() { this = js2evaljs().getACall() } | |
} | |
module Js2PyFlowConfiguration implements DataFlow::ConfigSig { | |
predicate isSource(DataFlow::Node node) { node instanceof RemoteFlowSource } | |
predicate isSink(DataFlow::Node node) { exists(EvalJsCall e | e.getArg(_) = node) } | |
} | |
module Js2PyFlow = TaintTracking::Global<Js2PyFlowConfiguration>; | |
import Js2PyFlow::PathGraph | |
from Js2PyFlow::PathNode source, Js2PyFlow::PathNode sink | |
where | |
Js2PyFlow::flowPath(source, sink) and | |
not exists(DisablePyImportCall c) | |
module Js2PyFlowConfiguration 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<Js2PyFlowConfiguration>; | |
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()) |
where | ||
Js2PyFlow::flowPath(source, sink) and | ||
not exists(DisablePyImportCall c) | ||
select sink, source, sink, "This can lead to arbitrary code execution" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This alert message is more in-line with our other alert messages
select sink, source, sink, "This can lead to arbitrary code execution" | |
select sink, source, sink, "This input to Js2Py depends on a $@.", source.getNode(), | |
"user-provided value" |
<recommendation> | ||
<p> This vulnerability can be prevented either by preventing an untrusted user input to lfow | ||
to an <code>eval_js</code> call. Or, thee impact of this vulnerability can be | ||
significantly reduced by disabling imports from the interepreted code. </p> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
significantly reduced by disabling imports from the interepreted code. </p> | |
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> |
QHelp previews: python/ql/src/experimental/Security/CWE-094/Js2Py.qhelpJavaScript code execution.Passing untrusted inputs to a JavaScript interpreter like `Js2Py` can lead to arbitrary code execution. RecommendationThis vulnerability can be prevented either by preventing an untrusted user input to flow to an ExampleIn the example below, the Javascript code being evaluated is controlled by the user and hence leads to arbitrary code execution. @bp.route("/bad")
def bad():
jk = flask.request.form["jk"]
jk = eval_js(f"{jk} f()") This can be fixed by disabling imports before evaluating the user passed buffer. @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()") |
@RasmusWL Changes done! |
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 ```
I would strongly advise to just push additional commits to your branch/PR next time. By doing so, it becomes very easy for me to see the changes you made after reading my review. With the force push, I have to remember what the code looked like before and try to figure out what has changed 🤔 |
@RasmusWL Sorry about that. I made a couple of mistakes while merging so I had to force push again. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks 👍
Before promoting this, I think we should expand tests a bit to also include js2py.disable_pyimport()
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.