fix(bin): allow Safari bridge logins without Origin - #86
Merged
Conversation
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>
This was referenced Aug 20, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
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
Originbridge POSTs only whenSec-Fetch-Siteor an HTTPSRefererproves same-origin, while continuing to reject mismatched origins.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.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_drainedis 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 thepasscode=...POSTgarbage. 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.shStarted an isolated bridge server and sent a Safari-shapedPOST /loginwithoutOriginbut withSec-Fetch-Site: same-origin; verified303 See Otherand session-cookie issuance.Sent no-Origin/no-proof and present-wrong-Origin login requests; verified both returned403 Forbidden.Used one raw HTTP/1.1 keep-alive socket for a rejected login body followed byGET /; verified responses were403then200, with no400/501garbage request.git status --shortafter testing to confirm no working-tree artifacts remained.✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.