Repository navigation
Close the remaining security gaps in apps, the dev server and the platform - #384
Conversation
a97e773 to
79b01ac
Compare
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
79b01ac to
f91d96f
Compare
f91d96f to
4877a4d
Compare
4877a4d to
4d47300
Compare
4d47300 to
9c638ee
Compare
bbc3703 to
e79a32b
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e79a32bdfb
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
e79a32b to
1cb95de
Compare
1cb95de to
71f5d05
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 71f5d05a63
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
71f5d05 to
5fb9562
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5fb9562f3e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
5fb9562 to
ed42747
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ed4274767e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
ed42747 to
811338f
Compare
Deployed Cloudflare Workers and Node, Bun and Deno servers no longer run getPrerenderPaths() for outside requests to POST /__void/prerender-paths. The prerender runner marks its own request with a __VOID_PRERENDER binding (Miniflare) or Hono env argument (Node); unmarked requests are ordinary app requests and go through env validation. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A query such as ?constructor, ?valueOf or ?__proto__ no longer picks an Object.prototype member as the page action; it gets the usual "Unknown action" 404. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A rule such as '/en/*' -> '/:splat' no longer turns /en//evil.com into a protocol-relative Location header. When a path destination's capture starts with a slash or backslash, the result is collapsed to a single leading slash. Destinations written as '//host/...' or 'https://...' are unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…oject owner Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…r gateway headers Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ts log view Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Cloudflare accepts a CNAME to the SaaS zone as hostname ownership, so the per-host TXT does not stop a leftover or wildcard CNAME from activating a hostname. The comment and design note now describe that trade-off instead of claiming the TXT blocks it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Provider-native calls through the platform proxy dropped every caller cf-aig-* header, so apps lost AI Gateway caching, timeouts and retries. Pass those headers through and scope a caller cache key to the project, in the same format ai.run uses. Metadata, logging, cost, gateway auth, stored-key and unknown gateway headers stay dropped. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Dropping only the ESC byte left stray text in log output, such as "[K" after a progress line or the hidden URL of a terminal hyperlink, and colon-separated truecolor codes lost their color. Complete CSI and OSC sequences are now removed whole and SGR codes with colons are kept; incomplete sequences still lose only their control characters. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A live hook that throws now answers "live: internal error." so production clients never see internal details. In `void dev` that also hid the message from developers, unlike deferred props and the page error renderer. The worker entry compiled by the Vite dev server now turns on the real message; builds never do, so production keeps the fixed text. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Deployed Cloudflare apps now apply Better Auth's default rate limits per client IP. A server that calls auth for many users shares one IP, and tests that sign in often hit the sign-in limit. Show a secret-header rule that skips the limits only for a trusted caller, and say that a proxy using it must limit the requests it forwards. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
85df620 to
6294a67
Compare
## Problem `main` CI fails on `test (windows-latest, 2/2)` on every push since #384: ``` dev-trigger-secret-guard.test.ts > still serves other files under .void AssertionError: expected 403 to be 200 ``` The test is not flaky. It never passed on Windows. #384 added it, and PRs only run Windows CI with the `test: windows` label. ## Cause ``` tmpdir() on the Windows runner = C:\Users\RUNNER~1\AppData\Local\Temp (8.3 short name) │ ▼ realpathSync (JS) ── keeps RUNNER~1 ──► test root │ GET /.void/routes.d.ts │ ├─ Void guard: no secret path → next() └─ Vite static step → isFileLoadingAllowed() if (isWindows && looksLikeWindowsShortNamePath(path)) return false → 403 ``` Vite 8.3 refuses every file under a short-name path on Windows. This is Vite's own rule. The Void guard is correct: all the 404 cases in the file pass on Windows. ## What this PR changes ``` realpathSync(tmp) → realpathSync.native(tmp) C:\Users\RUNNER~1\… C:\Users\runneradmin\… ``` `realpathSync.native` expands the short name. `plugin-env-schema.ts` already uses it for the same reason, and its tests pass on Windows. ## Testing | Check | Result | | --- | --- | | `vp test run packages/void/test/integration/dev-trigger-secret-guard.test.ts` (macOS) | 15/15 pass | | Windows simulation with a `RUNNER~1` root | 403 before, 200 with the long root | | `windows-latest` CI on this PR (`test: windows` label) | the real check | 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Attack surface before this PR
The PRs below this one fixed the project token. This PR closes the smaller holes that are left. Each line is one path that someone outside the team could use.
1. Apps built with Void
2. The platform
3. The local dev server
Anyone who can reach it: a
--hostpeer, another local account, or a page on localhost.4. Docs examples that apps copy
What this PR changes
Apps
trustedOriginsrun signed out.cf-connecting-ip./or\gives one slash:Location: /evil.com.router.visit()..paramsuseForm()throw for.and..segments and a leading//.basicAuth()actionscount.Platform
administrator,hostmasterorwebmaster.cf-aig-*headers, and counts only rows with a tag signed by the platform. A caller cache key gets the project as a prefix.void logsand operator CLI output drop control characters.frame-ancestors 'none'.Dev server
Decisions
storage: 'database'shares one count.What people see
trustedOrigins.rateLimitinauth.ts. The auth guide shows what to do behind a proxy and for end-to-end tests.routing.revalidateQueryAllowlist, or cold requests render live.router.visit()URLs and./..route params now throw.basicAuth()with an empty password returns a server error.collectLog, and its logging, metadata, cost and authcf-aig-*headers, are ignored. Cache, timeout and retry headers still apply. Provider-native calls with the app's own key still do not count toward the platform allowance.ai.toMarkdown()image descriptions now count toward the AI allowance. Text conversions stay free.maxRetries: 0. The suspend preview (CLI) and the web Suspend confirmation say so.void platform worker rollback-allrolls the API back before the email gateway.Rollout
The email gateway now goes before the API, so the Confirm button works during the deploy. The self-hosted installer keeps its order, because the gateway's service binding needs the API to exist. During a self-hosted upgrade, a Confirm press in the few seconds between the two gets a 404 and works on retry.
One-time effects:
JWT_SECRETdoes this again.void platform user plan <user-id> <current-plan> --yes.Before merge:
ai.run()call shows the signed tag in AI Gateway logs.ai.toMarkdown(). Check that the image adds AI usage and the PDF adds none.Cron limits query
Known limits (on purpose)
Leftover CNAMEs. Cloudflare accepts a CNAME to the platform as proof of ownership. If a host still points at the platform after its owner removed it, the first project that adds it can serve it with a valid certificate. Vercel and Netlify have the same limit. Owners must delete the DNS record when they remove a domain.
Suspension pauses queues by retrying each batch about once an hour, and each retry uses one of the queue's retries. A batch that runs out of retries is dropped: after about 3 hours with the default of 3 retries, and at once with
maxRetries: 0. Restore a wrong suspension quickly. Keeping messages without using retries (Cloudflare's pause delivery) is a follow-up; it should also cover the platform-wide traffic gate on main, which drops messages the same way after about 15 minutes. Usage-limit suspension does not pause cron jobs or queues.Cron pause lets a fire through when the suspension marker cannot be read, so a KV fault does not stop every tenant's cron jobs.
Cron and queue runs are still not metered. A hanging cron handler can still hold a slot in the shared cron queue.
Tenant Workers can still make the proxy read up to 8 MiB before their project token is checked. The caller is known and can be suspended.
Better Auth plugins that add trusted origins are not seen by the cookie check.
Time zone. The log bound reads
projects.created_atas UTC, like other platform code. A self-hosted database with a non-UTC session shifts it.Testing
vp run checkvp lint,vp fmt --checkvp run build, thenvp test run(on maincf45d55db)🤖 Generated with Claude Code