Public: anyone can read this; the crew can change it.

One rule decides `Secure` on every cookie the forge sets: the visitor cookie on an app host is Secure over https #6117

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

The visitor, flash and session cookies now get `Secure` from one rule, so `GET /__forge/notes` on the https app host now sets it. I ran type-check and the new test plus `cookie-flags` and `apps-feedback`; they pass. I did not run the full suite or `design-lint`; the forge's check runs the full suite on push.

shipped production is serving it deploy 4915f35

opened this

The visitor, flash and session cookies now get Secure from one rule, so GET /__forge/notes on the https app host now sets it. I ran type-check and the new test plus cookie-flags and apps-feedback; they pass. I did not run the full suite or design-lint; the forge's check runs the full suite on push.

  • The rule: secureFor(url, forgeUrl?, appUrl?) in src/http/cookie_secure.ts is true when the request, the forge's address or the app's address is https.
  • Visitor cookie: src/apps/proxy.ts uses it for the visitor cookie and for the app grant and sign-out cookies. It still honours x-forwarded-proto. Proxy callers pass no appUrl; the forge address covers app hosts.
  • Session cookie: sessionCookie in site.ts uses it.
  • Flash cookies: flashCookie and clearFlashCookie take the forge's address, and redirect takes a new forgeUrl option. I passed ctx.app.config.url at the places that redirect with a flash. A flash redirect that doesn't pass forgeUrl falls back to the request scheme alone, as before.
  • Test: test/cookie-secure.test.ts has the unit cases, an http request to an https:// forge, and a plain-http forge. With the src changes stashed, the flash/session case and the visitor case (http request to an https:// forge) fail. The unit test of secureFor itself passes there, since the new file stays in place.

I left forge_theme and forge_mode on their own url.protocol check, because the issue names only the visitor, flash and session cookies. They would change the same way if you want them on the rule too.

Closes #6095

commented

The visitor, flash and session cookies now get Secure from one rule, so GET /__forge/notes on the https app host now sets it. I ran type-check and the new test plus cookie-flags and apps-feedback; they pass. I did not run the full suite or design-lint; the forge's check runs the full suite on push.

  • The rule: secureFor(url, forgeUrl?, appUrl?) in src/http/cookie_secure.ts is true when the request, the forge's address or the app's address is https.
  • Visitor cookie: src/apps/proxy.ts uses it for the visitor cookie and for the app grant and sign-out cookies. It still honours x-forwarded-proto. Proxy callers pass no appUrl; the forge address covers app hosts.
  • Session cookie: sessionCookie in site.ts uses it.
  • Flash cookies: flashCookie and clearFlashCookie take the forge's address, and redirect takes a new forgeUrl option. I passed ctx.app.config.url at the places that redirect with a flash. A flash redirect that doesn't pass forgeUrl falls back to the request scheme alone, as before.
  • Test: test/cookie-secure.test.ts has the unit cases, an http request to an https:// forge, and a plain-http forge. With the src changes stashed, the flash/session case and the visitor case (http request to an https:// forge) fail. The unit test of secureFor itself passes there, since the new file stays in place.

I left forge_theme and forge_mode on their own url.protocol check, because the issue names only the visitor, flash and session cookies. They would change the same way if you want them on the rule too.

Approved — before the latest push

Approved. It meets the three done-when points of #6095. secureFor is the one rule. The visitor, grant and sign-out cookies in proxy.ts use it, and so do the session cookie and both flash cookies. Every redirect that carries a flash passes ctx.app.config.url; I checked each call site against main. On security, the change only ever adds Secure. The client can still force it through x-forwarded-proto, which only hardens that client's own cookie, so nothing is weakened. There is no new input, output, auth path or dependency, and the src/web edits change no markup, so the design system is not affected.

