From 112621f0c0f9e6000fbc119f43e52ea7737fe6f7 Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 16 Mar 2026 15:19:18 +0000 Subject: [PATCH] fix(wrapper): strip upstream auth response headers --- spec/unit/features/wrapper.feature | 9 +++++++++ spec/unit/wrapper_spec.lua | 28 ++++++++++++++++++++++++++++ src/fairvisor/wrapper.lua | 13 +++++++++++++ src/nginx/header_filter.lua | 6 ++++++ 4 files changed, 56 insertions(+) diff --git a/spec/unit/features/wrapper.feature b/spec/unit/features/wrapper.feature index b8fa0de..61e4372 100644 --- a/spec/unit/features/wrapper.feature +++ b/spec/unit/features/wrapper.feature @@ -193,6 +193,15 @@ Feature: LLM Proxy Wrapper Mode unit behavior When access_handler is called Then response exit code is 429 + Rule: Response auth header sanitization + Scenario: auth-related upstream response headers are stripped + Given the nginx mock is set up + And wrapper response headers contain auth-related headers + When I call strip_response_auth_headers + Then response header "Authorization" is nil + And response header "x-api-key" is nil + And response header "x-goog-api-key" is nil + And response header "Content-Type" is "application/json" Rule: wrapper init Scenario: init with valid deps table returns true When I call wrapper init with valid deps diff --git a/spec/unit/wrapper_spec.lua b/spec/unit/wrapper_spec.lua index 0304b42..b491429 100644 --- a/spec/unit/wrapper_spec.lua +++ b/spec/unit/wrapper_spec.lua @@ -285,6 +285,34 @@ runner:then_("^header descriptors is empty$", function(ctx) assert.equals(0, count) end) +-- --------------------------------------------------------------------------- +-- Response auth header sanitization +-- --------------------------------------------------------------------------- + +runner:given("^wrapper response headers contain auth%-related headers$", function(_ctx) + ngx.header = { + ["Authorization"] = "Bearer should-not-leak", + ["x-api-key"] = "anthropic-secret", + ["x-goog-api-key"] = "gemini-secret", + ["Content-Type"] = "application/json", + } +end) + +runner:when("^I call strip_response_auth_headers$", function(ctx) + wrapper.strip_response_auth_headers() + ctx.response_headers = ngx.header +end) + +runner:then_("^response header \"(.-)\" is nil$", function(ctx, name) + assert.is_not_nil(ctx.response_headers) + assert.is_nil(ctx.response_headers[name]) +end) + +runner:then_("^response header \"(.-)\" is \"(.-)\"$", function(ctx, name, value) + assert.is_not_nil(ctx.response_headers) + assert.equals(value, ctx.response_headers[name]) +end) + -- --------------------------------------------------------------------------- -- access_handler setup helpers -- --------------------------------------------------------------------------- diff --git a/src/fairvisor/wrapper.lua b/src/fairvisor/wrapper.lua index 65b2e06..d8f4dba 100644 --- a/src/fairvisor/wrapper.lua +++ b/src/fairvisor/wrapper.lua @@ -406,6 +406,19 @@ function _M.strip_policy_headers(header_set) end end +-- strip_response_auth_headers() +-- Removes upstream auth-related response headers before they reach the client. +function _M.strip_response_auth_headers() + if not ngx or not ngx.header then return end + + ngx.header["Authorization"] = nil + ngx.header["authorization"] = nil + ngx.header["x-api-key"] = nil + ngx.header["X-Api-Key"] = nil + ngx.header["x-goog-api-key"] = nil + ngx.header["X-Goog-Api-Key"] = nil +end + -- replace_openai_cutoff(output, cutoff_format) -- Post-processes streaming.body_filter() output to replace the OpenAI-style -- cutoff sequence with the provider-native format. diff --git a/src/nginx/header_filter.lua b/src/nginx/header_filter.lua index 519acb0..5896e8a 100644 --- a/src/nginx/header_filter.lua +++ b/src/nginx/header_filter.lua @@ -1,3 +1,9 @@ +if ngx.ctx and ngx.ctx.wrapper_provider then + local wrapper = require("fairvisor.wrapper") + wrapper.strip_response_auth_headers() + return +end + if ngx.var.fairvisor_mode ~= "reverse_proxy" then return end