0017. One upload guard and safe extractor for every upload path
Generated from docs/decisions/0017-upload-guard.md
- Status: Accepted (2026-10-09)
- Date: 2026-10-09
Context and problem statement
Section titled “Context and problem statement”SCORM, cmi5, course import and the file manager each handled uploads on their own. SCORM and cmi5
used ZipArchive::extractTo without entry checks (zip-slip), course import read files outside the
archive through paths in content.json, and SVG/HTML uploads were served inline from the storage
origin. Phase 1 decisions 2 and 14–19.
Decision
Section titled “Decision”- A package
api/packages/uploadswith an upload guard (size, MIME and extension per upload kind, optional virus scan) and a safe extractor (entry by entry: no absolute or..paths, no symlinks, limits on entry count, total size and compression ratio) used by every path that accepts archives: SCORM, cmi5, course import, LiaScript and the Adapt build import. - Virus scanning through an optional clamd service (compose profile
av), off by default, failing closed when enabled and unreachable. - Active content (SVG, HTML, XML, unknown types) is stored with
Content-Disposition: attachmentand an extension-basedContent-Type; the storage origin addsscript-src 'none'; sandboxfor SVG. No sanitiser. - Course import resolves every path from
content.jsoninside the extracted archive (ImportPath); nested archives are allowed and checked when imported themselves.
Consequences
Section titled “Consequences”- One place to tighten limits; every new upload path must use the guard.
- Old course exports with absolute entry names still import (a leading
/is stripped for course imports only).