Changelog — localproxy

Version history of localproxy — the local HTTP proxy for the JPKCom Developer Tools.

Version history of proxy-jpkcom-dev-tools (opens in a new tab). Entries are taken verbatim from the project's README. A hands-on guide with tips is available in the project guide.

v1.3.0

Feature release: parity with the PHP proxy of the JPKCom Tools (proxy.php 2.8.0) for SSL-certificate and redirect information, so the tools' Expert Mode shows the same details as their server-side mode. Same toolchain as v1.2.4 (Go 1.27.3); no new dependencies.

Release order — deploy the tools first. /proxy no longer forwards Location (see below). The updated JPKCom Tools frontend works with older proxies as before (unknown query parameters such as follow=1, diag=1 and get=1 are ignored, every new field is feature-detected), but following redirects needs v1.3.0: with an older proxy the browser still follows a 3xx from /proxy to the target itself, and a redirect to another site fails at the CORS preflight ("Failed to fetch"), exactly as with the previous frontend. Publish the frontend first, then this release; the frontend's MIN_VERSION moves to v1.3.0.

New

  • Full certificate report on /inspect and /page (ssl): every chain entry gets position, label (Server certificate, Intermediate CA, Root CA (self-signed), Cross-signed root (acts as intermediate)), whether the next certificate signed it, subject/issuer DN and organisation, validity as text and Unix time, serial, OpenSSL signature-algorithm name, key description, SANs, SHA-256/SHA-1 fingerprints and PEM. The report adds protocol and cipher in OpenSSL notation (TLSv1.3, ECDHE-RSA-AES128-GCM-SHA256), cipherSuite (IANA), bits, verified / valid, error / errorType, hostMatch (RFC 6125), selfSigned, issues (the PHP rule set and messages) and certSource. See ssl object (opens in a new tab).
  • diag=1 on /inspect and /page: a certificate checker needs details exactly when verification fails. With diag=1, /inspect reports an upstream failure inline (HTTP 200, error object), and a tls_error on either endpoint runs a handshake-only TLS diagnostic probe (opens in a new tab) that fills ssl with what the failing server presented — also for servers stuck on TLS 1.0/1.1, 3DES or RC4. Without diag, the 502/504 contract is unchanged.
  • type on error responses: a human-readable class with the PHP proxy's texts (Certificate expired, Could not resolve host, …), set on every upstream failure. See Error responses (opens in a new tab).
  • follow=1 on /proxy (GET and HEAD): the proxy follows up to 5 redirects itself, each target SSRF-checked, and reports X-Upstream-Final-URL and X-Upstream-Redirects. See Redirects on /proxy (opens in a new tab).
  • X-Upstream-Location on every 3xx from /proxy: the resolved, absolute redirect target.
  • /inspect: method, location (resolved 3xx target), and with body=1 truncated, maxBodySize and decodeError. get=1 takes the headers from a GET (body closed unread) instead of a HEAD, as the PHP proxy's headers-only fetch does — some servers, e.g. www.google.com, send Strict-Transport-Security only on GET.
  • /page: hops gain method, location, errorCategory, errorType and a certificate summary (certCN, certExpiry, certDaysLeft); the response gains truncated, maxBodySize and decodeError.

Behaviour changes

