Introduction
Sometimes your code needs to execute someone else’s code. This may be a requirement when offering high-flexibility solutions for your project, for example, when working with data analytics or providing code editors within your web application. Execution of user-supplied code can be among developers’ worst security nightmares. Without proper isolation, user-supplied code may do everything from vandalizing your web app to causing a total takeover of the underlying system. RCE (Remote Code Execution) may be considered among the most critical vulnerabilities your system may be susceptible to and we want to allow code execution as a service. Security concerns are obvious.
Some common ways of achieving isolation include the use of Virtual Machines, Containers, OS-level isolation (running code under a specific permission set), and many more.
In this article, we will explore the option of sandboxing using Web Assembly.
We will analyze two different approaches to using Web Assembly, two different technologies for running Web Assembly, and run a performance benchmark for various use cases.
WebAssembly (Wasm)
WebAssembly (Wasm) is a type of code that can be run in modern web browsers. It is a low-level assembly-like language with a compact binary format that runs with near-native performance and provides languages such as C/C++, C# and Rust with a compilation target so that they can run on the web. It is also designed to run alongside JavaScript, allowing both to work together. (reference: https://developer.mozilla.org/en-US/docs/WebAssembly).
Because Wasm is a compilation target for lower level-languages, Python is (was) not commonly used with Wasm. Compilation of Python code to Wasm is not always straightforward, especially when Python libs depend on C/C++, Rust, or another compiled language. However, Python is often a primary language choice for developers nowadays, and the need for adequate code isolation is present.
WASI
The WebAssembly System Interface (WASI) is a standardization effort that aims to define how Wasm modules interact with the underlying operating system. WASI-compatible runtimes may interact with the filesystem in a limited way, and we can configure them according to our security needs.
Compiling Python to Wasm
To run our Python code as a Wasm module, we need to compile it into Wasm. Luckily for us, there is an out-of-the-box tool called py2wasm. To run the compiled code locally, we need to use a Wasm runtime such as Wasmer or Wasmtime.
To install either of these runtimes, run:
curl https://get.wasmer.io -sSfL | sh curl https://wasmtime.dev/install.sh -sSf | bash
Using py2wasm is as simple as:
pip install py2wasm py2wasm myprogram.py -o myprogram.wasm # either wasmer or wasmtime wasmer myprogram.wasm
This approach may be useful for porting your Python project to run within the browser, to run precompiled programs, or to add a layer of security to the potentially vulnerable code, but it does come with its drawbacks. The compilation process is lengthy, and output files are not compact (hello_world.py compiled to Wasm is approx. 25 MB). This is not the optimal way of isolating user-supplied code due to the compilation time and file size overhead. However, as we will see in our tests, this approach provides performance benefits and would be a good choice for running code that is not subject to frequent changes.
However, as simple as this seems, only native Python dependencies will be compiled successfully. That means to use libraries such as numpy, you will have to compile their code to Wasm manually, which is outside of the scope of this article.
Runtime + Python Interpreter
There is a way to eliminate compilation overhead by having the Python Interpreter itself compiled to Wasm and allow the runtime to have limited access to the filesystem. This way, the interpreter runs in an isolated environment with sufficient file system access to read the Python files. and all loaded Python scripts are isolated because the Python Interpreter is isolated.
To achieve this, we do the following:
- Download a Wasm-compiled Python Interpreter from VMWare Labs.
- We need to mount the site-packages directory to a Wasm environment so that libraries can be accessed from within it by utilizing the wasmtime’s –dir argument.
- Use the following command line to run the interpreter with the desired python file:
wasmtime --dir <python_files_directory> --dir .venv/lib/python3.11/site-packages::/usr/local/lib/python3.12/site-packages wasm-python.wasm <python_file>
–dir specifies the folders WASI runtime has access to.
wasm-python.wasm is our Wasm-compiled Python interpreter.
<python_file> is a file within the <python_files_directory> that we intend to run.
With everything set, we can proceed with some benchmarks and use cases.
Benchmarks
Let’s first look at the performance. We have provided some trivial test scripts to be run. Test times were averaged over multiple executions, and the first execution was not included in the overall average due to its tendency to yield much longer execution times.
Test: Hello World
A basic hello world example:
def main():
print("Hello World!")
if __name__ == "__main__":
main()

Conclusion: Right away, we can see the overhead caused by running the Python interpreter as a Wasm module.
Test: Pyjokes
A simple script that includes a library import:
import pyjokes
def main():
joke_candidates = [pyjokes.get_joke() for i in range(5)]
print(min(joke_candidates, key=len))
if __name__ == "__main__":
main()

