Context: Real code fix regardless of environment — highest blast radius in the review.
Problem: Free-play student submissions in C#/Python/JS run unsandboxed (in-process Roslyn Assembly.Load + Invoke; bare interpreter subprocesses) inside a container that holds RabbitMQ/Mongo/Redis/Gitea credentials. Guardrails are substring blocklists, bypassable via reflection, string concatenation, or __import__. A runaway loop cannot be cancelled (the invoke ignores the token) and unbounded output OOMs the worker. (C/C++ ARE correctly sandboxed via wasmtime — the model exists, just not applied to C#/Python/JS.)
Fix: Execute untrusted code in a real sandbox — locked-down container / gVisor / Firecracker with no secrets, no network, seccomp, memory/CPU cgroups, non-root — plus a hard wall-clock kill and output-size caps. Keep blocklists only as defense-in-depth.
Acceptance criteria:
No untrusted code runs in-process or with network/secret access
Wall-clock timeout hard-kills execution; output is bounded
Reflection / __import__ bypass no longer yields host or secret access
Effort: L
**Context:** Real code fix regardless of environment — highest blast radius in the review.
**Problem:** Free-play student submissions in C#/Python/JS run unsandboxed (in-process Roslyn `Assembly.Load` + `Invoke`; bare interpreter subprocesses) inside a container that holds RabbitMQ/Mongo/Redis/Gitea credentials. Guardrails are substring blocklists, bypassable via reflection, string concatenation, or `__import__`. A runaway loop cannot be cancelled (the invoke ignores the token) and unbounded output OOMs the worker. (C/C++ ARE correctly sandboxed via wasmtime — the model exists, just not applied to C#/Python/JS.)
**Evidence:**
- `SpikerSoft.Business/Domain/CodeExecution/Execution/RoslynCodeExecutor.cs:235-259` (`Assembly.Load` + `entry.Invoke`)
- `PythonLessonExecutor.cs:101-165` (raw CPython subprocess)
- Blocklists: `CodeExecutionWorkerService.cs:812-814`, `:652-686`
**Fix:** Execute untrusted code in a real sandbox — locked-down container / gVisor / Firecracker with no secrets, no network, seccomp, memory/CPU cgroups, non-root — plus a hard wall-clock kill and output-size caps. Keep blocklists only as defense-in-depth.
**Acceptance criteria:**
- No untrusted code runs in-process or with network/secret access
- Wall-clock timeout hard-kills execution; output is bounded
- Reflection / `__import__` bypass no longer yields host or secret access
**Effort:** L
spikerj
added the agentic label 2026-07-05 20:24:35 +00:00
Current-state audit (2026-07-17) — partial mitigation landed since filing; core gap stands
Re-verified the per-language safety boundary against master. Confirms this ticket's analysis and records what has changed since 2026-07-05:
Partial mitigation now in place (post-dates this ticket): the coderunner stack scoped its OpenBao role to secret/services/codeexecution only (#546 / epic #543). Per spikersoft-coderunner/docker-stack.yml, a sandbox escape reading /run/secrets now exposes only this worker's own git password — not the broad bao_services_* vault. That partially satisfies the "no secret access" acceptance criterion. The container also runs non-root (Dockerfile: USER $APP_UID).
Still open (the core ask is unchanged):
The container remains attached to the mongo, redis, and rabbitmq overlay networks and holds its own runtime connection strings in-process, so an escape can still reach those data stores over the network even without vault access.
Python/JS/C# safety boundary is still a substring blocklist only (PythonForbiddenFragments = __import__, eval(, os.system, ... — evaded by getattr(__builtins__,'ev'+'al'), ().__class__.__bases__[0].__subclasses__(), exec(bytes.fromhex(...))). Python runs as a raw python3 "<script>" subprocess (no -I, no restricted env).
Confirmed already-solid (no action needed) — the model to copy:
C / C++: compiled to wasm32-wasi, run via wasmtime run --dir={workDir}::/ main.wasm — per-run temp dir as the only mount, no network, stdio only, wall-clock timeout with Kill(entireProcessTree:true). (ClangLessonExecutor.cs)
SQL: in-process DuckDB, ephemeral in-memory, no fs/network.
No shell-injection surface anywhere: all subprocess spawns use UseShellExecute=false + ArgumentList (argv), not shell strings.
So the remaining work is squarely: bring C#/Python/JS up to the C/C++ wasmtime (or gVisor/Firecracker) isolation model + network egress removal from the untrusted-exec container. Effort L as noted.
### Current-state audit (2026-07-17) — partial mitigation landed since filing; core gap stands
Re-verified the per-language safety boundary against `master`. Confirms this ticket's analysis and records what has changed since 2026-07-05:
**Partial mitigation now in place (post-dates this ticket):** the coderunner stack scoped its OpenBao role to `secret/services/codeexecution` only (#546 / epic #543). Per `spikersoft-coderunner/docker-stack.yml`, a sandbox escape reading `/run/secrets` now exposes only this worker's own git password — **not** the broad `bao_services_*` vault. That partially satisfies the "no secret access" acceptance criterion. The container also runs non-root (`Dockerfile`: `USER $APP_UID`).
**Still open (the core ask is unchanged):**
- The container remains attached to the `mongo`, `redis`, and `rabbitmq` overlay networks and holds its own runtime connection strings in-process, so an escape can still reach those data stores over the network even without vault access.
- Python/JS/C# safety boundary is still a substring blocklist only (`PythonForbiddenFragments` = `__import__`, `eval(`, `os.system`, ... — evaded by `getattr(__builtins__,'ev'+'al')`, `().__class__.__bases__[0].__subclasses__()`, `exec(bytes.fromhex(...))`). Python runs as a raw `python3 "<script>"` subprocess (no `-I`, no restricted env).
**Confirmed already-solid (no action needed) — the model to copy:**
- **C / C++**: compiled to `wasm32-wasi`, run via `wasmtime run --dir={workDir}::/ main.wasm` — per-run temp dir as the only mount, no network, stdio only, wall-clock timeout with `Kill(entireProcessTree:true)`. (`ClangLessonExecutor.cs`)
- **SQL**: in-process DuckDB, ephemeral in-memory, no fs/network.
- No shell-injection surface anywhere: all subprocess spawns use `UseShellExecute=false` + `ArgumentList` (argv), not shell strings.
So the remaining work is squarely: bring **C#/Python/JS** up to the C/C++ wasmtime (or gVisor/Firecracker) isolation model + network egress removal from the untrusted-exec container. Effort **L** as noted.
Board-sweep status (2026-07-22): core sandbox NOT built (07-17 audit stands); only blast-radius mitigation landed (#546 scoped codeexec AppRole). REMAINING: the actual wasmtime-style sandbox for C#/Python/JS.
Board-sweep status (2026-07-22): core sandbox NOT built (07-17 audit stands); only blast-radius mitigation landed (#546 scoped codeexec AppRole). REMAINING: the actual wasmtime-style sandbox for C#/Python/JS.
Verified 2026-08-07 against spikersoft-backend@98102023 and spikersoft-infrastructure@86d03ff6, and live: docker service ls → spikersoft-coderunner_spikersoft-coderunner4/4 running, so this path executes untrusted submissions in production today.
Status: core sandbox not built.RoslynCodeExecutor.cs still does Assembly.Load(...) at :115/:235 with .Invoke(...) at :137/:259 — untrusted C# still runs in-process; PythonLessonExecutor.cs still spawns a raw ProcessStartInfo on PythonExecutable with no -I and no restricted env; the only Python guardrail is still the PythonForbiddenFragments substring blocklist. (Paths moved with the #566 decomposition — the code is now under SpikerSoft.Business.CodeExecution/.) wasmtime remains confined to the Clang/C++ executors.
Blast-radius mitigations that did land are recorded on the children: the container runs non-root and its Bao AppRole is scoped to secret/services/codeexecution only. The still-open network half — the container is attached to the mongo, redis and rabbitmq overlays — is the infrastructure child.
Closing here. Work now lives in the repos that hold the fix, so fixes #<M> in a PR will auto-close it on merge. The umbrella tracker keeps cross-repo epics only.
— Opus 5 Agent
Migrated to **spikerj/spikersoft-backend#565** and **spikerj/spikersoft-infrastructure#179** as part of the umbrella-tracker breakup.
This ticket needed changes in more than one repo, so it became one issue per repo:
- Backend (sandbox the C#/Python/JS executors — the C/C++ wasmtime model): spikerj/spikersoft-backend#565
- Infrastructure (remove the coderunner container's mongo/redis/rabbitmq network reach): spikerj/spikersoft-infrastructure#179
Verified 2026-08-07 against `spikersoft-backend@98102023` and `spikersoft-infrastructure@86d03ff6`, and live: `docker service ls` → `spikersoft-coderunner_spikersoft-coderunner` **4/4 running**, so this path executes untrusted submissions in production today.
Status: **core sandbox not built.** `RoslynCodeExecutor.cs` still does `Assembly.Load(...)` at :115/:235 with `.Invoke(...)` at :137/:259 — untrusted C# still runs in-process; `PythonLessonExecutor.cs` still spawns a raw `ProcessStartInfo` on `PythonExecutable` with no `-I` and no restricted env; the only Python guardrail is still the `PythonForbiddenFragments` substring blocklist. (Paths moved with the #566 decomposition — the code is now under `SpikerSoft.Business.CodeExecution/`.) wasmtime remains confined to the Clang/C++ executors.
Blast-radius mitigations that *did* land are recorded on the children: the container runs non-root and its Bao AppRole is scoped to `secret/services/codeexecution` only. The still-open network half — the container is attached to the `mongo`, `redis` and `rabbitmq` overlays — is the infrastructure child.
Closing here. Work now lives in the repos that hold the fix, so `fixes #<M>` in a PR will auto-close it on merge. The umbrella tracker keeps cross-repo epics only.
— Opus 5 Agent
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Context: Real code fix regardless of environment — highest blast radius in the review.
Problem: Free-play student submissions in C#/Python/JS run unsandboxed (in-process Roslyn
Assembly.Load+Invoke; bare interpreter subprocesses) inside a container that holds RabbitMQ/Mongo/Redis/Gitea credentials. Guardrails are substring blocklists, bypassable via reflection, string concatenation, or__import__. A runaway loop cannot be cancelled (the invoke ignores the token) and unbounded output OOMs the worker. (C/C++ ARE correctly sandboxed via wasmtime — the model exists, just not applied to C#/Python/JS.)Evidence:
SpikerSoft.Business/Domain/CodeExecution/Execution/RoslynCodeExecutor.cs:235-259(Assembly.Load+entry.Invoke)PythonLessonExecutor.cs:101-165(raw CPython subprocess)CodeExecutionWorkerService.cs:812-814,:652-686Fix: Execute untrusted code in a real sandbox — locked-down container / gVisor / Firecracker with no secrets, no network, seccomp, memory/CPU cgroups, non-root — plus a hard wall-clock kill and output-size caps. Keep blocklists only as defense-in-depth.
Acceptance criteria:
__import__bypass no longer yields host or secret accessEffort: L
Current-state audit (2026-07-17) — partial mitigation landed since filing; core gap stands
Re-verified the per-language safety boundary against
master. Confirms this ticket's analysis and records what has changed since 2026-07-05:Partial mitigation now in place (post-dates this ticket): the coderunner stack scoped its OpenBao role to
secret/services/codeexecutiononly (#546 / epic #543). Perspikersoft-coderunner/docker-stack.yml, a sandbox escape reading/run/secretsnow exposes only this worker's own git password — not the broadbao_services_*vault. That partially satisfies the "no secret access" acceptance criterion. The container also runs non-root (Dockerfile:USER $APP_UID).Still open (the core ask is unchanged):
mongo,redis, andrabbitmqoverlay networks and holds its own runtime connection strings in-process, so an escape can still reach those data stores over the network even without vault access.PythonForbiddenFragments=__import__,eval(,os.system, ... — evaded bygetattr(__builtins__,'ev'+'al'),().__class__.__bases__[0].__subclasses__(),exec(bytes.fromhex(...))). Python runs as a rawpython3 "<script>"subprocess (no-I, no restricted env).Confirmed already-solid (no action needed) — the model to copy:
wasm32-wasi, run viawasmtime run --dir={workDir}::/ main.wasm— per-run temp dir as the only mount, no network, stdio only, wall-clock timeout withKill(entireProcessTree:true). (ClangLessonExecutor.cs)UseShellExecute=false+ArgumentList(argv), not shell strings.So the remaining work is squarely: bring C#/Python/JS up to the C/C++ wasmtime (or gVisor/Firecracker) isolation model + network egress removal from the untrusted-exec container. Effort L as noted.
Board-sweep status (2026-07-22): core sandbox NOT built (07-17 audit stands); only blast-radius mitigation landed (#546 scoped codeexec AppRole). REMAINING: the actual wasmtime-style sandbox for C#/Python/JS.
Migrated to spikerj/spikersoft-backend#565 and spikerj/spikersoft-infrastructure#179 as part of the umbrella-tracker breakup.
This ticket needed changes in more than one repo, so it became one issue per repo:
Verified 2026-08-07 against
spikersoft-backend@98102023andspikersoft-infrastructure@86d03ff6, and live:docker service ls→spikersoft-coderunner_spikersoft-coderunner4/4 running, so this path executes untrusted submissions in production today.Status: core sandbox not built.
RoslynCodeExecutor.csstill doesAssembly.Load(...)at :115/:235 with.Invoke(...)at :137/:259 — untrusted C# still runs in-process;PythonLessonExecutor.csstill spawns a rawProcessStartInfoonPythonExecutablewith no-Iand no restricted env; the only Python guardrail is still thePythonForbiddenFragmentssubstring blocklist. (Paths moved with the #566 decomposition — the code is now underSpikerSoft.Business.CodeExecution/.) wasmtime remains confined to the Clang/C++ executors.Blast-radius mitigations that did land are recorded on the children: the container runs non-root and its Bao AppRole is scoped to
secret/services/codeexecutiononly. The still-open network half — the container is attached to themongo,redisandrabbitmqoverlays — is the infrastructure child.Closing here. Work now lives in the repos that hold the fix, so
fixes #<M>in a PR will auto-close it on merge. The umbrella tracker keeps cross-repo epics only.— Opus 5 Agent