fix: tighten generic upstream passthrough headers (#422)
* fix: tighten generic upstream passthrough headers * fix: preserve codex passthrough version headers
This commit is contained in:
@@ -696,6 +696,93 @@ describe('buildUpstreamEndpointRequest', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('applies a sub2api-style allowlist to generic passthrough headers', () => {
|
||||
const request = buildUpstreamEndpointRequest({
|
||||
endpoint: 'chat',
|
||||
modelName: 'upstream-gpt',
|
||||
stream: false,
|
||||
tokenValue: 'sk-test',
|
||||
sitePlatform: 'sub2api',
|
||||
siteUrl: 'https://example.com',
|
||||
openaiBody: {
|
||||
model: 'gpt-5.2',
|
||||
messages: [{ role: 'user', content: 'hello' }],
|
||||
},
|
||||
downstreamFormat: 'openai',
|
||||
downstreamHeaders: {
|
||||
accept: 'application/json',
|
||||
'accept-language': 'zh-CN',
|
||||
'user-agent': 'client-ua/1.0',
|
||||
originator: 'codex_cli_rs',
|
||||
session_id: 'session-123',
|
||||
conversation_id: 'conversation-123',
|
||||
'x-codex-turn-state': 'turn-state',
|
||||
'x-codex-turn-metadata': 'turn-metadata',
|
||||
origin: 'https://client.example',
|
||||
referer: 'https://client.example/chat',
|
||||
'x-forwarded-for': '203.0.113.1',
|
||||
'x-real-ip': '203.0.113.2',
|
||||
version: '0.202.0',
|
||||
'x-test-header': 'drop-me',
|
||||
},
|
||||
});
|
||||
|
||||
expect(request.headers.accept).toBe('application/json');
|
||||
expect(request.headers['accept-language']).toBe('zh-CN');
|
||||
expect(request.headers['user-agent']).toBe('client-ua/1.0');
|
||||
expect(request.headers.originator).toBe('codex_cli_rs');
|
||||
expect(request.headers.session_id).toBe('session-123');
|
||||
expect(request.headers.conversation_id).toBe('conversation-123');
|
||||
expect(request.headers['x-codex-turn-state']).toBe('turn-state');
|
||||
expect(request.headers['x-codex-turn-metadata']).toBe('turn-metadata');
|
||||
|
||||
expect(request.headers.origin).toBeUndefined();
|
||||
expect(request.headers.referer).toBeUndefined();
|
||||
expect(request.headers['x-forwarded-for']).toBeUndefined();
|
||||
expect(request.headers['x-real-ip']).toBeUndefined();
|
||||
expect(request.headers.version).toBeUndefined();
|
||||
expect(request.headers['x-test-header']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('preserves codex compatibility headers while stripping browser and ip passthrough headers', () => {
|
||||
const request = buildUpstreamEndpointRequest({
|
||||
endpoint: 'responses',
|
||||
modelName: 'gpt-5.2-codex',
|
||||
stream: false,
|
||||
tokenValue: 'oauth-access-token',
|
||||
oauthProvider: 'codex',
|
||||
sitePlatform: 'codex',
|
||||
siteUrl: 'https://chatgpt.com/backend-api/codex',
|
||||
openaiBody: {
|
||||
model: 'gpt-5.2-codex',
|
||||
input: 'hello codex',
|
||||
},
|
||||
downstreamFormat: 'openai',
|
||||
downstreamHeaders: {
|
||||
'user-agent': 'OpenClaw/1.0',
|
||||
version: '0.202.0',
|
||||
session_id: 'session-from-client',
|
||||
'x-responsesapi-include-timing-metrics': '1',
|
||||
origin: 'https://openclaw.example',
|
||||
referer: 'https://openclaw.example/app',
|
||||
'x-forwarded-for': '203.0.113.1',
|
||||
'x-real-ip': '203.0.113.2',
|
||||
},
|
||||
providerHeaders: {
|
||||
Originator: 'codex_cli_rs',
|
||||
},
|
||||
} as any);
|
||||
|
||||
expect(request.headers.Version).toBe('0.202.0');
|
||||
expect(request.headers.Session_id).toBe('session-from-client');
|
||||
expect(request.headers['User-Agent']).toBe('OpenClaw/1.0');
|
||||
expect(request.headers['x-responsesapi-include-timing-metrics']).toBe('1');
|
||||
expect(request.headers.origin).toBeUndefined();
|
||||
expect(request.headers.referer).toBeUndefined();
|
||||
expect(request.headers['x-forwarded-for']).toBeUndefined();
|
||||
expect(request.headers['x-real-ip']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('builds codex responses requests against backend-api path and preserves oauth provider headers', () => {
|
||||
const request = buildUpstreamEndpointRequest({
|
||||
endpoint: 'responses',
|
||||
|
||||
@@ -115,6 +115,20 @@ const BLOCKED_PASSTHROUGH_HEADERS = new Set([
|
||||
'sec-websocket-version',
|
||||
'sec-websocket-extensions',
|
||||
]);
|
||||
const GENERIC_PASSTHROUGH_ALLOWED_HEADERS = new Set([
|
||||
'accept',
|
||||
'accept-language',
|
||||
'conversation-id',
|
||||
'conversation_id',
|
||||
'openai-beta',
|
||||
'originator',
|
||||
'session-id',
|
||||
'session_id',
|
||||
'user-agent',
|
||||
'x-codex-beta-features',
|
||||
'x-codex-turn-metadata',
|
||||
'x-codex-turn-state',
|
||||
]);
|
||||
const METAPI_INTERNAL_HEADER_BLOCKLIST = new Set([
|
||||
'x-metapi-tester-request',
|
||||
'x-metapi-tester-forced-channel-id',
|
||||
@@ -128,6 +142,7 @@ function shouldSkipPassthroughHeader(key: string): boolean {
|
||||
if (HOP_BY_HOP_HEADERS.has(key) || BLOCKED_PASSTHROUGH_HEADERS.has(key)) return true;
|
||||
if (METAPI_INTERNAL_HEADER_BLOCKLIST.has(key)) return true;
|
||||
if (key.startsWith('x-metapi-')) return true;
|
||||
if (!GENERIC_PASSTHROUGH_ALLOWED_HEADERS.has(key)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -197,6 +212,28 @@ function extractResponsesPassthroughHeaders(
|
||||
return forwarded;
|
||||
}
|
||||
|
||||
function extractCodexPassthroughHeaders(
|
||||
headers?: Record<string, unknown>,
|
||||
): Record<string, string> {
|
||||
if (!headers) return {};
|
||||
|
||||
const forwarded: Record<string, string> = {};
|
||||
for (const [rawKey, rawValue] of Object.entries(headers)) {
|
||||
const key = rawKey.toLowerCase();
|
||||
const shouldForward = (
|
||||
key === 'version'
|
||||
|| key === 'x-responsesapi-include-timing-metrics'
|
||||
);
|
||||
if (!shouldForward) continue;
|
||||
|
||||
const value = headerValueToString(rawValue);
|
||||
if (!value) continue;
|
||||
forwarded[key] = value;
|
||||
}
|
||||
|
||||
return forwarded;
|
||||
}
|
||||
|
||||
function extractClaudeBetasFromBody(body: Record<string, unknown>): {
|
||||
body: Record<string, unknown>;
|
||||
betas: string[];
|
||||
@@ -725,8 +762,12 @@ export function buildUpstreamEndpointRequest(input: {
|
||||
};
|
||||
|
||||
const passthroughHeaders = extractSafePassthroughHeaders(input.downstreamHeaders);
|
||||
const codexPassthroughHeaders = sitePlatform === 'codex'
|
||||
? extractCodexPassthroughHeaders(input.downstreamHeaders)
|
||||
: {};
|
||||
const commonHeaders: Record<string, string> = {
|
||||
...passthroughHeaders,
|
||||
...codexPassthroughHeaders,
|
||||
'Content-Type': 'application/json',
|
||||
...(input.providerHeaders || {}),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user