All JSON changes are additive — no key was renamed, removed or retyped — but some values and statuses change:

  • /proxy never forwards Location. The browser's fetch() followed it straight to the target, carrying X-Proxy-Token in a preflighted CORS request — leaking the token wherever the target's CORS policy let it through, and failing with "Failed to fetch" or a proxy-local 404 everywhere else. A 3xx now reaches the caller as such, with X-Upstream-Location; pass follow=1 to have the proxy follow it.
  • Timeouts are reported as timeout (504) on every endpoint, including dial, TLS-handshake and response-header timeouts, which used to be connection_error or upstream_error (502); a DNS lookup that times out (e.g. an unreachable --dns server) used to be dns_error (502). On /page this also applies to hop errors and the final GET, whose error.status was always 502.
  • /page reports the real error code. A chain failure used to arrive as upstream_error whatever happened; now error.error is the failed hop's errorCategory (tls_error, dns_error, timeout, redirect_loop, too_many_redirects, blocked_target, …), with status 504 for a timeout. On a failure ip names the server that failed.
  • /page stops when a 21st redirect follows with a too_many_redirects hop; exactly 20 redirects still reach the final page. Before, the request after the 20th redirect was fetched as the final page even if it redirected again.
  • /page treats an unusable Location as an error. An unparseable value (bad_request) or a non-http(s) scheme (blocked_target) ends the chain; before, the tracer stopped silently and fetched the redirecting URL again as the final page.
  • HEAD→GET fallback: /inspect and every /page hop repeat a request that the server refused with 405 or 501 as GET (headers only, body closed unread), as the PHP proxy does.
  • rawHeaders (top level and per hop) starts with the status line (HTTP/1.1 301 Moved Permanently, HTTP/2 200) and lists headers sorted by name; Go's map order made it differ from call to call.
  • No more binary html / body: a response in a registered coding Go cannot decode (br, zstd, compress, dcb, dcz, …), in stacked codings (gzip, br), or a corrupt gzip/deflate stream now gives decodeError: true and only the decoded text. Before, the raw compressed bytes were returned as the page. An unregistered token (Content-Encoding: none, UTF-8, …) still passes a plain body through as before, but only if it is valid UTF-8; binary bytes behind such a token now give decodeError: true and an empty body. See Compression (opens in a new tab).
  • Truncated bodies (/page html, /inspect body) are cut on a UTF-8 character boundary and flagged with truncated: true.
  • daysLeft is floored, matching the PHP proxy (differs only for expired certificates, which earlier versions never reported). It is computed on whole Unix seconds, so a NotAfter of 9999-12-31 (RFC 5280's "no expiration") is no longer capped at 106751 days.
  • ssl.chain lists at most 10 certificates (earlier versions listed every certificate the server sent).
  • chain[].subject / issuer fall back to the organisation, then "Unknown", when a certificate has no CN (was "").

Security

  • Token leak via Location closed (see above).
  • Handshake-only TLS probe. This is the one place the proxy skips certificate verification, and it is deliberately narrow: only with diag=1 after a tls_error, only to the IP the failed request had already connected to, through the same safeDialControl SSRF guard, bounded to 10 s, and the connection never carries application data. Every connection that carries HTTP data stays verified; tests enforce both rules. See TLS diagnostic probe (opens in a new tab).
  • Redirect targets followed by /proxy?follow=1 and /page must be http/https; userinfo is dropped, and each target passes the SSRF pre-check before it is requested.

Fixes

  • X-Upstream-IP and ip could name the wrong server: with Happy Eyeballs, a losing dial's late ConnectDone overwrote the winner's address. Only a successful connect is recorded now.
  • /inspect?body=1 timing included the HEAD encoding probe; it now describes the GET alone.
  • The specific certificate messages ("TLS certificate signed by unknown authority", "TLS certificate invalid", "TLS certificate hostname mismatch") were unreachable, because the wrapping *tls.CertificateVerificationError was tested first; every certificate failure read "TLS certificate verification failed".
  • An https:// URL pointing at a plain-HTTP server was still reported as upstream_error: http.Client reports that case as http.ErrSchemeMismatch rather than the tls.RecordHeaderError that v1.2.1 handled. It is a tls_error ("TLS protocol not supported") now.
  • /proxy cut a compressed chunked body (Content-Encoding set, no Content-Length) at --max-mb without saying so. It now sets X-Upstream-Truncated: 1, and X-Upstream-Content-Length is the capped size.

Known limitations

  • X-Upstream-Truncated is not set for a chunked /proxy body without Content-Encoding: that body is streamed, so the headers are already sent when the cap cuts it. A body with Content-Length, and a compressed chunked body (buffered before the headers go out), are flagged.
  • The diagnostic probe cannot describe servers Go refuses to handshake with even without verification (negative serial numbers, RSA keys below 1024 bit, SHA-1 handshake signatures in TLS 1.2, SSLv3); ssl then comes from the verification error or stays null.
  • Internationalised host names must arrive in punycode for hostMatch and the probe's SNI; the standard library has no IDNA.

Tests

  • New test PKI with real httptest TLS servers, covering the certificate report, every TLS verdict, the probe's guarantees, and the new /inspect, /page and /proxy behaviour — see Testing (opens in a new tab). All earlier tests pass unchanged apart from extractSSLInfo gaining its host argument.

Build & CI

  • No toolchain change: Go 1.27.3, CI pin go-version: "1.27"
  • gofmt, go vet, staticcheck, govulncheck and go test -race pass

v1.2.4

Security release. Toolchain-only: no changes to main.go or main_test.go, no client-side adjustments needed. Built with Go 1.27.3 (2026-10-19), an URGENT-track (opens in a new tab) release with a single security fix to crypto/x509. Every earlier binary is affected — please update.

CVE-2026-111943 — distrusted intermediate certificates (go.dev/issue/82345 (opens in a new tab)). Go now blocks trust in a small set of intermediate CA certificates "due to specific security concerns around their operation". Certificate.Verify drops every chain that contains one of them, matched by the SHA-256 hash of the certificate's public key (SPKI). The filter applies on every platform: the pure-Go verifier (Linux, BSD) and the macOS and Windows platform verifiers alike.

Relevant — the proxy verifies the certificate of every target. TLS verification is always on, so all three endpoints (/proxy, /inspect, /page) rely on crypto/x509 to decide whether a target is trustworthy. Before this release, a certificate issued through one of these intermediates was accepted as valid. /inspect and /page also reported it as valid in their SSL details. Now the handshake fails and the client gets a tls_error with the message "TLS certificate verification failed". The error format is unchanged.

Corrected in v1.3.0: this note originally named the message "TLS certificate invalid". The underlying error is an x509.CertificateInvalidError, but the v1.2.4 classifier tested the wrapping *tls.CertificateVerificationError first, so only the generic message was reachable. From v1.3.0 on, the specific x509 checks run first: the message is "TLS certificate invalid" and type is "TLS/SSL error".

govulncheck does not flag this yet: at release time the Go vulnerability database had no entry for CVE-2026-111943, so the scan is clean on 1.27.2 and 1.27.3 alike. The upgrade follows the Go team's announcement, not a scanner finding.

Build & CI

  • go.mod go directive bumped from go 1.27.2 to go 1.27.3
  • CI pin stays go-version: "1.27" and resolves to 1.27.3 automatically
  • The staticcheck recipe (v0.8.1 against golang.org/x/tools v0.51.0) works unchanged on 1.27.3
  • gofmt, go vet, staticcheck, govulncheck, go test -race and cross-compilation for all release targets pass on Go 1.27.3

v1.2.3

Security release. Toolchain-only: no changes to main.go or main_test.go, no client-side adjustments needed. Built with Go 1.27.2 (2026-10-08), which carries security fixes to the go command and to crypto/tls, html/template, net/http, net/textproto and os. Every v1.2.x binary built with Go 1.27.0 or 1.27.1 is affected — please update.

govulncheck against the 1.27.1 standard library reports nine advisories whose symbols this binary reaches, plus four in imported packages that the code does not call. With 1.27.2 it reports none. Reachable symbols do not mean every one of them can be triggered against the proxy. Assessed against its actual setup — an HTTP/1-only server on loopback, and an HTTP/2-capable client to arbitrary, user-supplied targets:

Relevant — the proxy is the HTTP/2 client of a potentially hostile target

  • GO-2026-6610 / CVE-2026-78660 — HTTP/2 transport accepted malformed framing-related headers. An HTTP/2 response could carry malformed framing-related headers such as Content-Length. /proxy forwards the target's Content-Length to the browser over HTTP/1, which is exactly the response-smuggling pattern the advisory describes for proxies. (Transfer-Encoding was already stripped as a hop-by-hop header.) With 1.27.2 such a response fails as an upstream error and is no longer passed through.
  • GO-2026-6611 / CVE-2026-78669 — CPU exhaustion from repeated SETTINGS_INITIAL_WINDOW_SIZE changes. This affects HTTP/2 clients as well as servers. A malicious target could keep the proxy busy for the length of a request. The per-request timeout limited this; it is now fixed outright.

Reachable in the call graph, not exploitable in this setup

  • HTTP/2 server issues — GO-2026-6617 (HPACK encoder race → crash), GO-2026-6612 (double flow-control refund), GO-2026-6603 (memory exhaustion via Trailer). The proxy serves plain HTTP on 127.0.0.1 without TLS and does not enable unencrypted HTTP/2, so its server never speaks HTTP/2.
  • CONNECT desync — GO-2026-6613 (server, 2xx to CONNECT) and GO-2026-6605 (client, CONNECT with a body). The proxy rejects CONNECT with 405 and never sends it upstream; only GET, HEAD and POST are accepted.
  • GO-2026-6607 — ECH outer extensions (crypto/tls): server-side ClientHello decoding. The proxy is only ever a TLS client.
  • GO-2026-6608 — MIME header memory limit (net/textproto): triggered by multipart form parsing, which the proxy does not do. net/textproto also sits in the HTTP/1 header path, so the fix applies there too.

Not reached — GO-2026-6609 (Range parsing in ServeContent/ServeFile), GO-2026-6604 (os.Root junctions, Windows), GO-2026-6599 and GO-2026-6600 (html/template). The go command fix affects building only. CI builds this repository's own stdlib-only code and fetches no third-party modules into the binary.

Build & CI

  • go.mod go directive bumped from go 1.27.1 to go 1.27.2
  • govulncheck reports no findings on the 1.27.2 toolchain (nine reachable advisories on 1.27.1)
  • CI pin stays go-version: "1.27" and resolves to 1.27.2 automatically
  • staticcheck build fixed for Go 1.27.2. The toolchain writes export data version 5, and staticcheck v0.8.1 still pins golang.org/x/tools v0.44, which stops at version 4. Without this fix the verify job fails on every push. CI now builds staticcheck v0.8.1 in a throwaway module against golang.org/x/tools v0.51.0 (replacing the v0.8.0-rc.1 pin). See Linting (opens in a new tab).
  • gofmt, go vet, staticcheck, go test -race and cross-compilation for all release targets pass on Go 1.27.2

v1.2.2

Toolchain-only release: no code changes, no behaviour changes, no client-side adjustments needed. Built with Go 1.27.1 (2026-09-01), a regular bug-fix point release — explicitly not a security release, it carries no CVE fix. The move keeps the binaries on the current patch level of the 1.27 series.

Of the twelve backported fixes, three touch packages the proxy imports; none of them has an observable effect on it:

  • net/http — Go issue #81027. Since 1.27.0, Request.Body.Close on the server side returned io.EOF instead of nil when a handler left part of the body unread (a refactor dropped the line that cleared it). /proxy hands the incoming POST body to the upstream request, and the transport closes it without checking that error on its failure paths — so the regression was invisible here. Fixed nonetheless.
  • encoding/json — Go issues #81083 and #81012. A quoted null was rejected for fields tagged ,string, and Decoder.Token lost io.ErrUnexpectedEOF after the move to the v2 backend. Both are decoding paths; the proxy only encodes JSON, and uses neither ,string tags nor Decoder.Token.

The rest does not apply: database/sql, debug/elf and simd are not imported; the compiler fixes cover generic methods on alias receivers and s390x/arm64 SIMD code generation, and the go fix fixes touch a build-time tool only.

Build & CI

  • go.mod go directive bumped from go 1.27.0 to go 1.27.1
  • govulncheck reports no findings on the 1.27.1 toolchain
  • CI pin stays go-version: "1.27" and resolves to 1.27.1 automatically

v1.2.1

Fixed — TLS handshake failures were misreported

classifyUpstreamError only recognised certificate problems as tls_error. Every other way a TLS handshake can fail was mislabelled, so the frontend could not tell "this host is unreachable" from "this host and I cannot agree on TLS":

Target Before Now
Server stuck on TLS 1.0 / 1.1 upstream_error tls_error
No mutually supported cipher suite connection_error tls_error
Peer alert (handshake_failure, certificate_required, …) connection_error tls_error
Plain HTTP server behind an https:// URL upstream_error tls_error
Connection refused / reset, DNS, certificates unchanged unchanged

Two separate causes, because errors.As cannot see either one:

  • crypto/tls delivers peer alerts as a *net.OpError with Op set to "remote error" or "local error", wrapping the unexported tls.alert type — the exported tls.AlertError never matches, so Op is the only usable marker. These were being swallowed by the generic *net.OpError check.
  • Failures the client rejects locally are bare fmt.Errorf values with no type whatsoever (261 of them in Go 1.27), such as tls: server selected unsupported protocol version 301. The only marker left is the package-wide tls: message prefix, which is read off the innermost error — never the flattened chain, because the wrapping *url.Error contains the caller-supplied URL and a crafted query string could otherwise forge a TLS verdict.

Verified against real failing handshakes (httptest in the test suite, plus badssl.com endpoints end-to-end); certificate, DNS, SSRF and genuine socket errors keep their existing codes.

Note this only affects the error code. HTTP status stays 502 and the message field remains display-safe, so no frontend change is required.

Corrected in v1.3.0: the "Plain HTTP server behind an https:// URL" row only held for a bare TLS handshake. Through http.Client that case surfaces as http.ErrSchemeMismatch and stayed upstream_error until v1.3.0.

v1.2.0

Toolchain release: the proxy moves to Go 1.27.0 (2026-08-19). No code changes, no API changes, no client-side adjustments — every endpoint, field and error code behaves exactly as in v1.1.1.

This is explicitly not a security release. Go 1.27.0 carries no stdlib fix that was not already in Go 1.26.6; the ten-CVE batch documented under v1.1.1 shipped in both. govulncheck reports no findings on either toolchain. The reason to move is that Go 1.26 reaches end of life when Go 1.28 lands (~February 2027), and 1.27 is where the fixes will arrive from now on.

Breaking for macOS 12 users

  • Go 1.27 discontinued support for macOS 12 Monterey and older; the darwin binaries now require macOS 13 Ventura or later. On an older macOS, stay on v1.1.1 (opens in a new tab) — it is functionally identical. No other platform in the release matrix changed its minimum requirement.

Behaviour changes inherited from the toolchain

  • crypto/x509 on macOS and Windows now honours SSL_CERT_FILE and SSL_CERT_DIR. When either is set, roots are read from disk and verification switches from the platform APIs to the pure-Go verifier. This is the one change that can alter what /inspect and /page report about a certificate chain — but only for users who have those variables exported (corporate MITM proxies, Homebrew OpenSSL setups). GODEBUG=x509sslcertoverrideplatform=0 restores the old behaviour. Linux was already reading these variables.
  • ML-DSA signature schemes (MLDSA44/65/87, FIPS 204) are now advertised by default in TLS 1.3 ClientHellos. Verified against TLS 1.3 and TLS 1.2-only targets with no interop change; the negotiated suite and everything /inspect reports under ssl are unaffected.
  • HTTP/1 response bodies are auto-drained on Close, capped at 256 KB and 50 ms, to improve connection reuse. It runs in the transport's background read loop and never blocks the handler — Close returns in microseconds either way (measured). The drain is skipped when Content-Length is above 256 KB, but a chunked response reports Content-Length: -1 and therefore passes that gate: aborting a chunked body early — which is exactly what the --max-mb truncation path does — now pulls up to ~256 KB more from the target in the background before the connection is dropped. Bounded, off the critical path, and invisible in the response, but it is extra upstream traffic that Go 1.26 did not generate. DisableKeepAlives does not suppress it; ForceAttemptHTTP2 keeps most HTTPS targets on h2, which never enters this loop at all.
  • net/http servers cap header values at 500 per request (DefaultMaxHeaderValueCount). Inbound hardening on the local listener only; no browser request comes close.
  • Legacy TLS escape hatches are gone for good: the tlsrsakex, tls3des, tls10server, tlsunsafeekm and x509keypairleaf GODEBUG settings were removed. All had been off by default since Go 1.22/1.23, so nothing changes in practice — a target that only speaks RSA key exchange or 3DES was already unreachable.
  • encoding/json is now backed by the v2 implementation. Output is unchanged: HTML escaping, sorted map keys, null for nil slices and U+FFFD replacement for invalid UTF-8 in header values all verified identical. Only the text of internal error messages may differ, and those are never sent to the client.

Build & CI

  • go.mod go directive bumped from go 1.26.6 to go 1.27.0
  • CI pin moved from go-version: "1.26" to "1.27" in both jobs
  • staticcheck is now pinned to @v0.8.0-rc.1 instead of @latest: the current stable release (2026.1 / v0.7.0) cannot read Go 1.27 export data and aborts on every stdlib import. Revert to @latest once 2026.2 is stable.
  • gofmt, go vet, staticcheck, govulncheck and the full test suite pass on 1.27.0 with no source changes

v1.1.1

Toolchain-only release: no code changes, no behaviour changes, no client-side adjustments needed. Built with Go 1.26.6 (2026-08-13), which carries ten security fixes. Four of them touch code paths this proxy actually executes:

  • encoding/asn1 — CVE-2026-33818. Unmarshal had no recursion limit, so a deeply nested structure could exhaust the stack. /inspect and /page parse the certificate chain of whatever host the caller names, so a hostile TLS server was a plausible route to crashing the proxy.
  • net (vendored dns/dnsmessage) — CVE-2026-46600. Parsing a malformed SVCB or HTTPS record with an overflowing parameter value could panic. The proxy uses the pure-Go resolver (PreferGo: true) against the servers given via --dns, and the parser walks every record in a reply — not just the A/AAAA answers it asked for.
  • net/url — CVE-2026-56860. resolvePath was quadratic in the number of .. segments. /page resolves each Location header against its base URL while tracing the redirect chain, so a target server could bill the proxy for the CPU time. The 20-hop limit bounded the number of resolutions but not the cost of one.
  • net/http — CVE-2026-56853. ReadHeaderTimeout was not applied while the server sniffed for an HTTP/2 preface on a new cleartext connection. The proxy sets ReadHeaderTimeout: 10s and serves cleartext HTTP, so the gap was real — though only reachable from the local machine, since the listener binds to 127.0.0.1 and the Host header must be loopback.

The remaining six do not apply: encoding/xml and html/template are not imported; x/net/idna (CVE-2026-39821) affects hostname normalisation, but the SSRF boundary sits in the dialer after resolution and cannot be steered by a label-parsing quirk; crypto/tls KeyUpdate flooding (CVE-2026-56862) targets TLS servers and the proxy is a client; the two x/mod/sumdb transparency-log issues (CVE-2026-56864, CVE-2026-56865) affect the go command at build time, not the shipped binary — relevant to CI, which now runs on the fixed toolchain.

Build & CI

  • go.mod toolchain directive bumped from go 1.26.5 to go 1.26.6
  • govulncheck reports no findings on the 1.26.6 toolchain
  • CI pin stays go-version: "1.26" and resolves to 1.26.6 automatically

v1.1.0

Security-focused release. No changes are required in client code — existing fields keep their meaning and only additive fields were introduced.

New

  • /inspect and /page report an http3 boolean derived from the target's Alt-Svc field (RFC 7838, ALPN token h3 per RFC 9114). This answers "does this site support HTTP/3?" without a QUIC stack — see HTTP/3 detection (opens in a new tab). Draft tokens like h3-29 are ignored.

Security

  • SSRF is now enforced in the dialer, after DNS resolution and before connect. The previous hostname pre-check resolved the name separately from the HTTP client, so an attacker-controlled zone with a short TTL could answer with a public address for the check and a loopback address for the connection. The pre-check remains as a fast-fail path only.
  • Blocked ranges extended: reserved and special-purpose IPv4 space (0/8, 192.0.0.0/24, 198.18/15, multicast, 240/4) plus the IPv6 forms that embed an IPv4 address — IPv4-mapped, NAT64 (64:ff9b::/96) and 6to4 (2002::/16). 64:ff9b::7f00:1 reaches 127.0.0.1 on a NAT64 network and was previously invisible to a v4-only check.
  • Host-header check: requests must address the proxy as 127.0.0.0/8, localhost or [::1], blocking DNS rebinding against the proxy itself. Override with --allow-any-host.
  • Credential and identity headers are no longer forwarded to targets: Cookie, Cookie2, Authorization, Proxy-Authorization, Origin, Referer and Sec-Fetch-*. Headers the caller sets deliberately — Accept, User-Agent, Accept-Language, custom X-* — still pass through.
  • Bounded work per request: /page derives one overall time budget from the client's request context. Redirect hops and encoding probes previously used context.Background(), so a client disconnect stopped nothing and 20 slow redirects could keep a handler alive for minutes.
  • DNS lookups in the SSRF pre-check are now bounded (5 s) instead of able to hang indefinitely against a black-holed resolver.
  • Constant-time token comparison via crypto/subtle.
  • Truncated bodies no longer break the response: when a body exceeds --max-mb, the upstream Content-Length is dropped (it would promise more bytes than are delivered, aborting the connection) and X-Upstream-Truncated: 1 is set instead.
  • Error responses are now readable by the caller: all 4xx responses carry CORS headers and a JSON body with a machine-readable code (bad_request, forbidden, blocked_target, method_not_allowed). A wrong token used to be indistinguishable from an unreachable proxy. Rejected origins still get a bare 403 with no CORS headers, by design.
  • X-Content-Type-Options: nosniff on all proxy-generated responses.

Fixes & quality

  • Content-Encoding: deflate now decodes both zlib-wrapped (RFC 1950) and raw DEFLATE (RFC 1951). Only raw was handled, so zlib-wrapped bodies — which a large share of servers send — came back empty or corrupt.
  • Headers named in a Connection header are treated as hop-by-hop (RFC 9110 §7.6.1).
  • Vary: Origin is now always set, not only when a CORS origin was echoed.
  • SSL info includes IP SANs, not just DNS names.
  • --port, --timeout and --max-mb are validated at startup; --timeout 0 previously produced an already-expired context and failed every request.
  • Upstream Content-Length is sanity-checked before being reported as transferSize.
  • Shared handler preamble replaces the duplicated method/origin/token/URL/SSRF blocks across the three endpoints.
  • JSON encoding errors are logged rather than silently discarded.

Build & CI

  • Built with Go 1.26.5 — security fixes in crypto/tls (GO-2026-5856 / CVE-2026-42505, ECH handshake de-anonymisation) and os
  • go.mod toolchain directive bumped from go 1.26.4 to go 1.26.5
  • CI now runs on every push and pull request, not only on tags — a broken test no longer surfaces first at release time
  • govulncheck and a gofmt check added to CI
  • GITHUB_TOKEN scoped to read-only by default; only the release job gets write

v1.0.5

  • Built with Go 1.26.4 — picks up upstream stdlib fixes for crypto/tls, crypto/x509, net/http, and the runtime
  • go.mod toolchain directive bumped from go 1.26.3 to go 1.26.4

v1.0.4

  • Built with Go 1.26.3 — picks up upstream stdlib fixes for crypto/tls, crypto/x509, net/http, and the runtime
  • go.mod toolchain directive bumped from go 1.26.0 to go 1.26.3

v1.0.3

  • Hardened /inspect and /page body decompression: the decompressed stream is now also capped at --max-mb, preventing zip-bomb-style payloads where a small compressed body would expand to gigabytes of memory
  • Breaking (JSON shape): /inspect and /page now return headers as map[string][]string (JSON arrays of values) instead of map[string]string (joined with ", "). This fixes RFC 6265 §3 — multiple Set-Cookie headers used to be joined with ", ", producing an unparseable string because cookie expiry dates contain literal commas (e.g. expires=Sun, 27 Apr 2025 14:03:58 GMT). Browser-side consumers should access values via headers["X"][0] for single-value or headers["X"].join(", ") for display. A Array.isArray() guard makes code compatible with both old and new format.
  • CI now runs staticcheck ./... on linux/amd64 before producing release binaries; any finding blocks the release
  • Code formatted with gofmt -w (struct alignment cleanup)

v1.0.2

  • Built with Go 1.26.2 — includes upstream security fixes for crypto/tls, crypto/x509, net/url, and net/http
  • CI now runs go test -race -v ./... on linux/amd64 before producing release binaries; failed tests block the release
  • Expanded README testing section with -race and -cover examples and a summary of what main_test.go covers

v1.0.1

  • Added --version CLI flag to print version information
  • Added /version endpoint returning version, license, author, and repo as JSON
  • Added GPL-2.0-or-later license
  • Startup banner now shows version, license, author, and repository URL
  • Startup banner groups Address/Token, DNS/Origins, and License/Author/Repo into visually separated sections
  • Fixed resource safety: response body in redirect chain tracing is now closed immediately after receiving the response
  • Added FreeBSD (amd64, arm64), OpenBSD (amd64), and NetBSD (amd64) release binaries

v1.0.0

  • Initial release
  • Streaming proxy (/proxy), metadata inspection (/inspect), full page analysis (/page)
  • Per-session cryptographic token authentication
  • Origin allowlist, SSRF protection, configurable DNS resolver
  • CORS with Private Network Access support
  • Cross-platform binaries via GitHub Actions