Convert untrusted documents in a locked-down container
Offline tests: Python 3.11+, standard library.
Live tests: Docker with Linux containers (Docker Desktop or Docker Engine) and internet
access to build the converter image once (about 200 MB of Debian packages).

Reference solution: not included here, so the exercise does not hand you the answer.
Attempt the repair first. Then download document-conversion-sandbox-solution.zip from the
project card on https://softwaresecurity.com/labs/#project-document-conversion-sandbox and
extract it into this folder; it adds reference.py without changing your files, and the
reference commands below need it.

Goal
A web application turns uploaded DOCX, XLSX, ODT, ODS and PDF files into PDFs, text and
thumbnails. The parsers that do the work (LibreOffice and Ghostscript) are large C and C++
programs that read attacker-controlled bytes, so the worker in exercise.py treats each
conversion as hostile:
- preflight() looks at the package before any parser does: the type from the bytes, member
  names, member count, the bytes inflation actually produces, DTDs in XML parts and macros;
- sandbox_argv() runs one fresh container per document with no network, a read-only root,
  a size-limited tmpfs, a non-root user, no capabilities, no-new-privileges, Docker's default
  seccomp profile and limits on memory, CPU, processes and file size;
- run_with_deadline() kills the container, not just the docker client, at the deadline;
- validate_output() accepts only output that looks like the format asked for.
The document goes in on stdin and the result comes out on stdout, so nothing on the host is
mounted into the container.

1. See the gaps offline [OFFLINE: Python only]
   python3 -m unittest -v test_worker.py
   Expected starter: 6 failures in 12 tests (a 64 MB zip bomb and three DTD payloads pass
   preflight, the container keeps its network and a writable root, and a hung container
   outlives the deadline). fake_docker.py stands in for the docker CLI. Like real Docker,
   it runs the "container" as a separate process, so killing the client does not stop it.
   Reference: EXERCISE_MODULE=reference python3 -m unittest -v test_worker.py

2. Repair exercise.py
   Count inflated bytes while reading each member, refuse any DTD with an Expat parser,
   add --network=none, --read-only with a tmpfs and a file-size limit, and kill the
   container by name at the deadline. Run step 1 again.

3. Build the converter image [LIVE: Docker and internet access]
   docker build -t doc-converter .
   The Dockerfile pins debian:trixie-slim by digest and installs from the Debian snapshot
   of 11 October 2026, so a rebuild gets the same package versions. Move the digest and the
   snapshot date together, at least monthly.

4. Run the live tests [LIVE: Docker; no internet needed]
   EXERCISE_MODULE=reference python3 -m unittest -v test_live.py
   Then the same with your exercise.py (EXERCISE_MODULE=exercise, the default).
   - A 262 KB DOCX that inflates to 256 MiB is rejected before any container starts. Fed to
     LibreOffice directly, it hits the container's file-size limit instead.
   - DOCX files whose document.xml declares an external entity (a local file, a URL, or a
     parameter entity) are rejected; past preflight, LibreOffice refuses to load them and
     the canary server the tests start receives nothing.
   - A DOCX with a linked image converts with no request to the canary. A connection
     attempted from inside the sandbox fails with "Network is unreachable"; the same probe
     with networking on reaches the canary, which shows the test can fail.
   - A 17 KB DOCX with 100,000 page breaks is still being laid out at the 15-second
     deadline: the container is killed and no longer exists afterwards. A PostScript
     infinite loop sent to Ghostscript is killed the same way (preflight refuses
     PostScript in the first place).
   On a Linux Docker Engine, set CANARY_BIND=0.0.0.0 (or the docker0 address) so containers
   can reach the canary through host.docker.internal; Docker Desktop needs nothing extra.
   CONVERTER_IMAGE selects another image tag; CONTAINER_PREFIX changes the container names.

Execution status
The offline tests run in the site's test suite. The live tests were last run on
11 October 2026 on Docker 29.4.3 (Docker Desktop, Apple silicon, cgroup v2) with the image
built from this Dockerfile: LibreOffice 25.2.3 (Debian 4:25.2.3-2+deb13u8) and Ghostscript
10.05.1 (Debian 10.05.1~dfsg-1+deb13u3), driven by Python 3.14.7 on the host. Check yours with
  docker run --rm doc-converter dpkg-query -W libreoffice-core-nogui ghostscript
The image ID changes on every rebuild, because files written during the build, such as
package logs, carry timestamps; the package versions do not. The reference passed all 8 live
tests in about 35 seconds; the starter failed 5 and left two hung containers running, which
the tests now remove. LibreOffice 25.2 in --convert-to mode did not fetch any linked resource
we tried, even with the network on; the network block is what keeps that true for other
versions and other converters. Repeat the run after every image update.

Limits
This is a teaching worker, not content disarm and reconstruction. Output validation checks
the format, not whether a PDF carries JavaScript or embedded files; for documents people
will open, re-render pages to pixels (as Dangerzone does) or serve the result as a download
from a separate origin. Containers share the host kernel: for stronger isolation, run the
same flags under gVisor (--runtime=runsc) or in a microVM. Docker's own daemon is not part of
the sandbox, so run the worker on a host that does nothing else.

Dev container
.devcontainer/devcontainer.json opens this folder in VS Code Dev Containers or GitHub
Codespaces using the Dev Containers Python 3.13 image, pinned by digest, for the offline
steps. The live steps need Docker on the host, so run them outside the container.