Test: Rational expression sum – 10 million iterations
We used the expression x^2 - 14/(x+1) + 100 to avoid run-time optimizations and summed it for 10 million values of X. Since there is a considerable workload, we also included a native Python benchmark.
import time
def main():
time_start = time.time()
i = 0
workload_calculation = 0
while i < 10000000:
workload_calculation += (i * i) - 14.0 / (i + 1) + 100.0
i = i + 1
print(f"Time spent executing main(): {time.time() - time_start}")
if __name__ == "__main__":
main()
Simple security test
One of the common web-based attacks is path traversal (https://owasp.org/www-community/attacks/Path_Traversal). Usually a malicious actor will try to access credential files or try to exfiltrate other sensitive information.
Let’s say we are providing code execution as a service, and a user submits this code:
def main():
passwd_file = open("/etc/shadow").read()
print(passwd_file)
if __name__ == "__main__":
main()
Running this code without isolation will print the system’s /etc/shadow file (on Linux systems). This file contains password hashes of the users.
If we were to run this script without isolation within the docker container, we would get:
/ # python3 script.py root:$6$/LyL8uP/a8nudhMB$Q8vYDlS/O6UHJOEXuoqXAnQVBsrCgn1GnvWw/8gvCF6P8Cl3bAVMtWQYH3KMdW/4iXqlZUuEHQ/yp5im0t4li0:20220:0:::::
An attacker might be able to brute force this hash and gain credentials.
If we run this script compiled to Wasm we would get:
Traceback (most recent call last): File "./etc_shadow.py", line 6, in <module> File "./etc_shadow.py", line 2, in main FileNotFoundError: [Errno 44] No such file or directory: '/etc/shadow'
Wasm compiled programs do not have access to the host filesystem (by default).
Leveraging SDK with the Python Interpreter
To use Wasmtime in your program, you may leverage the SDK it provides. It is available in a number of popular programming languages, listed here.
Here is a simple example of how to leverage the Wasmtime’s Python SDK to create a specific WASI config. Using this approach imposes even greater performance drawbacks, with our tests showing approximately an additional 1.2 seconds of overhead compared to regular interpreter operation.
from wasmtime import Engine, Store, WasiConfig, Module, Linker, DirPerms
import argparse
import time
def run_wasm_python(python_file):
engine = Engine()
store = Store(engine)
wasi_config = WasiConfig()
wasi_config.inherit_stderr()
wasi_config.inherit_stdout()
wasi_config.argv = ("python", python_file)
wasi_config.preopen_dir(
"./examples",
"./examples",
dir_perms=DirPerms.READ_ONLY
)
# Change Python versions as needed
wasi_config.preopen_dir(
"./.venv/lib/python3.11/site-packages",
"/usr/local/lib/python3.12/site-packages",
dir_perms=DirPerms.READ_ONLY
)
store.set_wasi(wasi_config)
module = Module.from_file(engine, "./wasm-python/python-3.12.0.wasm")
linker = Linker(engine=engine)
linker.define_wasi()
instance = linker.instantiate(store, module)
start = instance.exports(store)["_start"]
try:
start(store)
except Exception as e:
print(e)
raise e
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--path")
args = vars(parser.parse_args())
time_start = time.time()
run_wasm_python(python_file=args["path"])
print(f"Total time spent executing: {time.time() - time_start}")
Conclusions
Both technologies are up to the task at hand: providing an isolated environment for code execution. We also see the pros and cons of using two different approaches. Both Wasmer and Wasmtime offer an SDK that allows you to specifically configure Wasi to work according to your specific use case. Performance edge changes from version to version, so why pick Wasmtime over Wasmer?
As of this writing, Wasmtime has been more rigorously tested for vulnerabilities. This can be viewed from the track record found at Github’s Security tab, or in the Snyk Security Database.
There are far more reported vulnerabilities in Wasmtime. However, rather than being an indicator of poor security, it is an indicator of effective security testing.
Simply put, Wasmtime prioritizes security, even if it comes with a performance trade-off. It even has mitigations against hardware-based exploits, such as Spectre.
Wasmer, on the other hand, may occasionally provide better performance, but it also offers its own cloud services that allow you to run your apps within Wasm execution environments instead of containers. The Wasmer CLI also comes with numerous utilities that enable you to interact with the Wasmer ecosystem. It is not a solution for a single problem, but more like a suite of tools to run your existing apps in the Wasm cloud.
“Sandboxing Python code execution with WASM” Tech Bite was brought to you by Nikola Kovač, Junior Software Engineer at Atlantbh.
Tech Bites are tips, tricks, snippets or explanations about various programming technologies and paradigms, which can help engineers with their everyday job.
