All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[Unreleased]
[0.4.0] - 2026-07-14
Security
- Fix SSRF via egress filtering (GHSA-4325-m2h3-7rwf). Philter previously connected to any caller-supplied
:upstreamwith no egress validation, allowing requests to loopback, RFC1918, link-local and cloud metadata (169.254.169.254 / IMDS) addresses, and was vulnerable to DNS rebinding. Deny-by-default egress filtering (block_private_networks: true) now rejects upstreams that resolve to private, loopback, link-local, CGNAT or otherwise internal ranges (IPv40.0.0.0/8,10.0.0.0/8,100.64.0.0/10,127.0.0.0/8,169.254.0.0/16,172.16.0.0/12,192.168.0.0/16,240.0.0.0/4; IPv6::,::1,fc00::/7,fe80::/10, plus IPv4-mapped, IPv4-compatible and NAT64 forms unwrapped to their embedded IPv4 and re-checked). Blocking is on the resolved address tuples, not the URL literal, so octal/hex/decimal IP-encoding tricks are moot. A blocked address returns403with a static body; the resolved IP is logged server-side only, never returned to the client. - Require Mint
~> 1.9(was~> 1.7). Mint 1.9.1 is the lowest release clearing five 2026 advisories relevant to proxying: CRLF injection via an unvalidated HTTP method, HTTP response smuggling via lenient Content-Length parsing, unbounded buffering of chunked response bodies, HTTP/2 CONTINUATION flooding, and stream exhaustion via unenforced PUSH_PROMISE limits.
Changed
- Breaking: Replaced the Finch transport with a Mint-direct "resolve-and-pin" transport (
Philter.Transport). It resolves the upstream host once, validates every A/AAAA answer against the egress policy, and connects the socket to a validated IP tuple without ever re-resolving, while preserving the original hostname for the Host header, TLS SNI and certificate verification. This closes DNS rebinding. Host applications no longer need to supervise a Finch pool for Philter. - The transport is raw HTTP/1 with no connection pooling, so every request opens a fresh connection.
Added
Philter.Egressmodule — resolves a hostname and validates the resolved addresses against the SSRF egress policy. Transport-agnostic; policy is supplied per call.:block_private_networksoption (defaulttrue) — deny-by-default egress filtering forproxy/2andProxyPlug.:allowed_hostsoption (default[]) — opt-in escape hatch listing host strings that bypass the egress check, matched case-insensitively ignoring a trailing dot, for deliberately reaching internal hosts.:dns_timeoutoption (default5_000ms) — bounds upstream DNS resolution; on timeout the request is rejected with504. An unresolvable host returns502.
Deprecated
:finch_nameoption is deprecated and ignored — the transport no longer uses Finch, so host apps no longer need to supervise a Finch pool for Philter. Accepting it keeps existing callers from crashing.
[0.3.0] - 2026-04-09
Changed
- Breaking: All timing consolidated into
finished_result.timingmap —finished_result.duration_usreplaced bytiming.total_us, andbody_observation.duration_us/body_observation.time_to_first_byte_usremoved. Observations are now purely content metadata (hash, size, preview, body)
Added
collect_timing: trueoption forproxy/2enables per-phase timing capture (queue, connect, send, recv, idle_time, reused_connection) from HTTP client telemetryPhilter.Timingmodule for telemetry-based phase timing with lazy global handler attachment
[0.2.1] - 2026-03-11
Fixed
- Ensure handler module is loaded before checking optional callback exports, fixing silent callback skip on first use (
7b47409)
[0.2.0] - 2026-03-11
Added
:extra_headersoption forproxy/2andProxyPlug— merge additional headers into the filtered outbound request, replacing existing headers with matching names (044e8fc):strip_headersoption forproxy/2andProxyPlug— remove named headers (case-insensitive) before forwarding (044e8fc)- Mutual exclusion validation:
:headerscannot be combined with:extra_headersor:strip_headers(raisesArgumentError) (044e8fc) - Configurable logging via
:log_leveloption — lifecycle events logged at the configured level, or suppressed withfalse(daa8657)
Fixed
- Preserve explicit
hostheader in caller-supplied:headersinstead of always rewriting it (d2ac7b1) - Rewrite
hostheader to match upstream URL in the default (filtered) path (475da4c)
[0.1.0] - 2025-02-04
Added
- Initial release (extracted from Spike)
Philter.proxy/2for controller-based proxyingPhilter.ProxyPlugfor router-level forwardingPhilter.Handlerbehaviour for lifecycle callbackshandle_request_started/2- Called before sending to upstreamhandle_response_started/2- Called on first byte received (TTFB)handle_response_finished/2- Called with complete observations
- Body observation with O(1) memory:
- SHA256 hash computed incrementally
- Byte size tracking
- UTF-8 safe preview (first 64KB)
- Timing information (TTFB, duration)
- Conditional body accumulation based on content-type and size
- Configurable timeouts, payload limits, content-type filtering
- Full test coverage with Bypass for upstream mocking