NexCTF Sandbox
A NexCTF plugin that checks answers by running Python: a code runner that tests players' programs against test cases, and a script checker that validates answers with a function you write. Every run happens in a throwaway microsandbox microVM, booted for the submission and removed right after.
Quick Start
Add the plugin to NexCTF's NEXCTF_PLUGINS and restart it; NexCTF installs it from PyPI and applies its migrations:
NEXCTF_PLUGINS=nexctf-sandbox==1.0.0
Each plugin release supports one NexCTF minor version, so upgrade the two together:
| nexctf-sandbox | NexCTF |
|---|---|
| 1.0.x | 0.12.x |
The NexCTF process that checks submissions boots the microVMs, so it needs read and write access to /dev/kvm. Nothing else: the network setting is enforced by microsandbox's own network stack, with no extra capability or root.
With NexCTF's compose.yml, that goes on the app service, e.g. in a compose.override.yml. /dev/kvm belongs to the host's kvm group, which the image's unprivileged user has to join; get its id with stat -c %g /dev/kvm:
services:
app:
devices:
- /dev/kvm
group_add:
- "993" # the kvm group id on the host
environment:
NEXCTF_PLUGINS: nexctf-sandbox==1.0.0
Then, on a question, pick the solution type runner or script.
Features
🏃 Code runner
- Runs the player's Python 3 code against up to 20 test cases, feeding each one's input on stdin
- Accepted only when every case's stdout matches the expected output, whitespace stripped
- A crash or a non-zero exit fails the case
- All the cases of a submission share one microVM, run one after the other
- For questions with a code input
✅ Script checker
- Validates the answer with your own Python function,
check(answer, team_id, team_fields) -> bool - Hands the checker the team's custom fields, typed, private ones included: per-team flags and seeds work out of the box
- The starter checker lists the team custom fields that exist
- Checkers written without
team_fieldskeep working as they are - For questions with an input, text or code input
Writing a checker
The checker is a function named check that returns True to accept the answer. team_id is the team's id as a string, or None; team_fields maps each team custom field name to its value (int, bool or str), and a key is missing when the team left the field empty. Declare team_fields only if you need it:
import hashlib
def check(answer: str, team_id: str | None, team_fields: dict) -> bool:
seed = team_fields.get("seed", "")
flag = "FLAG{" + hashlib.sha256(seed.encode()).hexdigest()[:16] + "}"
return answer.strip() == flag
The checker runs in its own microVM, with the base image's Python and the network access set below.
Settings
Under Settings → Plugins → Sandbox:
| Setting | Default |
|---|---|
| Base image | python:3.14-slim; any OCI image with python3 on PATH |
| vCPUs | 1 |
| Memory | 256 MiB, plus the root disk |
| Root disk | 64 MiB, in RAM |
| Network access | disabled; internet for public addresses and the host's DNS, all for unfiltered egress, the host's own network included |
| Max concurrent microVMs | 8, across all challenges; takes effect after a restart |
Development
The tests stub out the microVMs and need no database. NexCTF is a dev dependency installed from git (currently its v0.12.0 tag).
uv sync
uv run pytest
uv run ruff check . && uv run ruff format --check . && uv run ty check .
The live tests boot real microVMs and skip without /dev/kvm; the one reaching the internet also skips when the host has none. Run them on the host that will check submissions before an event:
uv run pytest -m live
To load a local checkout in a dev NexCTF, add its path to NEXCTF_PLUGINS in NexCTF's .env.dev.override, then restart task dev:backend.
Metadata
Release files for nexctf-sandbox 1.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| nexctf_sandbox-1.0.0.tar.gz | 11.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| nexctf_sandbox-1.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 26.4 kB
Release files / nexctf_sandbox-1.0.0.tar.gz
| Download URL | nexctf_sandbox-1.0.0.tar.gz |
|---|---|
| Size | 11.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
f61c15a54d017bf66e83ca5d9840a62af44c564524f876a74f4d19f02f960f9d
|
|
BLAKE2b-256 checksum How to use checksums |
9306178930fdb2945c133880d1813bdb3765b57a8180906f16cbb16d783c2be4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 11, 2026.
Transparency logRelease files / nexctf_sandbox-1.0.0-py3-none-any.whl
| Download URL | nexctf_sandbox-1.0.0-py3-none-any.whl |
|---|---|
| Size | 14.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b69a59e036c549aaa61387b3ddeead1e71c6de6cf5655afea03eda054f9a7a20
|
|
BLAKE2b-256 checksum How to use checksums |
24378fb5ab485d69148ab13c170a532737b04eb0d2266917b14c56e660bdde47
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 11, 2026.
Transparency log