Skip to content

fix(bin): allow Safari bridge logins without Origin - #86

Merged
Amplify-Logic merged 4 commits into
mainfrom
fm/fm-bridge-safari-login-fix-s3
Aug 19, 2026
Merged

fix(bin): allow Safari bridge logins without Origin#86
Amplify-Logic merged 4 commits into
mainfrom
fm/fm-bridge-safari-login-fix-s3

Conversation

@Amplify-Logic

Copy link
Copy Markdown
Owner

Intent

Bridge login must work from iPhone Safari; it never has.

The captain has never successfully logged into the live bridge (bin/fm-bridge-view.py, served at https://larss-macbook-pro-2.taile26864.ts.net/) from his iPhone. Logs show GET / returns 200, then POST /login returns 403, followed by a garbage request line containing the passcode body (400 Bad request syntax / 501 Unsupported method ('passcode=...POST')).

Required fix, and only this defect — no other bridge changes:

  1. CSRF / Origin: when Origin is present it must match the expected origin exactly (keep current behavior). When Origin is absent, accept the POST only if same-origin is otherwise proven: Sec-Fetch-Site is same-origin or none, OR the Referer parses to the expected host over https. Reject when neither proves it. Document the reasoning inline briefly and in docs/bridge-view.md.
  2. Keep-alive garbage: early-return paths (403/404/413) were responding without reading the request body, so unread bytes parsed as the next request line. Drain (read and discard up to the declared Content-Length, bounded) before responding on every early return, or send Connection: close on error responses.
  3. Tests: a Safari-shaped login POST with NO Origin but Sec-Fetch-Site: same-origin succeeds with the right passcode; absent Origin with no same-origin proof is rejected; present-but-wrong Origin is still rejected; the drain fix (no request-line garbage after a 403 on a keep-alive connection); existing tests stay green.

After CI is green, open/keep the PR. Live redeploy (launchctl kickstart of com.firstmate.bridge-view) and a real no-Origin curl login against the live HTTPS URL are post-merge steps, not part of this validation run.

What Changed

  • Accept no-Origin bridge POSTs only when Sec-Fetch-Site or an HTTPS Referer proves same-origin, while continuing to reject mismatched origins.
  • Drain bounded request bodies on early error responses, closing slow or incomplete connections to keep leftover POST data from corrupting keep-alive requests.
  • Document the CSRF behavior and add regression coverage for Safari login, rejected origins, and keep-alive body handling.

Risk Assessment

✅ Low: The change is narrowly scoped, satisfies the Safari CSRF and keep-alive requirements, and the prior per-request state and slow-drain risks are now addressed with matching regression coverage.

Testing

The focused bridge suite passed, and an isolated end-to-end HTTP run demonstrated successful iPhone-Safari-shaped login, strict rejection of unproven or wrong-origin requests, and clean keep-alive reuse after a 403; the reviewer-visible transcript was saved and the worktree remained clean.

Evidence: Safari login and keep-alive transcript

Safari-shaped login returned 303 and issued a session cookie. Unproven and wrong-Origin requests returned 403. A follow-up GET on the rejected POST’s keep-alive connection returned 200 with no request-line garbage.

Safari-shaped POST /login (Origin absent; Sec-Fetch-Site: same-origin)
  response: HTTP/1.1 303 See Other
  session cookie issued: yes
POST /login (Origin absent; no same-origin proof)
  response: HTTP/1.1 403 Forbidden
POST /login (wrong Origin present; same-origin fetch metadata also present)
  response: HTTP/1.1 403 Forbidden
Same HTTP/1.1 connection after rejected POST with unread form body
  first response: HTTP/1.1 403 Forbidden
  follow-up GET response: HTTP/1.1 200 OK
  request-line garbage observed: no

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 2 issues found → auto-fixed (2) ✅
  • 🚨 bin/fm-bridge-view.py:1230 - Required criterion 2 is not met on reused connections. _body_drained is stored on the handler instance, which serves multiple HTTP/1.1 requests, and is never reset per request. A preceding GET sets it true, so a subsequent rejected POST returns without draining and can reproduce the passcode=...POST garbage. Reset this state at the start of every request or track consumption per request. The keep-alive test should issue GET before POST to match Safari’s flow.
  • ⚠️ bin/fm-bridge-view.py:1241 - An unauthenticated error path now blocks indefinitely while reading up to 15 MB from a client that may declare a body and send it slowly or incompletely. Because the threaded server has no request socket timeout, tailnet clients can accumulate stuck threads. Close the connection on these errors, or enforce a short read timeout while draining.

🔧 Fix: Reset request drain state and bound incomplete-body reads
1 warning still open:

  • ⚠️ bin/fm-bridge-view.py:1246 - The 1-second socket timeout is per blocking read, not a total drain deadline. A client can keep an unauthenticated error-path thread alive indefinitely by sending a small amount less than once per second until the declared body is exhausted. Use a monotonic total deadline with the timeout reduced to the remaining budget, or close error connections without draining.

🔧 Fix: Enforce total deadline for rejected body drains
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • bash tests/fm-bridge-view.test.sh
  • Started an isolated bridge server and sent a Safari-shaped POST /login without Origin but with Sec-Fetch-Site: same-origin; verified 303 See Other and session-cookie issuance.
  • Sent no-Origin/no-proof and present-wrong-Origin login requests; verified both returned 403 Forbidden.
  • Used one raw HTTP/1.1 keep-alive socket for a rejected login body followed by GET /; verified responses were 403 then 200, with no 400/501 garbage request.
  • git status --short after testing to confirm no working-tree artifacts remained.
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

Amplify-Logic and others added 4 commits August 19, 2026 14:24
iPhone Safari does not send Origin on the same-origin login form, so the
strict header check returned 403 and left the unread body on the keep-alive
socket. Accept an absent Origin only with Sec-Fetch-Site or https Referer
proof, and drain bounded unread bodies before error responses.

Co-authored-by: Cursor <cursoragent@cursor.com>
@Amplify-Logic
Amplify-Logic merged commit 6c2b65a into main Aug 19, 2026
13 checks passed
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