Skip to content

fix(build): exclude Vite build manifests from Cloudflare asset uploads#1850

Merged
james-elicx merged 1 commit into
mainfrom
fix/cloudflare-vite-manifest-exposure
Jun 8, 2026
Merged

fix(build): exclude Vite build manifests from Cloudflare asset uploads#1850
james-elicx merged 1 commit into
mainfrom
fix/cloudflare-vite-manifest-exposure

Conversation

@james-elicx

Copy link
Copy Markdown
Member

Summary

On Cloudflare Workers, the ASSETS binding serves any uploaded file matching the request path before the Worker runs. Cloudflare builds enable build.manifest (and ssrManifest for Pages Router) so the worker entry can compute lazy chunks — this writes Vite's manifests to dist/client/.vite/. Nothing excluded that directory from the uploaded asset bundle, so on a deployed Worker:

curl https://<app>.workers.dev/.vite/manifest.json      # was: 200, returns the build manifest
curl https://<app>.workers.dev/.vite/ssr-manifest.json  # Pages Router

returned the Vite build/SSR manifests to anyone. These leak the full source-file → output-chunk mapping, including the paths of routes that are never linked from the UI (e.g. app/admin/internal-tools/page.tsx) — unauthenticated reconnaissance.

This is an information disclosure the framework already intends to prevent: the Node production server explicitly blocks /.vite/ (server/static-file-cache.ts, with tests). The block simply never existed on the Cloudflare deployment target, because Cloudflare serves assets ahead of the Worker (the deploy config uses not_found_handling: "none" and no run_worker_first).

Fix

Write a .assetsignore into dist/client during the vinext:cloudflare-build closeBundle hook (right after the existing _headers generation, after the manifests have been read for lazy-chunk computation). wrangler reads .assetsignore from the assets directory and matches it with .gitignore semantics via the ignore package, so the bare .vite entry excludes the directory and everything beneath it from upload — and .assetsignore itself is never served.

  • New typed helper packages/vinext/src/build/assets-ignore.ts (ensureAssetsIgnore) owns the write.
  • The merge logic preserves any user-authored .assetsignore and only appends missing patterns, so it's idempotent across rebuilds and never clobbers user config.
  • Files stay on disk (the Node prod server, which reads dist/client/.vite/manifest.json, is unaffected) — they're only excluded from the Cloudflare asset upload.

Why .assetsignore (not delete / run_worker_first)

  • Lowest risk: doesn't delete build outputs other tooling may read; doesn't change request routing.
  • Covers both deploy paths: the @cloudflare/vite-plugin build and vinext deploywrangler deploy both honor .assetsignore.

Scoped to .vite (the confirmed issue). Source maps aren't emitted to dist/client by default; if we later want to exclude *.map when build.sourcemap is opted into, the helper already accepts a custom pattern list.

Testing

  • tests/assets-ignore.test.ts — unit tests for the helper: default patterns, dir creation, idempotency (no duplicate entries), preserving user content, and honoring custom patterns.
  • tests/cache-adapters-build.test.ts — extends the existing real Cloudflare build to assert dist/client/.assetsignore contains .vite while dist/client/.vite/manifest.json still exists on disk (proves the exclusion is both wired and meaningful, at zero extra CI cost).
✓ tests/assets-ignore.test.ts (7 tests)
✓ tests/cache-adapters-build.test.ts (1 test)
✓ vp check (format + lint + types) on all changed files

Verification suggestion

