--lan first load: redirect commits the key cookie; static goes public
The phone's first visit 403'd its own subresources: Safari's preload scanner fetches /static/* before the document response's Set-Cookie is committed, and favicon/apple-touch-icon probes are cookie-less system fetches. A ?k= visit now answers 303-with-cookie to the same path — the cookie is committed before any document loads, and the key is scrubbed from the phone's address bar and history. /static/* and the icon probe paths are exempt from the key: they're the app's own css/js/artwork, no user data (shelf photos stay gated). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
This commit is contained in:
co-authored by
Claude Fable 5
parent
95fd18ca5c
commit
344a930a8a
@@ -109,5 +109,6 @@
|
||||
"IMG_4556.jpeg|Top row, green textured spine with cartoon dinosaur, left of 'Ravensbu...' spine||Green speckled/scaly texture spine with small cartoon dinosaur illustration",
|
||||
"IMG_4556.jpeg|Top row, second shelf, spine reading 'Ravensbu...' between an unidentified dark box and green dinosaur-patterned spine|Ravensbu...|White spine with blue text, likely Ravensburger logo/publisher rather than title, top cut off",
|
||||
"IMG_4566.jpeg|Top shelf, background, partially obscured behind and above the two Alice Is Missing boxes|TOWN...FUKU (possibly Japanese text)|Colorful box with cartoon character illustrations, appears to be a small/medium sized game box, mostly obscured by foreground items",
|
||||
"IMG_4573.jpeg|Bottom right of cover art, near 'Blackboard' box||Small dark box with logo icon, title illegible due to size and angle"
|
||||
"IMG_4573.jpeg|Bottom right of cover art, near 'Blackboard' box||Small dark box with logo icon, title illegible due to size and angle",
|
||||
"IMG_4573.jpeg|Upper right area of the book cover art, stacked above 'ACTION CASTLE I' box|AC... (partially obscured by hand/fingers)|Red/orange box spine, appears to be part of a series with other 'Action Castle' related titles"
|
||||
]
|
||||
|
||||
@@ -330,11 +330,24 @@ def create_app(
|
||||
origin_host = urlsplit(origin).hostname if origin else None
|
||||
client_ip = request.client.host if request.client else ""
|
||||
loopback = client_ip.startswith("127.") or client_ip == "::1"
|
||||
# /static/* is the app's own css/js/artwork — no user data. Safari's
|
||||
# preload scanner fetches them before the document's Set-Cookie is
|
||||
# committed, and browsers probe favicons cookie-less; gating them
|
||||
# would break the first paint for nothing.
|
||||
public = request.url.path.startswith("/static/") or request.url.path in (
|
||||
"/favicon.ico",
|
||||
"/apple-touch-icon.png",
|
||||
"/apple-touch-icon-precomposed.png",
|
||||
)
|
||||
# Same-machine browsing stays keyless under --lan: a network peer
|
||||
# cannot arrive with a loopback CLIENT address, and these requests
|
||||
# fall through to the localhost guard below, which still blocks
|
||||
# rebinding and cross-origin pages by their foreign Host/Origin.
|
||||
if lan_token is not None and not (loopback and host in ALLOWED_HOSTS):
|
||||
if (
|
||||
lan_token is not None
|
||||
and not (loopback and host in ALLOWED_HOSTS)
|
||||
and not public
|
||||
):
|
||||
# --lan has no login, so EVERY request — reads included: shelf
|
||||
# photos and pipeline state are private — needs the per-run
|
||||
# key from the printed URL; a cookie carries it afterwards.
|
||||
@@ -355,20 +368,26 @@ def create_app(
|
||||
},
|
||||
status_code=403,
|
||||
)
|
||||
if request.method in ("GET", "HEAD") and request.query_params.get("k"):
|
||||
# the key arrived in the typed URL: commit it to a cookie
|
||||
# via redirect BEFORE the document loads (the preload
|
||||
# scanner races Set-Cookie on a streaming document), and
|
||||
# scrub the key from the address bar and history
|
||||
response = Response(
|
||||
status_code=303,
|
||||
headers={"Location": request.url.path or "/"},
|
||||
)
|
||||
response.set_cookie(
|
||||
LAN_COOKIE, lan_token, httponly=True, samesite="lax"
|
||||
)
|
||||
return response
|
||||
if request.method not in ("GET", "HEAD", "OPTIONS") and (
|
||||
origin_host is not None and origin_host != host
|
||||
):
|
||||
return JSONResponse(
|
||||
{"detail": "cross-origin request refused"}, status_code=403
|
||||
)
|
||||
response = await call_next(request)
|
||||
if request.query_params.get("k"):
|
||||
# the key came in the typed URL: hand it to the browser so
|
||||
# navigation and fetches keep working without it
|
||||
response.set_cookie(
|
||||
LAN_COOKIE, lan_token, httponly=True, samesite="lax"
|
||||
)
|
||||
return response
|
||||
return await call_next(request)
|
||||
if request.method not in ("GET", "HEAD", "OPTIONS") and (
|
||||
host not in ALLOWED_HOSTS
|
||||
or (origin_host is not None and origin_host not in ALLOWED_HOSTS)
|
||||
|
||||
@@ -919,9 +919,14 @@ def test_lan_token_gates_every_request(tmp_path):
|
||||
# reads are gated too: shelf photos and state are private
|
||||
assert phone.get("/api/state").status_code == 403
|
||||
assert phone.get("/api/state?k=wrong").status_code == 403
|
||||
first = phone.get("/titles?k=sekret")
|
||||
assert first.status_code == 200
|
||||
assert "bggpipe_key" in first.cookies # the URL key becomes a cookie
|
||||
# the app's own assets are public: the preload scanner fetches them
|
||||
# before the document's cookie commits
|
||||
assert phone.get("/static/app.css").status_code == 200
|
||||
first = phone.get("/titles?k=sekret", follow_redirects=False)
|
||||
assert first.status_code == 303 # cookie commits BEFORE the document
|
||||
assert first.headers["location"] == "/titles" # key scrubbed from URL
|
||||
assert "bggpipe_key" in first.cookies
|
||||
assert phone.get("/titles").status_code == 200 # cookie carries it now
|
||||
# cookie carries the session: mutations work from ANY host the phone
|
||||
# used (no allowlist dependence — DHCP/multi-interface safe), with a
|
||||
# same-origin Origin header and a port, like a real phone browser
|
||||
|
||||
Reference in New Issue
Block a user