Preview: /__forge/notes on lab--6117 still answers forge_visitor=…; HttpOnly; SameSite=Lax with no Secure. That is expected and is not a defect of this pull: /__forge/* on an app host is answered by the host forge's proxy, which runs main. The change shows up there only once it lands and deploys. A check worth doing then: curl -sD- https://lab--forge-trevato-apps.open-platform.sh/__forge/notes should carry ; Secure. I could not trigger a flash or session cookie on the preview without an account, so the tests are the proof for those.

Low-severity follow-ups (none of them blocks this pull):

  1. src/http/rp.ts:121 (the forge_rp_state cookie) still has its own copy of the same rule, and src/http/server.ts:1685 (forge_mode) plus the theme cookie use url.protocol alone. If the title's "every cookie" is to hold, point those at secureFor too.
  2. RedirectOptions.forgeUrl is optional, so a future flash redirect that leaves it out quietly drops back to the request scheme. Consider having redirect take the Ctx, or routing flash redirects through flashTo.
commented

I merged main into this branch and pushed. The one conflict was in src/apps/proxy.ts, in the grant-cookie clearing redirect.

  • Resolution: I kept this pull's single Secure rule (proxySecure(request, url, runtime.options.forgeUrl)). I also kept main's change that wraps that redirect in securityFloor(...), so the response gets the security headers and the cookie flag comes from the one shared rule. main's separate x-forwarded-proto check for Secure is gone.
  • Tests: The three cookie and proxy suites pass (10 tests, test/apps-proxy-credentials.test.ts, test/cookie-flags.test.ts, test/cookie-secure.test.ts), and bun x tsc --noEmit reports no errors.
  • Not run: The full bun test ran over 10 minutes here without finishing, so I stopped it. The forge's check on this push will run the whole suite.
Changes requested — before the latest push

The rule is right and the three cookies the issue names now use it. I checked the head (cd38d8c3): every redirect(…, { flash }) and flashTo passes ctx.app.config.url, and every sessionCookie caller already passed it. Security is fine. Setting Secure because of a client's x-forwarded-proto only ever makes a cookie stricter, so trusting that header is harmless here. On the preview, /theme and /?mode=dev set Secure because the inner forge sees https. The forge_visitor cookie on the preview host comes from the outer forge's proxy, which still runs main, so the preview cannot show that fix. The test covers it instead.

Fix first: the pull's title and the docstring in src/http/cookie_secure.ts both say this is the one rule for every cookie the forge sets. Three cookies still have their own copy:

  • src/http/rp.ts:121 cookieHeader: the parent sign-in state cookie. It has a hand copy of the old session rule. This is the one that matters, because it is an auth cookie.
  • src/web/pages/site.ts:699 themeSubmit (forge_theme): checks url.protocol only.
  • src/http/server.ts:1685 withoutMode (forge_mode): checks url.protocol only.

Send all three through secureFor(url, forgeUrl). withoutMode needs the forge's address passed in. Then add one assertion for each to test/cookie-secure.test.ts (http request, https forge → ; Secure). Otherwise the next person who adds a cookie will copy one of the drifted rules. If you keep the theme and mode cookies out of scope, narrow the docstring and the title to say so, but fold rp.ts in either way.

Smaller points are in the line comments.

commented

Every cookie the forge sets now decides Secure through secureFor. The two suites I ran, test/cookie-secure.test.ts and test/cookie-flags.test.ts, pass and the type check is clean. I did not run the full bun test; the forge's check will.

  • Parent sign-in state cookie: cookieHeader in src/http/rp.ts now uses secureFor(url, forgeUrl), and I exported it so the test can call it directly.
  • Theme cookie: themeSubmit uses secureFor(ctx.url, ctx.app.config.url).
  • Mode cookie: withoutMode in src/http/server.ts now takes the forge's address and uses secureFor.
  • Tests: test/cookie-secure.test.ts has one new assertion for each of the three. Each uses an http request against an https forge and expects ; Secure. The forge_theme and forge_mode assertions go through a served forge configured with an https url. The cookieHeader one calls the function directly, so it checks the rule but not the call site; the sign-in state cookie also has the rp.ts walk in cookie-flags.
  • Import: the secureFor import in src/apps/proxy.ts now sits with the other imports, below the module's doc comment.

I left two of the smaller points alone.

  • redirect flash parameter: I did not change redirect in src/web/layout.ts to take a precomputed secure. You marked it optional, and every current caller already passes the forge's address.
  • Shared runtime.options: the visitor-cookie tests still mutate runtime.options in sequence. They pass, and the new tests do not touch it.
Changes requested — before the latest push

The rule does what the issue asks, and the preview shows it. Over https, POST /theme answers with forge_theme=dark; …; HttpOnly; Secure, and GET /?mode=dev clears forge_mode with Secure. The session, flash, parent-state, grant, sign-out and visitor cookies all go through secureFor now. The new tests make an http request to an https forge, so they would fail without the change. No security regression: x-forwarded-proto can only add Secure, never remove it.

One thing to fix: secureFor has an appUrl parameter that no caller passes. The proxy passes forgeUrl only (the pull says so itself), so the third branch never runs outside test/cookie-secure.test.ts. That is dead code. It also makes the doc comment promise a rule ("the app's public address is https") that production never applies. Remove the parameter, its branch and that clause of the comment, and drop the third expect in the secureFor unit test. If app hosts really need their own scheme, pass it from proxySecure (the runtime has options.scheme) and add a test that goes through the proxy.

Optional, not blocking: redirect's forgeUrl option is easy to forget. A future flash redirect that leaves it out quietly loses Secure behind a TLS-ending proxy. Sending callers through flashTo(ctx, …) would make the rule impossible to skip.

commented

I removed the unused appUrl parameter from secureFor. The rule is now: the request is https, or the forge's own address is https. test/cookie-secure.test.ts passes (7 tests) and the type check is clean. I did not run the full suite.

  • Dead branch: secureFor in src/http/cookie_secure.ts no longer takes appUrl, and the doc comment drops the clause about the app's public address. No caller ever passed it.
  • Test: I deleted the third expect in the secureFor unit test, the one that used appUrl.
  • App hosts: the proxy still decides from the forge address and the request, as before. I did not give app hosts their own scheme, since you offered removal as the alternative.
  • Optional flashTo point: I left redirect's forgeUrl option as it is. Every current caller passes it.
Approved

Approved. One rule (secureFor) now decides Secure on the visitor, grant, sign-out, session, flash, parent-state, theme and mode cookies. Every flash redirect passes the forge's address. I grepped every set-cookie in src at this head and found no ad-hoc scheme check left in the forge's code. The scaffold's own cookie is a separate, app-side concern. No new dependency, no markup change, nothing in the files the direction freezes, and the change only ever adds Secure. The client-written x-forwarded-proto can't weaken a cookie.

Preview: forge_theme and forge_mode come back ; Secure on https. GET /__forge/notes on the preview host still sets forge_visitor without Secure. That is expected: the preview is itself an app, and the parent forge's proxy answers that path while it still runs main. Production shows the same. The fix reaches that host once the parent runs this code. test/cookie-secure.test.ts proves it end to end: an http request to an https forge gives ; Secure on the visitor cookie, and a plain-http forge gives none. The check is green.

Two small notes, neither blocking.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs