PST-088: Behind an HTTP(S) proxy, pinned HTTPS requests verify the certificate against the pinned address, not the typed name #817

Closed
opened 2026-10-10 10:56:55 +00:00 by tiagoagueda · 1 comment
Owner

Found in the security audit of 2026-10-10 (SECURITY-AUDIT-2026-10-10.md), run against main at c30851b from 8 to 10 October 2026, as PST-088. Severity: Informational.

The text below is the audit's entry, unchanged apart from links to the other issues it names. The finding was produced by automated review and checked by a second automated pass; re-check it before acting on it.


  • Severity: Informational
  • Category: tls / proxy-interaction
  • Location: src/postulo/plugins/http.py (177-201,294)
  • Found by: SSRF and outbound traffic

Who can do it, and what they need. Not an attacker path as such; an operator who sets HTTPS_PROXY/HTTP_PROXY (honoured because httpx.Client is built with the default trust_env=True). …

How it works.

  1. Operator sets HTTPS_PROXY=http://proxy:3128.
  2. _pin rewrites the URL host to the approved IP and sets extensions["sni_hostname"]=<typed name>.
  3. httpx mounts an HTTPTransport(proxy=...); httpcore's TunnelHTTPConnection issues CONNECT <ip>:443 and then calls start_tls(server_hostname=self._remote_origin.host), i.e. the IP, ignoring the sni_hostname extension (httpcore 1.0.9 _sync/http_proxy.py line ~311; the direct path _sync/connection.py line ~150 does honour it).
  4. No SNI is sent for an IP literal and hostname verification runs against the IP, so nearly every public site fails with SSLCertVerificationError; _fall_back_between_addresses retries each approved address with the same result. …

Impact. Capture, logo fetch, link check, web push, update check and the plugin catalogue all fail for HTTPS destinations whenever an outbound proxy is configured in the environment, with an error that reads as a certificate problem. The failure is closed, not open, so no spoofing results; the risk is that the obvious 'fix' (ALLOW_PRIVATE=true) widens the policy for every account. …

Verification (verdict: confirmed; severity after verification: Informational). Line references are right (http.py 177-201 and 294; httpcore line is 312, not ~311). Two corrections: (1) the 'documented workaround' is not documented anywhere in the repo, so drop that claim from the impact; the real consequence is only that guarded HTTPS fetches (capture, logo, link check, web push, update check, catalogue, token refresh) fail with a certificate error whenever HTTP(S)_PROXY is in the environment. …

Suggested remediation. Either pass trust_env=False so the guarded clients ignore proxy variables (and document a Postulo-specific proxy setting that, when used, keeps the name on the URL and relies on the proxy to resolve, with the policy enforced by re-resolving and comparing), or wrap the proxy transport so the tunnel's TLS uses the sni_hostname extension, and add a test that pins through a proxy and asserts the server_hostname handed to start_tls is the typed …

Found in the security audit of 2026-10-10 (`SECURITY-AUDIT-2026-10-10.md`), run against `main` at `c30851b` from 8 to 10 October 2026, as **PST-088**. Severity: **Informational**. The text below is the audit's entry, unchanged apart from links to the other issues it names. The finding was produced by automated review and checked by a second automated pass; re-check it before acting on it. --- - **Severity:** Informational - **Category:** tls / proxy-interaction - **Location:** `src/postulo/plugins/http.py` (177-201,294) - **Found by:** SSRF and outbound traffic **Who can do it, and what they need.** Not an attacker path as such; an operator who sets HTTPS_PROXY/HTTP_PROXY (honoured because `httpx.Client` is built with the default `trust_env=True`). … **How it works.** 1. Operator sets `HTTPS_PROXY=http://proxy:3128`. 2. `_pin` rewrites the URL host to the approved IP and sets `extensions["sni_hostname"]=<typed name>`. 3. httpx mounts an `HTTPTransport(proxy=...)`; httpcore's `TunnelHTTPConnection` issues `CONNECT <ip>:443` and then calls `start_tls(server_hostname=self._remote_origin.host)`, i.e. the IP, ignoring the `sni_hostname` extension (httpcore 1.0.9 `_sync/http_proxy.py` line ~311; the direct path `_sync/connection.py` line ~150 does honour it). 4. No SNI is sent for an IP literal and hostname verification runs against the IP, so nearly every public site fails with `SSLCertVerificationError`; `_fall_back_between_addresses` retries each approved address with the same result. … **Impact.** Capture, logo fetch, link check, web push, update check and the plugin catalogue all fail for HTTPS destinations whenever an outbound proxy is configured in the environment, with an error that reads as a certificate problem. The failure is closed, not open, so no spoofing results; the risk is that the obvious 'fix' (ALLOW_PRIVATE=true) widens the policy for every account. … **Verification** (verdict: confirmed; severity after verification: Informational). Line references are right (http.py 177-201 and 294; httpcore line is 312, not ~311). Two corrections: (1) the 'documented workaround' is not documented anywhere in the repo, so drop that claim from the impact; the real consequence is only that guarded HTTPS fetches (capture, logo, link check, web push, update check, catalogue, token refresh) fail with a certificate error whenever HTTP(S)_PROXY is in the environment. … **Suggested remediation.** Either pass `trust_env=False` so the guarded clients ignore proxy variables (and document a Postulo-specific proxy setting that, when used, keeps the name on the URL and relies on the proxy to resolve, with the policy enforced by re-resolving and comparing), or wrap the proxy transport so the tunnel's TLS uses the `sni_hostname` extension, and add a test that pins through a proxy and asserts the server_hostname handed to start_tls is the typed …
tiagoagueda added this to the 1.0.0 milestone 2026-10-10 10:58:18 +00:00
Author
Owner

Fixed in c78572a81 on main.

Fixed in c78572a81 on main.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Postulo/postulo#817
No description provided.