Repository navigation
Security Considerations
For operators and .NET library users deploying Titanium — what protections apply, and when they do not.
The 5.0 hardening pass (see Migration from 4.x for the full list of behavior changes through current 7.x) closes a lot of gaps, but none of it should be read as a blanket guarantee. This page calls out where a protection is conditional — it only applies under a specific configuration or code path — so you don't assume coverage you don't actually have.
The reserved-opcode rejection, pre-buffer frame-size validation, and RFC 6455-conformant close
handling described in Protocol Support only run when the proxy
is actually parsing WebSocket frames — i.e. the connection was decrypted (decryptSsl: true, or plain
HTTP) and the WebSocket upgrade was intercepted by WebSocketInterceptRelay. Two paths bypass all of
this by construction, not by omission:
-
decryptSsl: falseon the endpoint/tunnel handling that connection: the TLS bytes are relayed opaquely end-to-end, so the proxy cannot see — let alone validate — anything inside them, WebSocket or otherwise. -
Non-HTTP relay on a
SocksProxyEndPoint: traffic that doesn't look like HTTP at the start of the connection is relayed transparently to the SOCKS-negotiated destination with no HTTP or WebSocket parsing at all (see SOCKS endpoint).
If you need the WebSocket protections, make sure the traffic in question is actually being decrypted and intercepted, not relayed opaquely.
MaxBufferedBodyBytes (and its HTTP/2/HTTP/3 equivalents) now cumulatively bounds every
whole-body-buffering code path — see
Cumulative body budgets.
It does not bound:
-
The streaming hooks (
OnRequestBodyWrite/OnResponseBodyWrite, see Streaming Bodies) — by design, these process a body in bounded-size pieces with no cap on total length, so an endless stream (e.g. server-sent events) can run indefinitely. Memory stays flat inside the proxy regardless, but only because it never accumulates the body itself — if your own handler copies every piece it sees into a growing buffer, that accumulation is on you, not the proxy. -
A custom
RespondStreamingproducer you supply — the proxy imposes no ceiling on how much you choose to write. -
HTTP/1 aggregate header-block bytes —
PolicyFamily.HeaderLimitsexists as a named policy family (seePolicyFamily.cs) but is not yet wired to numeric enforcement at every header-reading call site; only the HTTP/2 decoded-header-list cap (MaxDecodedHeaderListBytes) and the client header read deadline (a time bound, not a byte bound — see New: client header read deadline) are actually enforced today.
Treat memory-exhaustion protection as scoped to the specific mechanism you're using, and prefer the streaming APIs for anything whose size you can't bound in advance.
Every HTTP/3-specific fix from this hardening pass — background-task ownership, the critical-stream
and missing-SETTINGS enforcement, the QPACK immediate-failure behavior instead of blocking, and the
HTTP/3 body budgets — only has any effect if ProxyServer.EnableHttp3 = true. The QPACK
dynamic-table-specific behavior further requires ProxyServer.EnableQpackDynamicTable = true on top of
that. Neither is on by default, and both remain marked [Experimental("TWP001")]: if you do opt in,
treat the HTTP/3 path as less battle-tested in production than the HTTP/1.x/HTTP/2 path, independent
of how thoroughly it's been hardened here.
Moving the certificate store to a per-user folder and no longer passing the real PFX password on the
certutil.exe command line (see
Certificate store relocated)
raises the bar from "any local user or process on the machine can read the CA private key" to "any
process running as the same OS user account can." That second bar is not, and cannot be, eliminated
by this proxy: a MITM TLS proxy must hold a CA private key somewhere accessible to itself at runtime in
order to sign leaf certificates, and any other code running as your account has the same filesystem
access you do. If your threat model includes other processes running as your own account (e.g. a
shared, multi-tenant build/CI user), protect the key with OS-level mechanisms outside the proxy's
control — a hardware-backed key store, a dedicated service account, or equivalent — rather than
expecting the file relocation alone to isolate it.
ProxyServer.EnableDecryptFailureBypass (off by default in the library; on by default in Inspector) tunnels
later CONNECTs without decrypt after repeated origin TLS handshake failures under MITM, or after
MITM HTTPS 403/429 that carry a bot-management marker (document navigations activate immediately and
meta-refresh onto a new opaque CONNECT; other requests use the shared strike threshold; synthetic
inspector/script responses are ignored). A marker is a vendor header (cf-mitigated, x-datadome*,
x-amzn-waf-action, x-kpsdk-*), Server: AkamaiGHost, a bot-management cookie (_abck, bm_sz,
datadome, incap_ses_*, _px*, ...), or "captcha" / "challenge" in a header name or value (CSP, CORS,
WWW-Authenticate and similar are skipped). A plain 403 or a Retry-After rate-limit 429 is never
learned. The check reads headers only; it does not fingerprint JA3 itself — TLS failures look like
ordinary AuthenticationException. False positives are possible; use a TTL/LRU-bounded session list and
Promote known-bad hosts into permanent tunnel-only exclusions. Post-MITM conversion of the forged-cert
session is impossible; seamless recovery relies on a new CONNECT. Learned hosts skip MITM prefetch.
Embedded proxies should leave the flag off unless they need this behavior.
When interception is off and both legs are HTTP/2, Titanium relays compressed HPACK blocks
verbatim for throughput (PolicyFamily.Http2RelayValidation = Disabled, the Balanced default).
Semantic checks from RFC 9113 §8.3 (malformed pseudo-headers, connection-specific fields, and so on)
are skipped on that path by design.
-
Observe— decode and log violations without rejecting; does not mutate headers. -
Enforce— decode and tear down withGOAWAY(PROTOCOL_ERROR)on violations.ProxyProfile.PublicFacing/ProxyPolicyModes.AllEnforceselect this automatically.
Trust the upstream peer when leaving the default Disabled; switch to Observe then Enforce
when clients are untrusted.
Setting ProxyServer.IgnoreServerCertificateErrors = true accepts every SslPolicyErrors
value — name mismatch, expired certificates, and untrusted CAs included. Prefer
ServerCertificateValidationCallback for scoped trust. A future major version will mark the
boolean property [Obsolete].
SslStream.RemoteCertificateValidationCallback is synchronous. If your async callback posts work
back onto the calling thread (typical on WPF/WinForms), a naive .GetAwaiter().GetResult() can
deadlock. Titanium keeps the IsCompletedSuccessfully fast path for already-completed callbacks
and otherwise runs the incomplete task via Task.Run so it does not capture the handshake
thread's SynchronizationContext.
- Migration from 4.x — cumulative behavior changes from 4.x through current 7.x.
- Protocol Feature Support — full Yes/No/Partial breakdown per protocol.