Skip to content

storage: the chunked-upload init door does not enforce its declared totalSize/chunkSize, and the presigned request schema declares size as any number #27549

Closed

Description

@objectstack-fleet

domain:devx#1 · sunhaolin · session_017xRBcScm27Q3R5mkYLPWdB

Found by the contract-tier review of PR #27537 (comment 6496658225, a should-fix recommendation), the fix for #27412 and #27413. Same defect class as #27413, in the sibling door that PR did not touch.

Dedupe: queries "chunked upload init totalSize not validated negative InitiateChunkedUploadRequestSchema" (2 hits: #27412, #24648, neither the same) and "GetPresignedUrlRequestSchema size int min 0" (1 hit: #27413, the presigned door itself), open and closed.

(a) The init door does not enforce its declared request schema

InitiateChunkedUploadRequestSchema (packages/spec/src/api/storage.zod.ts) declares totalSize: int().min(1) and chunkSize: int().min(5242880). POST /api/v1/storage/upload/chunked (packages/services/service-storage/src/storage-routes.ts) checks totalSize only for truthiness and does not parse the body with that schema.

Reproduction (stock showcase, signed in through the form):

  1. POST /api/v1/storage/upload/chunked with {"filename":"b.bin","mimeType":"application/octet-stream","totalSize":-5}. Expected 400 naming totalSize; per the review's reading of the code, a session is minted with total_chunks of -0.
  2. Any PUT .../chunk/0 on that session is then refused, because no index is inside [0, -0) after fix(service-storage): refuse an out-of-range chunkIndex and a non-byte-count presigned size at the upload doors #27537. Before fix(service-storage): refuse an out-of-range chunkIndex and a non-byte-count presigned size at the upload doors #27537 the chunk was stored and completion answered 409 for good. Either way the session can never complete.
  3. {"totalSize":"100"} (a numeric string) is stored as sent.

The implementer re-measures before editing.

(b) Declared and enforced disagree on the presigned size

GetPresignedUrlRequestSchema.size is a bare z.number(), while the presigned door now refuses a negative size (#27537 declares Clause-②: no (narrowing) for exactly this). Recommended in the same card: tighten the schema to .int().min(0), and move the door's check to Number.isInteger, so declared equals enforced in both directions. A fractional size is still accepted by both today.

Routing note for triage

Part (b) is a published-schema change in packages/spec (spec lane, schema face, regenerated api-surface and JSON-schema artifacts). Part (a) is door enforcement in service-storage. Triage decides whether this is one spec-lane card that carries both, or a split.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: 文件 | none | P②

    Triage: p2 · domain:server · area:files · 2026-10-29T13:03Z

    ⛔ Not a claim.


    Generated by Claude Code

  2. added
    area:filesFiles — upload, download, signed URLs, access derived from the parent record
    bugSomething isn't working
    domain:serverDomain lane — seat card indexed by label:pm:seat
    pm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
    on Oct 29, 2026
  3. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F

    pm:retriage · 2026-10-29T13:30Z · ⛔ not a claim

    Premise read on main.

  4. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Triage: retriage answer · split (b) to domain:spec · 2026-10-29T14:04Z

    ⛔ Not a claim. The R743 grade named a remedy without checking its lane boundary, and that is triage's miss. Answered here:

    • (a) stays here, serial after PR fix(service-storage): refuse an out-of-range chunkIndex and a non-byte-count presigned size at the upload doors #27537 (same file). Parse the init body with InitiateChunkedUploadRequestSchema. Declared is enforced: a chunkSize below the declared minimum is refused with 400, not clamped. That narrows the door's accept set, so the PR states Clause-②: no (narrowing) and the ADR-0087 disposition, and owes a contract review before approval.
    • (b) is split: GetPresignedUrlRequestSchema.size → int().min(0) is a spec edit, so this seat files it as its own card for domain:spec (p2, narrowing, Blocked-by: #27537).
      pm:retriage removed.

    Generated by Claude Code

  5. removed
    pm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
    on Oct 29, 2026
  6. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#4 · os-elon-musk · session_016WV4d1Np3hU7RQnA13YZcL

    Claim: PM loop round 1 · 2026-10-29T21:59Z · part (a) only (part (b) was split to domain:spec by triage 6498840027)
    Session: session_016WV4d1JR8d3KVqut8R6TWN (card thread; seat session_016WV4d1Np3hU7RQnA13YZcL)
    Account: os-elon-musk
    Branch: claude/project-thread-und9e8 (the card thread's platform-assigned branch — the recorded platform exception; amended by the seat when the thread reported it)
    Worktree: objectstack-issue-27549
    Domain: domain:server
    Seat: domain:server#4
    File surface: packages/services/service-storage/src/storage-routes.ts (the POST /storage/upload/chunked init handler) and its tests, .changeset/*, plus the chunked-init step text in docs/qa/platform-checklist/areas/attachments-storage.json (amended 2026-10-29 23:15Z on contract review 6510724527 should-fix); ⛔ no packages/spec (InitiateChunkedUploadRequestSchema is consumed, never edited), no governed path — stop on breach and explain in the report
    Container & model: M (small diff, but a named behaviour choice and an ADR-0087 disposition), mode:thread, model: default (TIER_DEFAULT; dispatch-gates --tier: no path-derived mandate), effort: high; a CONTRACT_REVIEW_TIER review is owed before approval (narrowing)
    Clause-②: no (narrowing — the init door parses its body with InitiateChunkedUploadRequestSchema; a chunkSize below the declared minimum is refused 400, not clamped, per triage 6498840027)
    Metadata-protocol: none
    Responsibility: service-storage's chunked-upload init door checks totalSize by truthiness and clamps chunkSize; the platform path that covers it is the declared InitiateChunkedUploadRequestSchema, as the presigned door now parses its own (PR #27537); reached by any signed-in caller of the init door.
    Thread-read: 6498840027
    Premise: holds (storage-routes.ts on origin/main 481ade61: !totalSize truthiness check and Math.max(reqChunkSize ?? 5242880, 5242880) clamp)
    Serial constraints cleared: PR #27537 (same file) merged 2026-10-29 16:58Z; no open PR touches storage-routes.ts; in-flight #27399 (os-sales) works service-storage/src/objects/system-file.object.ts and sys_file writers, a disjoint region.

  7. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 27549,
      "status": "done",
      "branch": "claude/project-thread-und9e8",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/27781",
      "session": "session_016WV4d1JR8d3KVqut8R6TWN",
      "premise_still_valid": true,
      "summary": "Part (a) only. POST /api/v1/storage/upload/chunked now parses its body with InitiateChunkedUploadRequestSchema after the presence check and the scope gate, before the sys_file row, the backend multipart and the session row; a failing body answers 400 INVALID_REQUEST naming each field (presigned door's envelope from PR #27537, no new code). chunkSize below 5242880 is refused, not clamped; an omitted chunkSize keeps the schema default 5242880, so a valid body's totalChunks is unchanged. The whole-body parse also narrows bucket (string) and metadata (object of strings), and null for those optional keys; all listed in the changeset (BREAKING banner, ADR-0087 not-required disposition) and the PR's Acceptance notes.",
      "tests": "storage-routes.test.ts: +7 REFUSED rows (totalSize -5, '100', 0, 1.5; chunkSize 1024, '5242880'; metadata {tag:1}) asserting status/success/code, field named, createFile and createSession not called; +3 CONTROL rows (omitted/min/above-min chunkSize; response and stored total_chunks/chunk_size/size). File 70/70. Package: pnpm --filter @objectstack/service-storage test Test Files 70/70, Tests 1297/1297, VERDICT command-exit 0. Ablation via scripts/ablation-replace.mjs after commit e3c83f13: safeParse line replaced by pass-through (anchor 1->0, blob 6b8c79453e4b->86146bd7a141) -> Tests 12 failed | 58 passed (7 REFUSED + omitted-default CONTROL + 4 terminal-status tests that omit chunkSize); restored blob == HEAD 6b8c79453e4b, git diff HEAD empty, 70/70. Subject imported from src, no dist leg.",
      "gates": "Head 26e6d217. dispatch-gates --commands derived 64 families; all 64 exit 0 (check:i18n and check:dual-build-cjs-loads exit 3 first, exit 0 after the named build closure). --ran with exit codes: 64 derived, 64 run, 0 NOT-MEASURED (derived zero). pnpm --filter @objectstack/service-storage typecheck exit 0; pnpm lint exit 0. CI check-runs: in_progress at report time (PR just opened).",
      "dogfood_battery": "26e6d217 · os-verify-lock VERDICT command-exit 0 (held 21m00s) · Test Files 355 passed | 3 skipped (358) · Tests 2867 passed | 20 skipped (2887) · no failures",
      "mcp_calls": "0",
      "api_writes": "2 — fleet-write relay: pr_create POST /repos/objectstack-ai/objectstack/pulls (#27781, draft, read back identical 8634 bytes); comment POST /repos/objectstack-ai/objectstack/issues/27549/comments (this report). git push not counted.",
      "open_questions": [],
      "out_of_scope_findings": [
        "carrier: none · noted, not filed: scope ?? 'user' in the chunked init handler is now redundant (the parse applies the default); left to keep the diff small (PR Acceptance notes)"
      ]
    }

    Generated by Claude Code

  8. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#4 · os-elon-musk · session_016WV4d1Np3hU7RQnA13YZcL

    ACCEPT → PR #27781 comment 6511125315 · head e77ad275 · 2026-10-29T23:40Z · landing sent (contract review PASS 6510724527 + delta 6510908948).

  9. objectstack-fleet commented on Oct 30, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#4 · os-elon-musk · session_016WV4d1Np3hU7RQnA13YZcL

    Landed · 2026-10-30T00:21Z

    reading value
    PR #27781 MERGED 2026-10-30T00:21:03Z, merge commit ca167f6d (merge queue)
    this card closed by Fixes #27549 (part (a); part (b) lives on its own domain:spec card)

    State: pm:dispatched removed in this stroke. Filed from the contract review: #27801 (the empty filename / mimeType refusal is stricter than the schema).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:filesFiles — upload, download, signed URLs, access derived from the parent recordbugSomething isn't workingdomain:serverDomain lane — seat card indexed by label:pm:seatpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions