Skip to content

Security: jkmpod/goboxd

Security

docs/security.md

Security

This document describes the mitigations applied to limit the damage an adversarial submission can do. File and line references point to the implementation in this repository.


1. Oversize HTTP body rejected at the HTTP layer

Risk: A client sends a multi-megabyte body to exhaust memory or cause a slow read.

Mitigation: http.MaxBytesReader wraps the request body with a 256 KiB hard cap before any JSON decoding begins.

internal/handler/run.go:60r.Body = http.MaxBytesReader(w, r.Body, maxBodyBytes)

The limit constant is defined at internal/handler/run.go:15.


2. Oversize source rejected at the application layer

Risk: A 256 KiB source string passes the HTTP cap but takes excessive time or memory during compilation.

Mitigation: After JSON decoding and language lookup, the source length is checked against a 64 KiB cap. This sits below the HTTP cap so the two limits are distinguishable.

internal/handler/run.go:99 — source length check; run.go:16 — the constant.


3. Flag injection blocked by a per-language allow-list

Risk: A caller passes compiler or interpreter flags that write to arbitrary paths (-o /etc/passwd), load plugins (-fplugin=evil.so), or change security-relevant settings.

Mitigation: Each language in config/languages.yaml declares a flag_allowlist. Every caller-supplied flag is matched against the list using path.Match glob patterns. A flag that does not match any pattern is rejected with 400 disallowed_flag. A missing or empty allow-list denies all caller flags.

internal/sandbox/flags.go:17CheckFlagAllowlist internal/handler/run.go:105 — enforcement in the handler


4. Path traversal blocked in source filenames

Risk: A caller sets filename to ../../etc/passwd or similar to write the source file outside the working directory.

Mitigation: ValidateFilename rejects any name that is empty, longer than 255 bytes, starts with a dot, contains a path separator, or differs from its own filepath.Base.

internal/sandbox/sandbox.go:76ValidateFilename internal/handler/run.go:115 — enforcement in the handler


5. Predictable or colliding working directories prevented

Risk: If working directory names are sequential or time-based, an attacker could predict the next name and plant files, or two concurrent requests could collide.

Mitigation: Each working directory is named goboxd-<pid>-<atomicCounter>-<random> where the random suffix comes from os.MkdirTemp. The pid and an atomic counter make the names unique across processes and concurrent goroutines without relying on randomness alone.

internal/sandbox/sandbox.go:21CreateWorkDir


6. Subprocess output capped to prevent memory exhaustion

Risk: A program that prints without limit drives the server's heap into OOM.

Mitigation: limitWriter wraps bytes.Buffer and silently discards writes beyond 1 MiB per stream. Both stdout and stderr are capped independently. Truncated output gets a [truncated] suffix.

internal/sandbox/nsjail.go:92 — stdout and stderr writers


7. Orphaned working directories cleaned up on restart

Risk: If the server process is killed mid-request, the deferred sandbox.Cleanup never runs, leaving working directories on disk indefinitely.

Mitigation: At startup, SweepOrphans scans for goboxd-* directories older than 10 minutes and removes them. This is a backstop for the defer that covers normal operation.

internal/sandbox/sandbox.go:30SweepOrphans cmd/goboxd/main.go:29 — called at startup


8. Per-process time and memory limits enforced by nsjail

Risk: A submission runs forever or allocates unbounded memory.

Mitigation: nsjail is invoked with --time_limit (wall-clock seconds) and --rlimit_as (address-space MiB) on every run. These come from the language config's timeout_seconds and the per-request memory_mb override (default 256 MiB). nsjail enforces these limits in the kernel via rlimits and kills the process when they are exceeded.

internal/sandbox/nsjail.go:52-53--rlimit_as and --time_limit in args


9. Bounded concurrency with graceful queue rejection

Risk: A flood of concurrent requests causes unbounded goroutine and nsjail process growth, exhausting CPU and memory.

Mitigation: A buffered-channel semaphore limits active sandbox executions to GOBOXD_MAX_CONCURRENT (default runtime.NumCPU()). Excess requests wait up to GOBOXD_QUEUE_TIMEOUT_S (default 8 s) for a slot to open. On timeout the handler returns HTTP 429 server_busy rather than accumulating goroutines. The semaphore slot is released immediately after runner.Run returns — before the JSON response write — so a slow-reading client cannot pin a sandbox execution slot.

internal/handler/limiter.godefaultLimiter, queueTimeout internal/handler/run.go:123TryAcquire before runner.Run


10. Syscall filtering via Kafel seccomp policy

Risk: A sandboxed process calls a privileged or dangerous kernel interface (ptrace, bpf, mount, kexec_load, etc.) to escape the sandbox or corrupt the host.

Mitigation: nsjail is compiled with Kafel support and launched with --seccomp_policy_file /etc/goboxd/seccomp.policy. The policy uses DEFAULT ALLOW with KILL_PROCESS on 27 syscalls covering:

  • Ptrace / process introspection: ptrace, process_vm_readv, process_vm_writev
  • Kernel module loading / kexec: init_module, finit_module, delete_module, kexec_load, reboot
  • Clock manipulation: settimeofday, adjtimex, clock_adjtime
  • Namespace / device access: mknodat, chroot, pivot_root, unshare, setns, userfaultfd, name_to_handle_at, open_by_handle_at
  • Audit / capability / keyring: acct, bpf, syslog, add_key, request_key, keyctl, fanotify_init, capset, mount

DEFAULT ALLOW preserves full Python, Java, Ruby, and Zig stdlib access. The policy is loaded conditionally via os.Stat — absent in the dev/tools image, the --seccomp_policy_file flag is simply not passed.

scripts/nsjail-seccomp.policy — Kafel policy
internal/sandbox/nsjail.go — conditional os.Stat load in BuildNsjailArgs


11. Additional nsjail rlimits

Risk: A sandbox process creates unlimited child processes (fork bomb), writes a core dump exhausting disk, consumes excessive stack, or monopolises a CPU core unfairly.

Mitigation: Four additional rlimits are set on every nsjail invocation:

Flag Value Protection
--rlimit_nproc 128 Fork-bomb cap; above JVM thread count (~40), far below OS capacity
--rlimit_core 0 No core dumps — prevents disk exhaustion in /tmp
--rlimit_stack 8 MiB Matches the standard Linux default; prevents stack overflow cascades
--max_cpus 1 One CPU per jail — fairness and reproducible wall-clock timing

internal/sandbox/nsjail.go:BuildNsjailArgs — all four flags added unconditionally

There aren't any published security advisories