Skip to content

fix(net): harden the loopback transport guard across all HTTP clients#804

Open
benw5483 wants to merge 2 commits into
mainfrom
oauth-exact-loopback-guard
Open

fix(net): harden the loopback transport guard across all HTTP clients#804
benw5483 wants to merge 2 commits into
mainfrom
oauth-exact-loopback-guard

Conversation

@benw5483

@benw5483 benw5483 commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Several HTTP clients in this crate allow clear-text http for a local dev / mock server by disabling reqwest's https_only, and they all decided "is this loopback?" with a raw string prefix check (starts_with("http://localhost") / 127.0.0.1 / [::1]). That prefix match is satisfied by attacker-controlled hosts:

  • http://localhost.evil.com / http://127.0.0.1.evil.com — lookalike hosts that merely start with the loopback literal;
  • http://127.0.0.1@evil.com — a userinfo prefix whose real connection host is evil.com.

Any of these made a client treat a non-loopback host as loopback, disabling https_only and letting credentials be sent in clear text when the caller controls the base URL (--api-url / ACTUAL_API_URL).

The same bypassable check was duplicated across ~6 constructors: the auth client (auth/oauth.rs), the platform API client (api/client.rs, which attaches the login-session bearer to every authenticated call), the model-cache probes (model_cache.rs), and the OpenAI / Anthropic runner clients.

This extracts one hardened helper — net::is_loopback_http_url — and routes every constructor through it. The helper parses the URL, requires the http scheme, rejects any userinfo component, and matches the host exactly against localhost / 127.0.0.1 / ::1. Genuine loopback dev endpoints keep working; every other URL is forced through HTTPS. No divergent copies of the check remain.

Test plan

  • New unit tests on the helper assert the bypass URLs are not loopback (the guard stays on) and that real loopback / https endpoints behave correctly; the auth client's integration tests cover its use of the helper.
  • cargo test green; cargo fmt --check and cargo clippy -- -D warnings clean.
  • A grep confirms no starts_with loopback guard remains — every client routes through the shared helper.
  • Reviewer: confirm the accepted loopback set (localhost / 127.0.0.1 / ::1) matches the intended dev endpoints for all of these clients.

Generated by the operator's software factory.
• City: factory-main · Agent: local-core.builder-2
• On behalf of: @benw5483

`build_http_client` decided whether to relax the HTTPS-only transport
guard with `str::starts_with("http://localhost")` / `127.0.0.1` / `[::1]`
on the raw URL. That prefix match is satisfied by attacker-controlled
hosts — `http://localhost.evil.com`, `http://127.0.0.1.evil.com`, and the
userinfo form `http://127.0.0.1@evil.com` (the connection's real host is
`evil.com`) — so a caller who controls the base URL could disable
`https_only` and send credentials to a non-loopback host in clear text.

Replace the prefix check with a parsed-URL match (`is_loopback_http_url`):
require the `http` scheme, reject any userinfo component, and match the
host exactly against `localhost` / `127.0.0.1` / `::1`. Genuine loopback
dev endpoints still work; every other URL is forced through HTTPS.

Add negative tests for the three bypass URLs (the guard now refuses them)
and positive tests for real loopback and https endpoints.

Generated by the operator's software factory.
City: factory-main · Agent: local-core.builder-2
On behalf of: @benw5483
Co-Authored-By: Actual Factory Bot <factory-bot@actual-software.invalid>
@benw5483 benw5483 changed the title fix(auth): match loopback hosts exactly in the HTTPS transport guard fix(net): harden the loopback transport guard across all HTTP clients Jul 6, 2026
@benw5483 benw5483 force-pushed the oauth-exact-loopback-guard branch from 1bbaa61 to 0acc438 Compare July 6, 2026 15:26
…uard

The bypassable loopback check fixed in the auth client was duplicated,
with the same `str::starts_with` shape, in every other HTTP-client
constructor in the crate: the platform API client (which attaches the
login-session bearer to every authenticated call), the model-cache
probes, and the OpenAI / Anthropic runner clients. Each could be tricked
by `http://localhost.evil.com` or `http://127.0.0.1@evil.com` into
disabling `https_only` for a non-loopback host.

Extract the hardened check into a single `crate::net::is_loopback_http_url`
(parse with `url::Url`, require the `http` scheme, reject userinfo, and
match the host exactly against `localhost` / `127.0.0.1` / `::1`) and
route every one of these constructors through it, so there is one guard
and no divergent copies. Behavior is preserved for genuine loopback dev
endpoints; every other URL is forced through HTTPS. The predicate's unit
tests move to the new module alongside it.

Generated by the operator's software factory.
City: factory-main · Agent: local-core.builder-2
On behalf of: @benw5483
Co-Authored-By: Actual Factory Bot <factory-bot@actual-software.invalid>
@benw5483 benw5483 force-pushed the oauth-exact-loopback-guard branch from 0acc438 to b26c7b8 Compare July 6, 2026 15:49
@benw5483 benw5483 marked this pull request as ready for review July 6, 2026 17:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant