Skip to content

console CSP: connect-src must include the storage endpoint the server presigns uploads to — since #25569 every browser upload to S3/R2 fails with 'Failed to fetch' (hosted production) #26200

Closed

Description

@hotlong

Filing gate: ① a measured product defect with reach: every browser file upload on hosted production. Filed on the maintainer's direct word in the cloud ops seat's chat, 2026-10-26: 「另在 objectstack 立卡让 CSP 自动带上存储端点」.
Reader: the domain:server / CLI seat that owns packages/cli/src/utils/console.ts (the Console CSP), dispatch step: one PR.
Dedupe:

维护者速读

#25569 给 Console 文档加了 nonce CSP,connect-src 只有 'self'、默认地图源,以及运维在 OS_CONSOLE_CSP_CONNECT_SRC 里写的来源。但 S3/R2 存储的上传是浏览器直接 PUT 到预签名 URL,指向存储端点的主机。这个主机不在 connect-src 里,浏览器就拒绝请求(TypeError: Failed to fetch),于是附件上传整条坏掉。

框架自己知道存储端点(OS_STORAGE_S3_ENDPOINT,以及适配器给出的预签名主机),CSP 应该自动把它加进 connect-src,不该要求每个部署再手配一个变量。

Measured (2026-10-26)

Production jack.objectos.ai (cloud v2.5.0 ef4c5d2d, framework pin c475b45b56):

  • POST /api/v1/storage/upload/presigned → 200, uploadUrl on https://<account>.r2.cloudflarestorage.com/objectos-attachments-prod/projects/<env>/user/<id>.txt, method PUT;
  • the browser's PUT to that URL → TypeError: Failed to fetch;
  • the document's CSP reads connect-src 'self' https://demotiles.maplibre.org;
  • a curl preflight to the same bucket with Origin: https://jack.objectos.ai answers 204 with Access-Control-Allow-Origin: https://jack.objectos.ai. So the bucket's CORS allows it, and the page's CSP is what refuses.

Staging os-4e4qa4.objectos.app (cloud 3689c6f6, same pin): the same failure.

The same round trip passed on 2026-10-25 at cloud b64d2cbf, whose framework pin predates #25569 (presigned 200 → PUT 200 → complete 200 → download 200, bytes back; objectstack-ai/cloud#2767 step (d)).

Hosted path: cloud's objectos-runtime mounts the Console through @objectstack/cli/console (mountConsoleStatic), which reads OS_CONSOLE_CSP_CONNECT_SRC once at boot (readExtraConnectSources).

Change

  1. When the deployment's storage serves uploads through presigned URLs, add its endpoint origin to the Console CSP connect-src at boot. Read it from the same resolved storage settings the storage service builds its adapter from (the OS_STORAGE_S3_ENDPOINT origin, or the adapter's own presign host). Don't keep a second copy of the endpoint rule.
  2. If the endpoint is virtual-hosted (bucket in the host name) or region-templated, add the exact origin the presigner emits.
  3. OS_CONSOLE_CSP_CONNECT_SRC stays for operators' extra sources.
  4. Tests:
    • with S3 storage configured, the served CSP's connect-src contains the endpoint origin;
    • with local storage, it adds nothing;
    • a parity test that the CSP origin equals the origin of a real presigned URL from the adapter.
    • Ablate the addition.

Acceptance

  • The tests above.
  • On a hosted environment with S3/R2 storage and no OS_CONSOLE_CSP_CONNECT_SRC: the browser upload round trip (presigned → PUT → complete → download) succeeds and the console logs no CSP violation.

Interim (hosted)

The cloud ops seat is setting OS_CONSOLE_CSP_CONNECT_SRC to the R2 origin on both cloud planes as a stopgap. Once this ships, it can be removed.

Clause-②: no (widens what the document lets the page reach, to a host the server itself hands the page). The seat confirms.
Metadata-protocol: none

