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

Conversation

ghost
Copy link

@ghost ghost commented Jun 15, 2024

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/pyloadalong with it's fix. The databases can be downloaded from the links bellow.

https://file.io/qrMEjSJJoTq1
https://filetransfer.io/data-package/a02eab7V#link

@ghost ghost self-requested a review as a code owner June 15, 2024 15:23
@ghsecuritylab ghsecuritylab marked this pull request as draft June 15, 2024 16:02
@ghsecuritylab
Copy link
Collaborator

Hello porcupineyhairs 👋
You have submitted this pull request as a bug bounty report in the github/securitylab repository and therefore this pull request has been put into draft state to give time for the GitHub Security Lab to assess the PR. When GitHub Security Lab has finished assessing your pull request, it will be marked automatically as Ready for review. Until then, please don't change the draft state.

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.

  • the submission models widely-used frameworks/libraries
  • the vulnerability modeled in the submission is impactful
  • the submission finds new true positive vulnerabilities
  • the submission finds very few false positives
  • code in the submission is easy to read and will be easy to maintain
  • documentation is written clearly, highlighting the impact of the issue it finds and is written without grammatical or other errors. The code samples clearly show the vulnerability
  • the submission includes tests, change note etc.

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!

@sylwia-budzynska
Copy link
Contributor

@github/codeql-python 👋 This submission is ready for review.

@ghsecuritylab ghsecuritylab marked this pull request as ready for review July 1, 2024 11:22
@RasmusWL RasmusWL self-assigned this Jul 2, 2024
Copy link
Member

@RasmusWL RasmusWL left a 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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
Copy link
Member

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

Suggested change
* @id python/js2py-rce
* @id py/js2py-rce

Comment on lines 19 to 44
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)
Copy link
Member

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.

Suggested change
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"
Copy link
Member

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

Suggested change
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>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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>

Copy link
Contributor

github-actions bot commented Jul 2, 2024

QHelp previews:

python/ql/src/experimental/Security/CWE-094/Js2Py.qhelp

JavaScript code execution.

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

Recommendation

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).

Example

In 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()")

@ghost
Copy link
Author

ghost commented Jul 3, 2024

@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
```
@RasmusWL
Copy link
Member

RasmusWL commented Jul 4, 2024

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 🤔

@ghost
Copy link
Author

ghost commented Jul 4, 2024

@RasmusWL Sorry about that. I made a couple of mistakes while merging so I had to force push again.

Copy link
Member

@RasmusWL RasmusWL left a 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()

@RasmusWL RasmusWL merged commit f41d2a8 into github:main Jul 11, 2024
12 checks passed
@ghost ghost deleted the js2py branch July 11, 2024 15:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants