"""Stop LiteLLM from forwarding the provider's response headers to callers. SECURITY (2026-10-27, member-disclosure finding 4): LiteLLM's proxy copies every upstream response header onto its own response as `llm_provider-` — see litellm_core_utils/llm_response_utils/get_headers.py::_get_llm_provider_headers, whose docstring reads "Forward all headers as llm_provider-{header}". Verified live on the co-op gateway: one chat completion returned 27 such headers, among them `llm_provider-x-credits-remaining: 16.52` — the co-op's remaining balance with the provider, readable by any member API key, and (read across calls) its burn rate. This patch makes _get_llm_provider_headers() return {} so no provider header is copied. get_response_headers() still forwards the four OpenAI-style x-ratelimit-* headers WHEN a provider sends them; those describe the caller's own request rather than the co-op's account, and no upstream in our config sends them today. Run at BUILD time (the runtime rootfs is read-only under Cloudron). Non-fatal, per house rule: if upstream moves or rewrites the function, WARN and exit 0 — a cosmetic leak must never keep the gateway from booting. Regression check: the daily litellm watcher asserts the gateway returns no `llm_provider-*` header. """ import glob import os import sys ROOT = os.environ.get("COOP_PATCH_ROOT", "/app") TARGET = "/litellm/litellm_core_utils/llm_response_utils/get_headers.py" ANCHOR = ''' for k, v in response_headers.items(): if "llm_provider" not in k: _key = "{}-{}".format("llm_provider", k) llm_provider_headers[_key] = v else: llm_provider_headers[k] = v return llm_provider_headers''' REPLACEMENT = ''' # CO-OP PATCH (2026-10-27): do NOT forward provider headers to callers. # Upstream prefixes EVERY response header with "llm_provider-", which leaked # the co-op's remaining provider balance (llm_provider-x-credits-remaining) # to any member API key. Delete this patch from the Dockerfile to revert. return {}''' candidates = glob.glob(f"{ROOT}/.venv/lib/python*/site-packages{TARGET}") if not candidates: candidates = glob.glob(f"{ROOT}/**{TARGET}", recursive=True) patched_any = False for p in candidates: src = open(p).read() if "CO-OP PATCH (2026-10-27)" in src: print(f"INFO: provider-header strip already present in {p}") patched_any = True continue if ANCHOR not in src: print(f"WARN: anchor not found in {p} (upstream changed it); " "provider headers are still forwarded (non-fatal)", file=sys.stderr) continue new = src.replace(ANCHOR, REPLACEMENT, 1) try: compile(new, p, "exec") except SyntaxError as e: print(f"WARN: patched source would not compile ({e}); not written " "(non-fatal)", file=sys.stderr) continue with open(p, "w") as f: f.write(new) print("[coop-patch] applied: provider response headers are no longer " f"forwarded to callers ({p})") patched_any = True if not candidates: print("WARN: get_headers.py not found; provider headers are still forwarded " "(non-fatal)", file=sys.stderr) elif not patched_any: print("WARN: no copy of get_headers.py patched (non-fatal)", file=sys.stderr) sys.exit(0)