Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions spec/unit/features/wrapper.feature
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,16 @@ Feature: LLM Proxy Wrapper Mode unit behavior
When I call get_provider for path "/api/v1/some-internal-endpoint"
Then provider is nil

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
Expand Down
28 changes: 28 additions & 0 deletions spec/unit/wrapper_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
-- ---------------------------------------------------------------------------
Expand Down
13 changes: 13 additions & 0 deletions src/fairvisor/wrapper.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
6 changes: 6 additions & 0 deletions src/nginx/header_filter.lua
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Loading