fix: limit and sanitize public status response #31
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/w12-status-response"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Fixes W12 from bugs.md.
/status.jsonmoved from the unlimited health scope onto the shared:public_apilimiter (100/min per IP):query_cacheETS for 5 seconds, bounding the cluster-wideLeaderElectioncalllogin_idandfilterdropped from the response — the APRS-IS login and server-side filter are operational details, not public statusVerification: focused controller tests passed (104), full suite passed (2237),
mix credo --strict, and commit hooks including Dialyzer passed.🤖 Skippy PR review
2 findings — 2 blocking before merge.
lib/aprsme_web/controllers/page_controller.ex:147lib/aprsme_web/router.ex:103Reviewed
ce50b0726491. Commentskippy reviewto re-run.@ -142,0 +144,4 @@# `/status.json` is public and hits the cluster-wide LeaderElection call,# so the payload is cached briefly (the page polls at most every few# seconds) and never includes the APRS-IS login id or server-side filter.🟡 Warning — Only the JSON path is sanitised — /status still renders login_id and filter
The JSON no longer carries them, but the same values are still served publicly:
lib/aprsme_web/live/status_live/index.ex:181rendersLogin ID: {@aprs_status.login_id}and:213rendersFilter: {@aprs_status.filter}, and/statusis a plainpipe_through :browser+mount_current_userroute with no auth check. Socurl -s https://aprs.me/statusstill hands out the APRS-IS login id and server-side filter that W12 wants gone — the leak is only closed on the JSON path. Either drop both fields from the LiveView (template and theget_aprs_status/0assigns at:444-445), or state that the page is intentionally allowed to show them.@ -98,0 +100,4 @@# Public status endpoint — cluster-wide status call behind the shared# API limiter; the payload itself is cached for a few seconds.scope "/", AprsmeWeb dopipe_through :public_api🟡 Warning — Limiter bucket is shared with :browser and :api, so /status.json traffic eats the v1 API allowance
RateLimiterkeys on"#{prefix}:#{key}"withprefixdefaulting to"rate_limit"(lib/aprsme_web/plugs/rate_limiter.ex:31,46) and Hammer's fix_window keeps one counter per{key, window}— so:browser(200/min),:api(100/min) and:public_api(100/min) all resolve to the same entry for an IP, each only checking its own limit against that shared count. Adding/status.jsonhere means status polling and/api/v1traffic from one address draw on one 100/min budget: a poller behind CGNAT/office NAT burns the allowance the v1 API is measured against, and every API call from that IP starts returning 429 even though it never touched the API (and 100 routed requests in the same minute already 429/status.json). Namespace the route —plug RateLimiter, scale: 60_000, limit: 100, prefix: "status_rate_limit"in a dedicated pipeline, or add a prefix to:public_api.Both earlier findings resolved in
735ad720— verified against the code as it stands, nothing new ince50b072..735ad720./statusLiveView no longer renders or assignslogin_id/filter(template blocks and thedefault_status/0keys are both gone), so the page and its LiveView payload are clean./status.jsonnow runs on its ownstatus_rate_limitbucket (router.ex:36), so status polling and/api/v1traffic from one IP no longer share a 100/min counter.Nothing left open from this review. 👍