Question
The website bakes cryptify's upload limits into its published bundle at build time. cryptify already serves those exact numbers — but only to a valid API key. May an unauthenticated caller learn the default tier's limits?
The duplication
apps/website/src/lib/env.ts (in encryption4all/postguard-js) reads two build-time env vars:
export const MAX_UPLOAD_SIZE = Number(requireEnv('VITE_MAX_UPLOAD_SIZE'))
export const ROLLING_LIMIT = Number(requireEnv('VITE_ROLLING_LIMIT'))
apps/website/src/lib/components/filesharing/inputs/FileInput.svelte uses them for user-visible validation before any upload exists — the dropzone's maxFilesize, effectiveLimit = Math.min(MAX_UPLOAD_SIZE, ROLLING_LIMIT - usedBytes), the "X GB remaining" text, and the overLimit state that blocks the send button.
cryptify already serves the same numbers from GET /usage: per_upload_limit_bytes, limit_bytes, window_days, used_bytes, resets_at. So the values are not unpublished — they are unreachable for the caller that needs them.
Why it is worse than the chunk-size seam it came from
#147 closed the upload_chunk seam by having cryptify serve its maximum on POST /fileupload/init. This is the same failure class one level up, and worse on three counts:
- These numbers drive user-visible UI. A stale value tells a user their file is too large when it is not, or accepts one the server will reject.
- They are baked into a published artifact at build time, so correcting them requires rebuilding and redeploying the website — not a config change. That is the shape of the dead-hostname prod bug already found in the Outlook add-in, where
fileshare.postguard.eu shipped in a released bundle and silently broke every file send.
- The two tiers differ by 20× (5 GB default, 100 GB with a validated API key), so a wrong choice of which tier's number to bake is not a rounding error.
It was deliberately not folded into #147, because the blocker is not a transport-config decision.
What to settle
-
Whether an unauthenticated caller may read the default tier's limits at all. GET /usage requires Authorization: Bearer PG-… and returns 401 otherwise — deliberately, and recently: GHSA-5rhx-xgvv-h78h was an unauthenticated GET /usage, fixed in cryptify-v0.1.28. The advisory was about a caller querying usage for an arbitrary address, which is a per-tenant secret. The limits are not obviously per-tenant secrets — the default tier's 5 GB is the same for everyone and is already displayed in the website's own UI. Decide whether that distinction is real, and if so, resolve it without re-opening what the advisory closed: the answer is probably a route that serves limits only, never used_bytes or resets_at, since those are the fields that leak.
-
Where it is served. Reusing /usage with a degraded unauthenticated response risks re-introducing exactly the shape the advisory fixed — an endpoint whose response contents depend on auth is one refactor away from leaking again. A separate limits route is more surface at a moment when #257 is trying to shrink it. Argue it rather than assuming.
-
Whether the website should fetch at all, or just stop baking. A third option: keep the values as deploy-time config but have the ops repo template them from the same source that configures cryptify, so the two cannot disagree. Cheaper, and it fits the finding that privacybydesign/postguard-ops is the source of truth for how these services actually run — but it leaves a rebuild in the loop, and the values still live in two places.
-
What the gate is. Whatever the answer, the regression to catch is the website displaying a limit the server does not enforce. Say what fails, and on which event.
Surfaced by the #147 session, 2026-08-11, while checking whether the chunk size was needed before init. It is not — chunking begins after init — but these are, which is what made them visible.
Part of #247.
Question
The website bakes cryptify's upload limits into its published bundle at build time. cryptify already serves those exact numbers — but only to a valid API key. May an unauthenticated caller learn the default tier's limits?
The duplication
apps/website/src/lib/env.ts(inencryption4all/postguard-js) reads two build-time env vars:apps/website/src/lib/components/filesharing/inputs/FileInput.svelteuses them for user-visible validation before any upload exists — the dropzone'smaxFilesize,effectiveLimit = Math.min(MAX_UPLOAD_SIZE, ROLLING_LIMIT - usedBytes), the "X GB remaining" text, and theoverLimitstate that blocks the send button.cryptify already serves the same numbers from
GET /usage:per_upload_limit_bytes,limit_bytes,window_days,used_bytes,resets_at. So the values are not unpublished — they are unreachable for the caller that needs them.Why it is worse than the chunk-size seam it came from
#147 closed the
upload_chunkseam by having cryptify serve its maximum onPOST /fileupload/init. This is the same failure class one level up, and worse on three counts:fileshare.postguard.eushipped in a released bundle and silently broke every file send.It was deliberately not folded into #147, because the blocker is not a transport-config decision.
What to settle
Whether an unauthenticated caller may read the default tier's limits at all.
GET /usagerequiresAuthorization: Bearer PG-…and returns 401 otherwise — deliberately, and recently: GHSA-5rhx-xgvv-h78h was an unauthenticatedGET /usage, fixed incryptify-v0.1.28. The advisory was about a caller querying usage for an arbitrary address, which is a per-tenant secret. The limits are not obviously per-tenant secrets — the default tier's 5 GB is the same for everyone and is already displayed in the website's own UI. Decide whether that distinction is real, and if so, resolve it without re-opening what the advisory closed: the answer is probably a route that serves limits only, neverused_bytesorresets_at, since those are the fields that leak.Where it is served. Reusing
/usagewith a degraded unauthenticated response risks re-introducing exactly the shape the advisory fixed — an endpoint whose response contents depend on auth is one refactor away from leaking again. A separate limits route is more surface at a moment when #257 is trying to shrink it. Argue it rather than assuming.Whether the website should fetch at all, or just stop baking. A third option: keep the values as deploy-time config but have the ops repo template them from the same source that configures cryptify, so the two cannot disagree. Cheaper, and it fits the finding that
privacybydesign/postguard-opsis the source of truth for how these services actually run — but it leaves a rebuild in the loop, and the values still live in two places.What the gate is. Whatever the answer, the regression to catch is the website displaying a limit the server does not enforce. Say what fails, and on which event.
Surfaced by the #147 session, 2026-08-11, while checking whether the chunk size was needed before init. It is not — chunking begins after init — but these are, which is what made them visible.
Part of #247.