Insights
A few days ago, the Anaconda project announced the PyScript framework, which allows Python code to be executed directly in the browser. Additionally, it also covers its integration with HTML and JS code.
An execution of the Python code in the browser is not new; the pyodide project has allowed this for a long time (by compiling Python to WebAssembly), but what’s new here is the integration with the rest of the browser ecosystem. Thanks to PyScript, we can easily include many modules directly from the pip repository: all modules written in “pure” Python should work, and some modules requiring native code have also been recompiled into WebAssembly.
Python has a very large standard library, as well as many great external libraries, so this project opens up many interesting opportunities to quickly build simple tools like security tests, or to allow showcasing some training concepts.
Let’s take a look at a very simple code written in PyScript:
1 2 3 4 5 6 7 8 9 10 11 12 |
<!doctype html> <html> <head> <script defer src="https://pyscript.net/alpha/pyscript.js"></script> </head> <body> <py-script> from js import alert alert("Securitum says hi!") </py-script> </body> </html> |
As you can see, all we need is to load the script, and then we can write python code in a special <py-script> tag. In the example above, I have used the js module built into the PyScript, which allows us to directly reference JavaScript functions. So, as you can guess, after loading the above page, you will see an alert with the text “Securitum says hi!”.
Let’s try to interact a bit more with JS. I will write a simple script that will use the python secrets module to generate random tokens. These tokens will be generated when the button is pressed and displayed in another HTML element. Here is an example implementation:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
<!doctype html> <html> <head> <script defer src="https://pyscript.net/alpha/pyscript.js"></script> </head> <body> <button>Click me</button> <div>The random token is: <b id=token></b></div> <py-script> from js import document,alert from pyodide import create_proxy import secrets def onclick(ev): pyscript.write("token", secrets.token_hex(16)) button = document.querySelector("button") button.addEventListener("click", create_proxy(onclick)) </py-script> </body> </html> |
And below, let’s see the code in action:
Click me
Random token is: da9315d5adc6b790aa3524d9e1e80090
While the use of modules from a standard library ends with the use of a normal import, some additional steps need to be taken if we want to load the module from the pip repository.
To name it: a special tag <py-env> has to be added in HTML. In that tag, we define a list of external modules. For example, if we wanted to import a primefac, we have to use the tag beforehand:
1 2 3 |
<py-env> - primefac </py-env> |
And in the following code, the basic import primefac will work.
primefac is a library useful for factoring numbers. So let’s write a very simple script that uses it to factorize a user-supplied number. Here is the code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
<!doctype html> <script defer src="https://pyscript.net/alpha/pyscript.js"></script> <py-env> - primefac </py-env> <p><label>Enter an integer in the range from 1 to 10000: <input id=n></label></p> <p>The prime factors are:: <span id=factors></span></p> <py-script> import primefac from pyodide import create_proxy input = document.querySelector("#n") def oninput(ev): num = int(ev.target.value) num = min(num, 10000) num = max(num, 1) factors = list(primefac.primefac(num)) pyscript.write("factors", factors) input.addEventListener("input", create_proxy(oninput)) </py-script> |
Here’s the code in action:
Enter an integer in the range from 1 to 10000:
The prime factors are: []
After learning basics in the examples above, we can try to use PyScript in a more practical way.
Bleach is a Python library developed by Mozilla, used to sanitize (clean) HTML code from malicious elements or attributes that allow the use of XSS vulnerabilities. Personally, I really like testing sanitizers, and one of the most convenient ways to do this is to write a code that will perform sanitation on an ongoing basis, i.e. immediately after pressing the next keys.
With PyScript, writing an environment like this is very simple
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
<!doctype html> <script defer src="https://pyscript.net/alpha/pyscript.js"></script> <py-env> - primefac </py-env> <p><label>Enter an integer in the range from 1 to 10000: <input id=n></label></p> <p>The prime factors are:: <span id=factors></span></p> <py-script> import primefac from pyodide import create_proxy input = document.querySelector("#n") def oninput(ev): num = int(ev.target.value) num = min(num, 10000) num = max(num, 1) factors = list(primefac.primefac(num)) pyscript.write("factors", factors) input.addEventListener("input", create_proxy(oninput)) </py-script> |
Provide HTML
HTML after sanitation
As a fun fact, I will add that after quick bleach testing using the above code, I might have a bypass of the sanitizer (albeit with a non-standard configuration). Thus, you can see that building the right test environment makes it easier to find security bugs.
In Python, there is a very useful library called cryptography, which allows you to perform basically any cryptographic operation (encryption, signing, generating one-time codes, hashing etc.). It is not written in “pure” Python, but is one of those libraries that is compiled specifically for pyodide (and also PyScript).
As part of the training, I decided to try to use this library and write a Google Authenticator simulator with it. I can easily do this because cryptography has a module ready to generate one-time codes using the TOTP algorithm.
So my plan is for the code to be able to:
Let’s look at each of these elements separately.
First step, the keys in the Authenticator are 80 bits (10 bytes) long and are Base32-encoded. For example, they look like this: 7NWLVFIDJSXOVSSV. Generating them simply requires the use of a cryptographically secure pseudorandom number generator, e.g. from the secrets module
1 2 3 |
import secrets def get_random_key(): return secrets.token_bytes(10) |
In the code, it doesn’t encode the data to Base32 because later the cryptography library will do it anyway.
Second step, generating QR codes. It will require the use of another library, namely: qrcode and pillow (used by qrcode to generate images).
Interestingly, PyScript provides the ability to easily display images generated in our code, but – unfortunately – currently this code has a bug and does not display them correctly. Fortunately enough, it’s easy to fix by defining your own render_image method which will generate the correct image.
PyScript code blocks are also interpreted such that the last line in our code is treated as the value returned by that block. Hence, if in the last line we simply put a reference to the image – PyScript will recognize that we want to display this image.
Below is an example of a code that displays a QR code with text:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<py-env> - qrcode - pillow </py-env> <py-script> import qrcode import base64 # Naprawiam oryginalną zepsutą metodę render_image def render_image(mime, value, meta): data = f"data:{mime};base64,{base64.b64encode(value).decode('utf-8')}" return f"\u003cimg src='{data}'\u003e" img = qrcode.make("Witaj Sekuraku!") # Zwracam referencję do obrazka w ostatniej linii kodu img </py-script> |
And the effect:
Of course, in order for Google Authenticator to accept a QR code, it must contain the appropriate content. As we find out from the documentation, the format is as follows:
otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example
However, we will not have to prepare such a URL format ourselves, because cryptography has a method called get_provisioning_uri for this.
So let’s move on to the last step, which is the generating of one-time codes. Below is an example of a code that will generate another one-time code for a given time and a given key:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 |
import os import time from cryptography.hazmat.primitives.twofactor.totp import TOTP from cryptography.hazmat.primitives.hashes import SHA1 # We are generating the key key = os.urandom(20) # We define the length of the TOTP to 6 characters and a regeneration # every 30 seconds totp = TOTP(key, 6, SHA1(), 30) time_value = time.time() # We generate the code for a given time totp_value = totp.generate(time_value) Once we have the totp object, we can perform the previously mentioned get_provisioning_uri method on it. So let's put it all together. The code is as follows: <div style="border: 1px solid black;padding: 1em; font-family:monospace"> <p><strong>TOTP key</strong>: <span id=otpkey></span></p> <p><strong>One-time code</strong>:</p> <p><span style=font-size:xxx-large;font-weight:bold id=otpotp>123</span></p> <p><strong>Next code in</strong>: <span id=next_code_in></span>s</p> <p><button id=newotpkey>Generate a new key</button></p> <div><img id=otpqr style=max-width:320px></div> <py-env> - qrcode - pillow - cryptography </py-env> <py-script> import qrcode import base64 import secrets from cryptography.hazmat.primitives.twofactor.totp import TOTP from cryptography.hazmat.primitives.hashes import SHA1 from js import setInterval, document from pyodide import create_proxy from io import BytesIO OTP_LENGTH = 6 OTP_INTERVAL = 30 state = { "key": b"", "otp": "", "totp_object": None, "qrcode": None } # Key regeneration and creation of the TOTP object def regenerate_key(arg=None): global state state["key"] = secrets.token_bytes(10) state["totp_object"] = TOTP(state["key"], OTP_LENGTH, SHA1(), OTP_INTERVAL, enforce_key_length=False) uri = state["totp_object"].get_provisioning_uri("PyScript", "Securitum") img = qrcode.make(uri) buf = BytesIO() img.save(buf, format="png") imgdata = f"data:image/png;base64,{base64.b64encode(buf.getvalue()).decode('utf-8')}" state["qrcode"] = imgdata generate_otp() def generate_otp(): global state state["otp"] = state["totp_object"].generate(time.time()) qrcode_image = document.getElementById("otpqr") # A function that is called every second to update the HTML state def update_html(): global state generate_otp() next_code_in = OTP_INTERVAL - time.time() % OTP_INTERVAL pyscript.write("otpkey", base64.b32encode(state["key"]).decode('utf-8')) pyscript.write("otpotp", state["otp"].decode('utf-8')) pyscript.write("next_code_in", round(next_code_in)) qrcode_image.src = state["qrcode"] newotpbutton = document.getElementById("newotpkey") newotpbutton.addEventListener("click", create_proxy(regenerate_key)) setInterval(create_proxy(update_html), 1000) regenerate_key() update_html() </py-script> </div> |
And the result of the action:
TOTP Key: HHDFEXJBIPBBJ23J
One-time code:
414435
Next code in: 12s
Generate a new key
The best way to check if the script is working correctly is to import the QR key into the Authenticator and verify that it generates the same codes as the script. As the picture below shows – it looks like it does!
The last example I want to show in this article will use sqlite. The sqlite handler is included in the Python standard library and is available from the PyScript level, hence I thought that it might be used as a demo of SQL Injection in a training.
I’ll build a very simple script that:
Without further ado, let’s go straight to the code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
<p><label>Enter the phrase: <input id=searchphrase oninput="search_sql(event.target.value)"></label></p> <p><strong>Executed SQL:</strong></p> <pre wrap id=sqlsql></pre> <p><strong>Result:</strong></p> <pre wrap id=sqlresult></pre> <py-script> import sqlite3 from js import window, document from pyodide import create_proxy # Creating an in-memory sqlite database con = sqlite3.connect(":memory:") cur = con.cursor() products = [ ("Product 1", "Description 1", 12), ("Product 2", "Description 2", 33), ("Product 3", "Description 3", 34), ] users = [ ("mb", "very-secret-password"), ("ms", "even-more-secret-password") ] cur.execute("CREATE TABLE products (name, description, amount)") cur.execute("CREATE TABLE users (name, description)") cur.executemany("INSERT INTO products VALUES (?, ?, ?)", products) cur.executemany("INSERT INTO users VALUES (?, ?)", users) sqlsql = document.getElementById("sqlsql") sqlresult = document.getElementById("sqlresult") def search(query): sql = f"SELECT * FROM products WHERE name LIKE '%{query}%' OR description LIKE '%{query}%'" sqlsql.textContent = sql try: cur.execute(sql) result = cur.fetchall() except Exception as ex: result = str(ex) sqlresult.textContent = result window.search_sql = create_proxy(search) </py-script> |
And the result:
Enter the phrase:
SQL executed:
The result:
Of course, if these types of scripts were actually to be used in trainings, it would have to look better and present more information to the user, but this example itself should already show how easily it can be prepared.
PyScript is an interesting new project that allows you to execute your own Python code directly in the browser. For now, it’s still in its early stages, and it’s not hard to find components that won’t work, but it has a lot of potential. From my point of view – it looks particularly interesting in terms of trainings, as well as preparing simple tools to facilitate everyday activities, using the extensive standard Python library.
Within last year I shared a a few writeups of my bypasses of HTML sanitizers, including: > Write-up of DOMPurify 2.0.0 bypass using mutation XSS > Mutation XSS via namespace confusion – DOMPurify < 2.0.17 bypass While breaking sanitizers is fun and I thoroughly enjoy doing it, I reached a point where I began to think whether I can contribute even more and propose a fix that will kill an entire class of bypasses.
A few days ago, the Anaconda project announced the PyScript framework, which allows Python code to be executed directly in the browser. Additionally, it also covers its integration with HTML and JS code. An execution of the Python code in the browser is not new; the pyodide project has allowed this for a long time...
Summary: During my research on other bug bounty program I've found Cross-Site Scripting vulnerability in cmp3p.js file, which allows attacker to execute arbitrary javascript code in context of domain that include mentioned script. Below you can find the way of finding bug bounty vulnerabilities from the beginning to the ...