After this merges, a deployed-Worker smoke check (curl -sw '%{http_code}' https://<preview>/.vite/manifest.json404) would convert this from code-confirmed to live-confirmed; happy to add it to scripts/smoke-test.sh if desired.

🤖 Generated with opencode

On Cloudflare Workers the ASSETS binding serves any uploaded file whose
path matches the request BEFORE the Worker runs. Cloudflare builds enable
`build.manifest` (and `ssrManifest` for Pages Router) so the worker entry
can compute lazy chunks, which writes Vite's manifests to
`dist/client/.vite/`. Because nothing excluded that directory from the
asset bundle, `/.vite/manifest.json` and `/.vite/ssr-manifest.json` were
publicly fetchable, leaking the full source-file -> chunk mapping
(including the paths of routes that are never linked from the UI).

The Node production server already blocks `/.vite/` explicitly
(server/static-file-cache.ts); this adds the Cloudflare-side equivalent by
writing a `.assetsignore` into `dist/client` during the cloudflare-build
closeBundle hook (right after the existing `_headers` generation, after the
manifests have been read). wrangler matches `.assetsignore` with gitignore
semantics, so the bare `.vite` entry excludes the directory and its
contents from upload, and the file is never served.

The merge logic preserves any user-authored `.assetsignore` and only
appends missing patterns, so it is idempotent across rebuilds. The writer
lives in a small typed helper (build/assets-ignore.ts) with focused unit
tests; a real Cloudflare build test asserts the wiring emits `.assetsignore`
with `.vite` while the manifest still exists on disk.
@pkg-pr-new

pkg-pr-new Bot commented Jun 8, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@vinext/cloudflare@1850
npm i https://pkg.pr.new/vinext@1850

commit: 6e1c678

@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor
Example Preview Production Original
app-router-cloudflare preview production
pages-router-cloudflare preview production
app-router-playground preview production original
realworld-api-rest preview production
nextra-docs-template preview production
benchmarks preview production
hackernews preview production original
workers-cache preview production
web preview production

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@ask-bonk ask-bonk Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: fix(build): exclude Vite build manifests from Cloudflare asset uploads

Verdict: Approve. The fix is correct, well-scoped, and the premise checks out against authoritative Cloudflare docs. No blocking issues found.

Verified

  • Root cause confirmed. Cloudflare's Worker script routing docs state plainly: "Cloudflare will first attempt to serve static assets if one matches the incoming request" before invoking the Worker. With run_worker_first unset (default false), an uploaded dist/client/.vite/manifest.json is served directly — the Worker's /.vite/ block never runs. The premise holds.
  • not_found_handling: "none" is not a red herring. I checked build/run-prerender.ts:197 which says "every request hits the worker first." That comment is about non-matching paths (page requests), and is consistent with this PR — not_found_handling only governs misses, not matching assets. No contradiction.
  • Parity claim is accurate. The Node prod server blocks /.vite/ at server/static-file-cache.ts:248; this .assetsignore is the correct Cloudflare-side equivalent.
  • .assetsignore semantics are correct. Per Cloudflare binding docs, the file uses .gitignore format, so a bare .vite line excludes the directory recursively. .assetsignore itself is never uploaded.
  • All deploy paths covered. No run_worker_first is set in deploy.ts (generateWranglerConfig) or any examples/*/wrangler.jsonc, so every Cloudflare target was vulnerable and is now fixed via the shared vinext:cloudflare-build closeBundle hook.
  • Hook placement is correct. ensureAssetsIgnore(clientDir) runs after the manifests are read for lazy-chunk computation (index.ts:4683-4706) and after _headers generation — so it never races the reads.

Implementation notes (non-blocking)

  • The merge logic correctly preserves user content and is idempotent. lines.join("\n") produces a single trailing newline; whitespace-only existing files fall through to the header branch with the full pattern set appended — both fine.
  • Unlike the _headers write (guarded by !fs.existsSync), ensureAssetsIgnore always runs and merges. This is the better behavior for a security guard (it self-heals if the file exists without .vite) and is intentional per the PR description.

Tests

  • tests/assets-ignore.test.ts (7) — pass. Good coverage: defaults, dir creation, idempotency, user-content preservation, no-duplicate, custom patterns.
  • tests/cache-adapters-build.test.ts (1) — pass. Asserts .assetsignore contains .vite and .vite/manifest.json still exists on disk, so the exclusion is both wired and meaningful. Reusing the existing real CF build for zero extra CI cost is the right call.
  • vp check on all 4 changed files: format, lint, types all clean.

Suggestions (optional, not blocking)

  • The PR's own "Verification suggestion" is worth taking: add a post-deploy smoke check (curl -sw '%{http_code}' https://<preview>/.vite/manifest.json404) to scripts/smoke-test.sh to convert this from code-confirmed to live-confirmed.
  • Consider (separate PR) whether _headers/_routes.json should also be in .assetsignore — though wrangler treats _headers as config rather than a servable asset, so likely a non-issue. Out of scope here.

LGTM.

@ask-bonk

ask-bonk Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Review posted on PR #1850.

Summary

I reviewed PR #1850 (fix(build): exclude Vite build manifests from Cloudflare asset uploads) and approve it — no blocking issues.

What I verified:

  • Premise is authoritative. Cloudflare's docs confirm matching assets are served before the Worker runs when run_worker_first is unset (the default). So /.vite/manifest.json was indeed publicly fetchable. The .assetsignore fix is the right mechanism.
  • No contradiction with not_found_handling: "none". That setting only governs non-matching paths; the run-prerender.ts comment about "worker first" is consistent and unrelated.
  • Parity confirmed with the Node prod server's /.vite/ block at server/static-file-cache.ts:248.
  • All deploy paths covered — no example or deploy.ts config sets run_worker_first, and the fix lives in the shared vinext:cloudflare-build hook with correct placement (after manifest reads).
  • Implementation is sound — idempotent merge, preserves user content, self-healing, correct trailing newline.
  • Tests pass (assets-ignore.test.ts 7/7, cache-adapters-build.test.ts 1/1) and vp check is clean on all 4 files. The integration test is meaningful (asserts both exclusion and on-disk presence).

I noted two optional, non-blocking suggestions: adopt the PR's own live smoke-test idea, and a separate-PR question about _headers/_routes.json (likely a non-issue since wrangler treats them as config).

github run

@james-elicx james-elicx merged commit 0c7ff57 into main Jun 8, 2026
51 checks passed
@james-elicx james-elicx deleted the fix/cloudflare-vite-manifest-exposure branch June 8, 2026 18:48
@github-actions github-actions Bot mentioned this pull request Jun 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant