Bodyless POST routes accept cross-origin simple requests carrying ambient credentials #336

Closed
opened 2026-10-23 05:26:52 -07:00 by rays · 1 comment
Owner

Found by the security-audit skill (run-1, 2026-10-20). Source-read only: nothing was run, so this is a lead that still needs its local check, not a confirmed finding. Two independent verifiers did not disprove it.

Six POST routes take no body extractor, so a cross-origin HTML form or no-cors fetch reaches them without a CORS preflight: /api/logout (web.rs:45), /api/feeds/{id}/read-all (57), /api/read-all (58, marks every entry of the caller's subscriptions read), /api/enclosures/{id}/download (60, queues a download of an enclosure in a feed the caller subscribes to), /api/directory/{id}/refresh (70, admin only) and /api/popular/{id} (74, subscribes the caller to a directory-listed feed and schedules a scan). Neither ipx (no Origin, Sec-Fetch-Site, CORS or anti-CSRF token handling anywhere in crates/ipx/src) nor Caddy (web/Caddyfile:82-87 proxies /api/* unchanged) checks the request's origin. The ipx_session and ipx_token cookies are SameSite=Lax (web.rs:196, 320), which blocks a cross-site form POST from an unrelated site but not one from a same-site sibling subdomain. On the Cloudflare Access path, identity comes from the trusted header and Cf-Access-Jwt-Assertion (web.rs:124, 238-256), which Cloudflare adds from its own CF_Authorization cookie; if that cookie is SameSite=None, any website can make these requests as the signed-in user. Effects are limited to integrity/nuisance actions on the victim's own state (read flags, an extra subscription to a directory-listed feed, a queued download, sign-out); no data is disclosed to the attacker, since responses are opaque cross-origin.

Claimed root cause. CSRF defence relies only on the ipx cookies' SameSite=Lax and on axum's Json extractor rejecting non-JSON content types (which forces a preflight that fails for lack of a CORS layer); routes with no Json body get only SameSite=Lax, and the proxy-vouched identity path (web.rs:117-152) does not use ipx's cookies at all, so its cross-site behaviour depends on Cloudflare Access's cookie policy, set outside the repository.

Local check. {'local': 'In a sandboxed scratch daemon on SQLite with a dummy user u1 holding a session and two subscribed fixture feeds with unread entries, send POST /api/read-all with only Cookie: ipx_session=, Origin: https://evil.example, Sec-Fetch-Site: cross-site, no body and no Content-Type. Expected with current code: 200 {"marked":N>0}; after a fix (reject unsafe methods whose Origin or Sec-Fetch-Site is not same-origin, or require a custom header) it should be 403. Repeat for POST /api/popular/{id} against a stub directory, and for the proxy path with trusted_proxies=["127.0.0.1"], trusted_header set and no access_team.', 'deployment': "Owner checks, read-only, the Cloudflare Zero Trust application ipodderx's cookie settings (SameSite attribute of CF_Authorization) and lists hosts under sdf1.net not under the owner's sole control. If SameSite is None, a cross-site form POST from any site is authenticated; if Lax/Strict and no sibling host is attacker-influenced, the issue reduces to defence in depth."}

Owner check (no probing). Owner checks, read-only, the Cloudflare Zero Trust application ipodderx's cookie settings (SameSite attribute of CF_Authorization) and lists hosts under sdf1.net not under the owner's sole control. If SameSite is None, a cross-site form POST from any site is authenticated; if Lax/Strict and no sibling host is attacker-influenced, the issue reduces to defence in depth.

Full detail: /config/security-audit-skill/ipx/run-1/NEEDS-VALIDATION.md (fingerprint ipx/web.rs:bodyless-post-no-csrf-check).

Found by the security-audit skill (run-1, 2026-10-20). Source-read only: nothing was run, so this is a lead that still needs its local check, not a confirmed finding. Two independent verifiers did not disprove it. Six POST routes take no body extractor, so a cross-origin HTML form or no-cors fetch reaches them without a CORS preflight: /api/logout (web.rs:45), /api/feeds/{id}/read-all (57), /api/read-all (58, marks every entry of the caller's subscriptions read), /api/enclosures/{id}/download (60, queues a download of an enclosure in a feed the caller subscribes to), /api/directory/{id}/refresh (70, admin only) and /api/popular/{id} (74, subscribes the caller to a directory-listed feed and schedules a scan). Neither ipx (no Origin, Sec-Fetch-Site, CORS or anti-CSRF token handling anywhere in crates/ipx/src) nor Caddy (web/Caddyfile:82-87 proxies /api/* unchanged) checks the request's origin. The ipx_session and ipx_token cookies are SameSite=Lax (web.rs:196, 320), which blocks a cross-site form POST from an unrelated site but not one from a same-site sibling subdomain. On the Cloudflare Access path, identity comes from the trusted header and Cf-Access-Jwt-Assertion (web.rs:124, 238-256), which Cloudflare adds from its own CF_Authorization cookie; if that cookie is SameSite=None, any website can make these requests as the signed-in user. Effects are limited to integrity/nuisance actions on the victim's own state (read flags, an extra subscription to a directory-listed feed, a queued download, sign-out); no data is disclosed to the attacker, since responses are opaque cross-origin. **Claimed root cause.** CSRF defence relies only on the ipx cookies' SameSite=Lax and on axum's Json extractor rejecting non-JSON content types (which forces a preflight that fails for lack of a CORS layer); routes with no Json body get only SameSite=Lax, and the proxy-vouched identity path (web.rs:117-152) does not use ipx's cookies at all, so its cross-site behaviour depends on Cloudflare Access's cookie policy, set outside the repository. **Local check.** {'local': 'In a sandboxed scratch daemon on SQLite with a dummy user u1 holding a session and two subscribed fixture feeds with unread entries, send POST /api/read-all with only Cookie: ipx_session=<sid>, Origin: https://evil.example, Sec-Fetch-Site: cross-site, no body and no Content-Type. Expected with current code: 200 {"marked":N>0}; after a fix (reject unsafe methods whose Origin or Sec-Fetch-Site is not same-origin, or require a custom header) it should be 403. Repeat for POST /api/popular/{id} against a stub directory, and for the proxy path with trusted_proxies=["127.0.0.1"], trusted_header set and no access_team.', 'deployment': "Owner checks, read-only, the Cloudflare Zero Trust application ipodderx's cookie settings (SameSite attribute of CF_Authorization) and lists hosts under sdf1.net not under the owner's sole control. If SameSite is None, a cross-site form POST from any site is authenticated; if Lax/Strict and no sibling host is attacker-influenced, the issue reduces to defence in depth."} **Owner check (no probing).** Owner checks, read-only, the Cloudflare Zero Trust application ipodderx's cookie settings (SameSite attribute of CF_Authorization) and lists hosts under sdf1.net not under the owner's sole control. If SameSite is None, a cross-site form POST from any site is authenticated; if Lax/Strict and no sibling host is attacker-influenced, the issue reduces to defence in depth. Full detail: `/config/security-audit-skill/ipx/run-1/NEEDS-VALIDATION.md` (fingerprint `ipx/web.rs:bodyless-post-no-csrf-check`).
rays added the bug label 2026-10-23 05:26:52 -07:00
Author
Owner

Fixed in d1149e1: a middleware on every route refuses anything but GET, HEAD and OPTIONS with 403 when Sec-Fetch-Site is not same-origin or none, or, without it, when Origin is not the Host. Requests with neither (scripts with a token) go through. Deployed; on production a token POST to /api/fetch got 202, a POST to /api/read-all with Sec-Fetch-Site: cross-site got 403, and the browser suite passes.

Fixed in d1149e1: a middleware on every route refuses anything but GET, HEAD and OPTIONS with 403 when Sec-Fetch-Site is not same-origin or none, or, without it, when Origin is not the Host. Requests with neither (scripts with a token) go through. Deployed; on production a token POST to /api/fetch got 202, a POST to /api/read-all with Sec-Fetch-Site: cross-site got 403, and the browser suite passes.
rays closed this issue 2026-10-23 07:37:07 -07:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: rays/ipx#336