HIGH cache poisoningphoenixmutual tls

Cache Poisoning in Phoenix with Mutual Tls

Cache Poisoning in Phoenix with Mutual Tls — how this specific combination creates or exposes the vulnerability

Cache poisoning occurs when an attacker tricks a cache (e.g., CDN, reverse proxy, or in-memory cache) into storing malicious content that is then served to other users. In Phoenix, this typically involves manipulating HTTP cache-related headers such as Cache-Control, Vary, and ETag to cause different responses to be cached under the same key. When mutual TLS (mTLS) is enforced between the client and server, the assumption is often made that the client identity is verified and that requests are therefore safe to cache or reuse. However, mTLS does not inherently prevent cache poisoning; it only authenticates the client to the server. If a Phoenix endpoint uses mTLS for client authentication but still varies cache keys on insufficient attributes (e.g., omitting the request body or specific headers), an authenticated client can submit a request that causes a poisoned cache entry to be stored and later served to other authenticated clients.

For example, an authenticated client with a valid mTLS certificate could send a request with a manipulated Accept-Language header and a poisoned Cache-Control: public directive. If the Phoenix controller caches the response based only on the path and query parameters, other clients with different language preferences may receive the incorrect, potentially malicious content. This becomes particularly dangerous when sensitive data or security-related headers are involved, as the cached response may inadvertently expose private information or execute unintended logic. Because mTLS ensures the client is known, developers might incorrectly trust the request and relax input validation or cache key design, inadvertently expanding the attack surface. The combination of strong client authentication and improperly designed caching logic can therefore amplify the impact of cache poisoning by making poisoned entries more likely to be reused across trusted clients.

Compounding the issue, Phoenix applications that rely on Elixir plug pipelines may inadvertently cache responses at the plug level when cache_resp or similar mechanisms are used without carefully scoping the :cacheable options. If the pipeline does not include the full set of differentiating headers or the request body in the cache key, an attacker can exploit this gap. Using mTLS also means that logging and monitoring may assume encrypted client identities provide sufficient context, potentially obscuring anomalies in cache behavior. This highlights the need to treat mTLS as an authentication layer, not a safeguard against injection via cache mechanisms. Proper remediation requires aligning cache key construction with the trust boundary introduced by mTLS and ensuring that all inputs that affect the response are part of the cache key, while also validating and normalizing headers before caching.

Mutual Tls-Specific Remediation in Phoenix — concrete code fixes

To mitigate cache poisoning in Phoenix while using mutual TLS, focus on ensuring that cache keys incorporate all request attributes that influence the response, including headers and body when relevant, and avoid trusting mTLS alone to prevent injection. Below are concrete code examples that demonstrate secure practices.

First, define a function to build a robust cache key that includes the request path, selected headers, and, if applicable, a hash of the body. This prevents different clients from sharing the same cached response when headers such as Accept-Language or custom authentication headers differ.

defmodule MyApp.Cache do
  def cache_key(conn) do
    path = conn.request_path
    headers = %{
      "accept-language" => conn.req_headers["accept-language"],
      "authorization"   => conn.req_headers["authorization"]
    }
    body_hash = hash_body(conn)
    :erlang.term_to_binary({path, headers, body_hash})
  end

  defp hash_body(%{body_params: body}) when not is_nil(body) do
    :crypto.hash(:sha256, Jason.encode!(body))
  end
  defp hash_body(_), do: :crypto.hash(:sha256, <<>>)
end

Next, integrate this key into your pipeline with a plug that caches responses only when safe, ensuring that sensitive or user-specific responses are not marked as public. Use Plug.Cache with explicit vary headers and avoid caching responses that contain private data.

defmodule MyApp.Pipeline do
  use Plug.Router
  plug Plug.Parsers, parsers: [:urlencoded, :json]
  plug Plug.Cache, 
      cache_opts: [cache: MyApp.CacheStore],
      key: &MyApp.Cache.cache_key/1,
      cacheable_statuses: [200],
      vary: ["Accept-Language", "Authorization"]

  plug :match
  plug :dispatch

  get "/profile" do
    # Ensure no sensitive data is cached inadvertently
    send_resp(conn, 200, Jason.encode!(%{user: conn.assigns.current_user}))
  end
end

On the mTLS side, verify client certificates explicitly and bind the verified peer identity to the connection assigns without assuming it is sufficient for caching. In your endpoint configuration, enforce client verification and map the certificate subject to a conn assign for later use in logging or authorization, but do not rely on it to determine cacheability.

defmodule MyApp.Endpoint do
  use Plug.Cowboy, otp_app: :my_app, scheme: :https

  def init(_opts) do
    [
      port: 443,
      certfile: "priv/cert.pem",
      keyfile: "priv/key.pem",
      cacertfile: "priv/ca.pem",
      verify: :verify_peer,
      depth: 10,
      customize_hostname_check: [match_fun: :public_key.pkix_verify_hostname_match_fun(:https)]
    ]
  end

  def handle(conn, _opts) do
    client_cert = Plug.SSL.client_certificate(conn)
    subject = extract_subject(client_cert)
    assign(conn, :client_subject, subject)
  end

  defp extract_subject(cert) when not is_nil(cert) do
    {:OTPCertificate, der} = :public_key.pkix_cert_decode(cert)
    {:TBSCertificate, tbs} = :public_key.pkix_extract_tbs(der)
    tbs[:subject]
  end
  defp extract_subject(_), do: nil
end

Finally, ensure that responses containing sensitive or user-specific data set appropriate Cache-Control headers to prevent caching by intermediaries. In Phoenix controllers, explicitly set headers to restrict caching when the response includes client-specific information, even when mTLS is enforced.

defmodule MyAppWeb.ProfileController do
  use MyAppWeb, :controller

  def show(conn, _params) do
    conn
    |> put_resp_header("cache-control", "no-store, no-cache, must-revalidate")
    |> json(%{profile: build_profile(conn.assigns.client_subject)})
  end
end

Frequently Asked Questions

Does mutual TLS prevent cache poisoning in Phoenix?
No. Mutual TLS authenticates the client to the server but does not alter caching behavior. If cache keys are not constructed to include all request attributes that affect the response, an authenticated client can still poison the cache.
What is a key remediation step for cache poisoning with mTLS in Phoenix?
Build cache keys that include the request path, relevant headers (such as Accept-Language and Authorization), and a hash of the request body when necessary, and avoid marking user-specific or sensitive responses as cacheable.