Activity

  1. objectstack-fleet commented on Oct 26, 2026

    @objectstack-fleet
    Contributor

    Triage: first grade — bug · priority:p1 · domain:server · area:files · pm:queue · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-26T16:05Z

    ⛔ Not a claim, ⛔ not a dispatch.

    • p1, urgent: every browser upload to S3/R2 on hosted production fails since feat(cli): serve the Console document with a nonce-based content-security-policy #25569's CSP (connect-src lacks the presign endpoint). This was filed on the maintainer's own word (「另在 objectstack 立卡让 CSP 自动带上存储端点」).
    • Boundary for the dev: add only the configured storage endpoint's origin, derived from the storage settings the server already presigns against, and nothing wider (no wildcard). An unset or invalid endpoint adds nothing. Pin both. Until this ships, the operator's OS_CONSOLE_CSP_CONNECT_SRC is the stop-gap on hosted, which cloud can set now.

    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:queueReady for the PM dispatch loop
    priority:p1High: required for production / M2
    on Oct 26, 2026
  3. objectstack-fleet commented on Oct 26, 2026

    @objectstack-fleet
    Contributor

    Claim: PM loop round 8 · card thread
    Session: session_01R1q2xdtYgU6FKrZ7LgjPCC (the seat); the card thread is session_01JFi7QBsDBsBeLzDyTyerZh
    Account: os-project-manager (the seat's linked user; the card's assignee)
    Branch: claude/project-thread-vldk8b (the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)
    Worktree: the card thread's own container checkout
    Domain: domain:server
    Seat: domain:server#1
    Ruling-ref: the maintainer's 「另在 objectstack 立卡让 CSP 自动带上存储端点」, quoted on the card, and triage first grade (2026-10-26T16:05Z, p1). Add only the configured storage endpoint's origin, exactly as the presigner emits it, derived from the storage settings the adapter is built from. No second copy of the endpoint rule, no wildcard. An unset or invalid endpoint, or a local driver, adds nothing. OS_CONSOLE_CSP_CONNECT_SRC stays.
    File surface: packages/cli/src/utils/console.ts (Node built-ins only; the origin reaches it from its caller), the CLI serve/mount call site, the storage service or its adapter (a presign-origin accessor), and their tests. A cloud call-site half, if any, is reported for a cloud-lane card. ⛔ No packages/spec/** (stop on breach; explain in the report)
    Container & model: S, mode:thread, default model and effort (set at thread open)
    Clause-②: no — provisional (the CSP widens only to a host the server itself hands the page); the dev measures it on the diff
    Metadata-protocol: none
    Responsibility: every browser upload to S3/R2 on hosted production fails with Failed to fetch since #25569 (connect-src lacks the presign host)
    Thread-read: none
    Premise: holds (read 2026-10-26T17:28Z at origin/main 3d50a43bcb: card open, unassigned, pm:queue, no claim; console.ts builds connect-src from 'self', the map-style origin and OS_CONSOLE_CSP_CONNECT_SRC only)
    Serial constraints cleared: no open PR touches packages/cli/src/utils/console.ts; the dev re-measures before pr_create

  4. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Oct 26, 2026
  5. objectstack-fleet commented on Oct 26, 2026

    @objectstack-fleet
    Contributor

    os-dev-report

    {
      "issue": 26200,
      "status": "done",
      "branch": "claude/project-thread-vldk8b",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/26281",
      "head": "1f201e46b45eb9399c22d0aad7d5b11175ecd2f1",
      "session": "session_01JFi7QBsDBsBeLzDyTyerZh",
      "premise_still_valid": true,
      "summary": "S3StorageAdapter.getPresignedUploadOrigin() signs a probe PutObject for its own bucket with the same client options every real presign uses (one private clientOptions(), fixed fake credentials, no network, no credential chain) and answers that URL's origin, so virtual-hosted, region-templated, path-style and dotted-bucket hosts come out exactly as the presigner emits them; SwappableStorageService forwards it for the active adapter (undefined for local). The Console mount (console.ts, still Node built-ins only) reads it off the kernel's `storage` service by name PER DOCUMENT and adds it to connect-src after the error-reporting origin and before OS_CONSOLE_CSP_CONNECT_SRC; a Setup settings swap is followed on the next document with no restart. No storage service, local storage, an endpoint the SDK refuses, or an answer outside the module's connect-source grammar adds nothing (the last two warn once). Deviation from the order's mechanism hypothesis: no serve.ts / createConsoleStaticPlugin option was added; reading the kernel service needs no call-site change on the hosted caller, keeps the public console face unchanged, and covers runtime settings changes. Reading taken on 'endpoint unset': AWS S3 with no custom endpoint still adds its region-templated origin (the ruling names region-templated hosts, which exist only without an endpoint); 'unset' read as no S3 store configured.",
      "tests": "Red first (base 3d50a43bc + new cases): CSP suite 4 failed / 40 passed, each storage case read connect-src 'self' https://demotiles.maplibre.org only. Green: cli CSP suite 45/45; service-storage s3-storage-adapter.presign-origin.test.ts 10/10. Ablation at head 0fd3cd0d via scripts/ablation-replace.mjs (console.ts imported from source, no dist leg): deleting the storageUploadOrigin push landed (anchor 1->0, blob 497ea91966dc->1f54438adfbc), CSP suite 4 failed / 41 passed, restore blob == HEAD and git diff HEAD empty. Head 0fd3cd0d: full cli unit project 334 files / 4884 tests passed (integration tier declared to CI; no spawn entry touched). After merging origin/main (head 8990880046, main touched service-storage): service-storage test 67 files / 1236 tests passed; service-storage and cli typecheck exit 0; CSP suite + test/published-subpath-console.pin.test.ts 2 files / 59 tests passed. eslint --no-inline-config over the six touched files exit 0.",
      "gates": "dispatch-gates --commands derived 72 at 0fd3cd0d; every exit captured by redirect. Three first exited 3 (prerequisite: shallow clone for check-plugin-teardown-shape --self-test; unbuilt packages outside the diff for check:dual-build-cjs-loads and check:i18n-coverage); rerun after unshallow/build, all exit 0. --ran: 72 derived, 72 run, 0 NOT-MEASURED, 0 UNRUN, exit 0. At merged head 8990880046 (derivation = same list less the two check-position-name-fold-loaders rows main retired): check:type-check-coverage, check:type-check-debt, check:query-options-erasure, check:test-source-alias, check:cross-package-test-inputs, check:nul-bytes exit 0. Final head 1f201e46b4 (changeset wording only): check-changeset-no-major, check-adr-0087-registration, check-empty-changeset, check:nul-bytes exit 0. PR check-runs: in_progress at report time (not waited on).",
      "declarations": "Clause-②: yes (widening) — measured: two new public methods on exported classes of a published package (S3StorageAdapter#getPresignedUploadOrigin, SwappableStorageService#getPresignedUploadOrigin) plus one more accepted connect-src origin; the claim's provisional 'no' did not hold on the diff. Metadata-protocol: none. patch changeset for @objectstack/cli and @objectstack/service-storage. No packages/spec path.",
      "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm",
      "mcp_calls": "0 — no MCP GitHub calls; reads went through gh REST GETs",
      "api_writes": "2 relay strokes: pr_create (POST /repos/objectstack-ai/objectstack/pulls + POST /issues/26281/assignees, run 40380541994, read-back byte-identical) and this os-dev-report comment (POST /issues/26200/comments). git push is not counted.",
      "open_questions": [],
      "out_of_scope_findings": [
        "carrier: domain:server seat 1 · cloud-lane verification, no cloud code change known to be needed: the Console mount now reads the kernel's `storage` service at document time. If objectstack-ai/cloud objectos-runtime registers StorageServicePlugin on the same kernel it starts createConsoleStaticPlugin in, hosted uploads work once the framework pin passes PR 26281 and the OS_CONSOLE_CSP_CONNECT_SRC stop-gap can be removed. If it composes storage elsewhere (another kernel, or mountStorageRoutes over a service that kernel never sees), the CSP adds nothing on hosted and the call site must register the storage service on the Console's kernel. Not measurable from this repository. Acceptance step: on a hosted env with R2 and no OS_CONSOLE_CSP_CONNECT_SRC, the document's connect-src names the presigned upload origin and the presigned -> PUT -> complete -> download round trip passes. dedupe words: objectos-runtime storage service console CSP, connect-src R2 stop-gap removal",
        "noted, not filed (PR Acceptance notes): the accessor is not on the IStorageService contract (claim forbids packages/spec), so an out-of-repo adapter presigning to another host is not covered until it implements the method; LocalStorageAdapter's baseUrl option could make local presigned URLs cross-origin, but no producer in this repository sets it and the ruling says local adds nothing. carrier: none"
      ]
    }

    Generated by Claude Code

  6. objectstack-fleet commented on Oct 26, 2026

    @objectstack-fleet
    Contributor

    Landed

    PR #26281 merged at 2026-10-26T19:57Z by the merge queue as 69aa630b9f on main (Fixes #26200; ACCEPT 6424921039).

    Reading Probe Result
    Queue membership git ls-remote origin 'refs/heads/gh-readonly-queue/main/pr-26281-*' 0 refs
    origin/main by content git cat-file -e origin/main:packages/services/service-storage/src/s3-storage-adapter.ts present
    Ancestry git merge-base --is-ancestor 69aa630b9f origin/main holds

    State: this card closed by Fixes (completed); pm:dispatched removed.


    Generated by Claude Code

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

Metadata

Metadata

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:p1High: required for production / M2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions