Source: Surfaced during retrospective verification of #443 against the live local cluster.
Severity: medium. Auth-gated (ornn:skill:create), but auth is cheap to acquire — any signed-up user can upload a structured zip-bomb that fills storage, occupies the AgentSeal scan subprocess, and stalls extraction.
Current state
#443 added a 50 MB cumulative uncompressed-size cap only in ornn-web/src/utils/zipValidator.ts — the JSZip pre-upload check before the SPA POSTs. Any client that hits /api/v1/skills directly (the SDKs, an agent over MCP, raw curl) skips that path.
Empirically confirmed today:
# 50 MB of zeros in assets/big.bin, properly structured under a kebab-case folder
$ curl -X POST -H "Content-Type: application/zip" --data-binary @bomb.zip /api/v1/skills
HTTP/1.1 201 Created
Location: /api/v1/skills/31b8b646-3c64-42be-be0d-fdf417812de4
The compressed payload was ~51 KB; the uncompressed contents would be ~50 MB. Backend accepted, uploaded to MinIO, queued AgentSeal scan.
Gap
/api/v1/skills (POST) and /api/v1/skills/:id (PUT) currently apply:
MAX_PACKAGE_SIZE_BYTES env-bound check on the request body (compressed size only) → fails-open on small-compressed / large-uncompressed bombs.
- ZIP format validation in
validateZipFormat (file structure, frontmatter, allowed root items) — doesn't sum uncompressed sizes.
Nothing on the backend asserts a cumulative uncompressed-size cap or per-entry size cap.
Recommendation
Mirror the client guard in ornn-api/src/shared/utils/zip*.ts (or in validateZipFormat in domains/skills/crud/service.ts):
- Cumulative uncompressed-size cap. Walk the ZIP entries before extraction; reject if the sum of
entry.uncompressedSize exceeds the cap (default 50 MB, env-overridable as MAX_PACKAGE_UNCOMPRESSED_BYTES). Reject with 413 payload_too_large + a code: "uncompressed_too_large" per CONVENTIONS.md §1.4.
- Per-entry uncompressed-size cap. Same shape, e.g. 25 MB per entry — catches a single oversized file even if the total stays under the cap (defence-in-depth).
- File-count cap. A ZIP with 100 k entries each just over the minimum is a separate DoS vector — reject above some sane count (e.g. 1000).
- Compression-ratio sanity check. If
total_uncompressed / total_compressed > 100, flag — that's the classic zip-bomb signature.
- Unit tests for each guard. Integration test against
POST /api/v1/skills with a curated bomb fixture.
Same guards on the GitHub-pull path (fetchSkillFromGitHub builds a ZIP from the repo contents — bounded by GitHub's per-blob limits already, but parity is cleaner).
Related
Source: Surfaced during retrospective verification of #443 against the live local cluster.
Severity: medium. Auth-gated (
ornn:skill:create), but auth is cheap to acquire — any signed-up user can upload a structured zip-bomb that fills storage, occupies the AgentSeal scan subprocess, and stalls extraction.Current state
#443 added a 50 MB cumulative uncompressed-size cap only in
ornn-web/src/utils/zipValidator.ts— the JSZip pre-upload check before the SPA POSTs. Any client that hits/api/v1/skillsdirectly (the SDKs, an agent over MCP, rawcurl) skips that path.Empirically confirmed today:
The compressed payload was ~51 KB; the uncompressed contents would be ~50 MB. Backend accepted, uploaded to MinIO, queued AgentSeal scan.
Gap
/api/v1/skills(POST) and/api/v1/skills/:id(PUT) currently apply:MAX_PACKAGE_SIZE_BYTESenv-bound check on the request body (compressed size only) → fails-open on small-compressed / large-uncompressed bombs.validateZipFormat(file structure, frontmatter, allowed root items) — doesn't sum uncompressed sizes.Nothing on the backend asserts a cumulative uncompressed-size cap or per-entry size cap.
Recommendation
Mirror the client guard in
ornn-api/src/shared/utils/zip*.ts(or invalidateZipFormatindomains/skills/crud/service.ts):entry.uncompressedSizeexceeds the cap (default 50 MB, env-overridable asMAX_PACKAGE_UNCOMPRESSED_BYTES). Reject with413 payload_too_large+ acode: "uncompressed_too_large"per CONVENTIONS.md §1.4.total_uncompressed / total_compressed > 100, flag — that's the classic zip-bomb signature.POST /api/v1/skillswith a curated bomb fixture.Same guards on the GitHub-pull path (
fetchSkillFromGitHubbuilds a ZIP from the repo contents — bounded by GitHub's per-blob limits already, but parity is cleaner).Related
docs/CONVENTIONS.md§1.4 —payload_too_largeerror code is already documented.