Skip to content

[Bug] Backend /skills upload accepts large uncompressed ZIPs — mirror #443 cap server-side #632

@sh1ningwang

Description

@sh1ningwang

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):

  1. 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.
  2. 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).
  3. 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).
  4. Compression-ratio sanity check. If total_uncompressed / total_compressed > 100, flag — that's the classic zip-bomb signature.
  5. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions