Repository navigation
fix(security): give Aetherscope an explicit auth boundary - #15
Merged
Merged
Conversation
This was referenced Oct 18, 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.
When
AETHERSCOPE_TOKENis set (now required for the Docker/compose setup, which binds0.0.0.0inside the container), the browser shows a one-time token gate page instead of the app; entering the token sets an HttpOnly cookie and loads the app as before. Loopback with no token (the normal local run) is completely unchanged — no gate, no login.Inventory (security question #2)
HTTP:
GET /(shell),GET /captures/<path>(IQ download),GET /api/captures(list) — all unauthenticated, trusting the bind.Socket handlers (29, characterized in
tests/test_auth.py):connectplusstart_sweep/decode/capture/adsb/radio/scan_radio/replay/scan,play_capture,decode_capture,cancel_capture,stop,snap_radio,refresh_device,list_captures,delete_capture,update_capture,list_bookmarks,add/update/delete/bump_bookmark,set_setting,redact_capture,start/stop_audio_record,list_inventory,clear_inventory— every one trusted the connection.Bind:
AETHERSCOPE_HOSTdefault127.0.0.1(app.py), Dockerfile0.0.0.0relying on compose127.0.0.1:8765:8765,allow_unsafe_werkzeug=Trueat the old app.py:1226. Frontend: same-originio(), no token existed.Design (pocock-codebase-design vocabulary)
backend/auth.py(named indocs/architecture.md): oneAuthPolicy(host, token)— a deep module with a small interface (requires_auth,ensure_safe_startup(),check_token(), cookie make/verify, oneis_authenticated(headers, cookies, auth)verdict). No new deps: stdlibhmac/hashlib, constant-time compare.before_requestfor HTTP (capture routes included) and the Socket.IOconnecthandler that every socket handler sits behind (not per-handler copies).allow_unsafe_werkzeug=Truestays (Flask-SocketIO threading serves via Werkzeug); the startup guard is what makes reaching that line safe — non-loopback cannot get there without a token.POST /auth/token→ HttpOnly,SameSite=Strict, signed cookie (<issued>.<hmac>, key = token; raw token never in the cookie, rotating the token invalidates cookies). Socket auth reads that cookie, anauth={"token": ...}connect payload, orAuthorization: Bearer/X-Aetherscope-Tokenheaders (API clients). Frontend:io({withCredentials: true}); on manager error it probes/api/capturesand bounces to/(the gate) only on 401.127.0.0.1:8765:8765unchanged).Scope: #2 only. Geotag defaults and capture-path exposure (#4/#5) untouched, except that HTTP auth now covers the capture routes as designed.
Tests + mutant
tests/test_auth.py(12, failing-first): startup refusal for non-loopback/no-token; handler-set characterization (a new socket handler not added to the list fails); real Flask + Socket.IO test-client cycles — 401 without/with-wrong token, 200 with Bearer/cookie, gate→cookie→shell+captures, capture download bytes, socket connect rejected/accepted, cookie-shared socket connect; loopback parity.check_token'scompare_digest→ 6/12 red; restored → 12/12.tests/test_*.pygreen,check_html_templates0,check_architecture_namesgreen,node --checkall frontend JS green.