-
Notifications
You must be signed in to change notification settings - Fork 30
fix: fix API proxy sidecar bugs preventing Anthropic-only usage #843
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
87f7028
0452e84
e3a3bd4
4fcfa7d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -10,19 +10,46 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * 4. Respects domain whitelisting enforced by Squid | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const express = require('express'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { createProxyMiddleware } = require('http-proxy-middleware'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const http = require('http'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const https = require('https'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { URL } = require('url'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { HttpsProxyAgent } = require('https-proxy-agent'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Max request body size (10 MB) to prevent DoS via large payloads | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const MAX_BODY_SIZE = 10 * 1024 * 1024; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Headers that must never be forwarded from the client. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // The proxy controls authentication — client-supplied auth/proxy headers are stripped. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const STRIPPED_HEADERS = new Set([ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'host', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'authorization', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'proxy-authorization', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'x-api-key', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'forwarded', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'via', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** Returns true if the header name should be stripped (case-insensitive). */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function shouldStripHeader(name) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const lower = name.toLowerCase(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return STRIPPED_HEADERS.has(lower) || lower.startsWith('x-forwarded-'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** Sanitize a string for safe logging (strip control chars, limit length). */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function sanitizeForLog(str) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (typeof str !== 'string') return ''; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // eslint-disable-next-line no-control-regex | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return str.replace(/[\x00-\x1f\x7f]/g, '').slice(0, 200); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Read API keys from environment (set by docker-compose) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const OPENAI_API_KEY = process.env.OPENAI_API_KEY; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Squid proxy configuration (set via HTTP_PROXY/HTTPS_PROXY in docker-compose) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const HTTP_PROXY = process.env.HTTP_PROXY; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const HTTPS_PROXY = process.env.HTTPS_PROXY; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const HTTPS_PROXY = process.env.HTTPS_PROXY || process.env.HTTP_PROXY; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log('[API Proxy] Starting AWF API proxy sidecar...'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log(`[API Proxy] HTTP_PROXY: ${HTTP_PROXY}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log(`[API Proxy] HTTPS_PROXY: ${HTTPS_PROXY}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (OPENAI_API_KEY) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log('[API Proxy] OpenAI API key configured'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -31,72 +58,174 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log('[API Proxy] Anthropic API key configured'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Create Express app | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const app = express(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Health check endpoint | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| app.get('/health', (req, res) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| res.status(200).json({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| status: 'healthy', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| service: 'awf-api-proxy', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| squid_proxy: HTTP_PROXY || 'not configured', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| providers: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| openai: !!OPENAI_API_KEY, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| anthropic: !!ANTHROPIC_API_KEY | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Create proxy agent for routing through Squid | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const proxyAgent = HTTPS_PROXY ? new HttpsProxyAgent(HTTPS_PROXY) : undefined; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!proxyAgent) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.warn('[API Proxy] WARNING: No HTTPS_PROXY configured, requests will go direct'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Forward a request to the target API, injecting auth headers and routing through Squid. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function proxyRequest(req, res, targetHost, injectHeaders) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Validate that req.url is a relative path (prevent open-redirect / SSRF) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!req.url || !req.url.startsWith('/')) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| res.writeHead(400, { 'Content-Type': 'application/json' }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| res.end(JSON.stringify({ error: 'Bad Request', message: 'URL must be a relative path' })); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Build target URL | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const targetUrl = new URL(req.url, `https://${targetHost}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Handle client-side errors (e.g. aborted connections) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| req.on('error', (err) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error(`[API Proxy] Client request error: ${sanitizeForLog(err.message)}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!res.headersSent) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| res.writeHead(400, { 'Content-Type': 'application/json' }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| res.end(JSON.stringify({ error: 'Client error', message: err.message })); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Read the request body with size limit | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const chunks = []; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let totalBytes = 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let rejected = false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| req.on('data', chunk => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (rejected) return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| totalBytes += chunk.length; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (totalBytes > MAX_BODY_SIZE) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| rejected = true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!res.headersSent) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| res.writeHead(413, { 'Content-Type': 'application/json' }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| res.end(JSON.stringify({ error: 'Payload Too Large', message: 'Request body exceeds 10 MB limit' })); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| chunks.push(chunk); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| req.on('end', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (rejected) return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const body = Buffer.concat(chunks); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Copy incoming headers, stripping sensitive/proxy headers, then inject auth | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const headers = {}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (const [name, value] of Object.entries(req.headers)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!shouldStripHeader(name)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| headers[name] = value; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Object.assign(headers, injectHeaders); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const options = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| hostname: targetHost, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| port: 443, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| path: targetUrl.pathname + targetUrl.search, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| method: req.method, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| headers, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| agent: proxyAgent, // Route through Squid | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const proxyReq = https.request(options, (proxyRes) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Handle response stream errors | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| proxyRes.on('error', (err) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error(`[API Proxy] Response stream error from ${targetHost}: ${sanitizeForLog(err.message)}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!res.headersSent) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| res.writeHead(502, { 'Content-Type': 'application/json' }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| res.end(JSON.stringify({ error: 'Response stream error', message: err.message })); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| res.writeHead(proxyRes.statusCode, proxyRes.headers); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| proxyRes.pipe(res); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| proxyReq.on('error', (err) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error(`[API Proxy] Error proxying to ${targetHost}: ${sanitizeForLog(err.message)}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Check warningCode scanning / CodeQL Log injection Medium
Log entry depends on a
user-provided value Error loading related location Loading
Copilot AutofixAI 4 months ago General approach: Ensure user-influenced strings are strictly normalized before logging. For plain-text logs, remove all control characters (including newlines and carriage returns) and limit the maximum length. Optionally, encode such values so their boundaries are clear in the log entry. Best fix here: strengthen Concretely:
Suggested changeset
1
containers/api-proxy/server.js
Copilot is powered by AI and may make mistakes. Always verify output.
Refresh and try again.
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!res.headersSent) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| res.writeHead(502, { 'Content-Type': 'application/json' }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| res.end(JSON.stringify({ error: 'Proxy error', message: err.message })); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (body.length > 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| proxyReq.write(body); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| proxyReq.end(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+70
to
+158
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Health port is always 10000 — this is what Docker healthcheck hits | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const HEALTH_PORT = 10000; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // OpenAI API proxy (port 10000) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (OPENAI_API_KEY) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| app.use(createProxyMiddleware({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| target: 'https://api.openai.com', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| changeOrigin: true, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| secure: true, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onProxyReq: (proxyReq, req, res) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Inject Authorization header | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| proxyReq.setHeader('Authorization', `Bearer ${OPENAI_API_KEY}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log(`[OpenAI Proxy] ${req.method} ${req.url}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onError: (err, req, res) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error(`[OpenAI Proxy] Error: ${err.message}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| res.status(502).json({ error: 'Proxy error', message: err.message }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const server = http.createServer((req, res) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (req.url === '/health' && req.method === 'GET') { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| res.writeHead(200, { 'Content-Type': 'application/json' }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| res.end(JSON.stringify({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| status: 'healthy', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| service: 'awf-api-proxy', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| squid_proxy: HTTPS_PROXY || 'not configured', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| providers: { openai: true, anthropic: !!ANTHROPIC_API_KEY }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| })); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log(`[OpenAI Proxy] ${sanitizeForLog(req.method)} ${sanitizeForLog(req.url)}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Check warningCode scanning / CodeQL Log injection Medium
Log entry depends on a
user-provided value Error loading related location Loading
Copilot AutofixAI 4 months ago In general, to fix log injection you must ensure any user-controlled value is sanitized before logging: remove or neutralize newline and carriage-return characters (and other control characters if desired), limit the length, and clearly delimit user input so forged structure is obvious. For HTML-rendered logs, you would also HTML-encode the content. For this specific code, the best fix is to improve Concretely:
No new imports or external libraries are needed; we simply adjust the existing helper function.
Suggested changeset
1
containers/api-proxy/server.js
Copilot is powered by AI and may make mistakes. Always verify output.
Refresh and try again.
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| proxyRequest(req, res, 'api.openai.com', { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'Authorization': `Bearer ${OPENAI_API_KEY}`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| server.listen(HEALTH_PORT, '0.0.0.0', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log(`[API Proxy] OpenAI proxy listening on port ${HEALTH_PORT}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // No OpenAI key — still need a health endpoint on port 10000 for Docker healthcheck | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const server = http.createServer((req, res) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (req.url === '/health' && req.method === 'GET') { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| res.writeHead(200, { 'Content-Type': 'application/json' }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| res.end(JSON.stringify({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| status: 'healthy', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| service: 'awf-api-proxy', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| squid_proxy: HTTPS_PROXY || 'not configured', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| providers: { openai: false, anthropic: !!ANTHROPIC_API_KEY }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| })); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| })); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| app.listen(10000, '0.0.0.0', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log('[API Proxy] OpenAI proxy listening on port 10000'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log('[API Proxy] Routing through Squid to api.openai.com'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| res.writeHead(404, { 'Content-Type': 'application/json' }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| res.end(JSON.stringify({ error: 'OpenAI proxy not configured (no OPENAI_API_KEY)' })); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| server.listen(HEALTH_PORT, '0.0.0.0', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log(`[API Proxy] Health endpoint listening on port ${HEALTH_PORT} (OpenAI not configured)`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Anthropic API proxy (port 10001) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (ANTHROPIC_API_KEY) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const anthropicApp = express(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| anthropicApp.get('/health', (req, res) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| res.status(200).json({ status: 'healthy', service: 'anthropic-proxy' }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const server = http.createServer((req, res) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (req.url === '/health' && req.method === 'GET') { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| res.writeHead(200, { 'Content-Type': 'application/json' }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| res.end(JSON.stringify({ status: 'healthy', service: 'anthropic-proxy' })); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| anthropicApp.use(createProxyMiddleware({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| target: 'https://api.anthropic.com', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| changeOrigin: true, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| secure: true, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onProxyReq: (proxyReq, req, res) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Inject Anthropic authentication headers | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| proxyReq.setHeader('x-api-key', ANTHROPIC_API_KEY); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| proxyReq.setHeader('anthropic-version', '2023-06-01'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log(`[Anthropic Proxy] ${req.method} ${req.url}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onError: (err, req, res) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error(`[Anthropic Proxy] Error: ${err.message}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| res.status(502).json({ error: 'Proxy error', message: err.message }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log(`[Anthropic Proxy] ${sanitizeForLog(req.method)} ${sanitizeForLog(req.url)}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Check warningCode scanning / CodeQL Log injection Medium
Log entry depends on a
user-provided value Error loading related location Loading
Copilot AutofixAI 4 months ago In general, to fix log injection, any user-controlled value being logged should be normalized so it cannot introduce line breaks or other control characters, should be length-limited, and should be clearly delimited so it cannot be mistaken for system-generated log content. For this code, the best minimal fix is to (a) tighten Concretely, in
No additional imports are needed; we only adjust the existing helper.
Suggested changeset
1
containers/api-proxy/server.js
Copilot is powered by AI and may make mistakes. Always verify output.
Refresh and try again.
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Only set anthropic-version as default; preserve agent-provided version | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const anthropicHeaders = { 'x-api-key': ANTHROPIC_API_KEY }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!req.headers['anthropic-version']) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| anthropicHeaders['anthropic-version'] = '2023-06-01'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| })); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| proxyRequest(req, res, 'api.anthropic.com', anthropicHeaders); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| anthropicApp.listen(10001, '0.0.0.0', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| server.listen(10001, '0.0.0.0', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log('[API Proxy] Anthropic proxy listening on port 10001'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log('[API Proxy] Routing through Squid to api.anthropic.com'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The URL construction using
new URL(req.url, ...)could be vulnerable to path traversal or open redirect attacks if req.url contains an absolute URL or protocol-relative URL. An attacker could potentially send requests likehttp://evil.com/pathor//evil.com/pathto bypass the intended targetHost. Consider validating that req.url is a relative path starting with '/' before constructing the target URL, or use url.parse() to extract only the pathname and search components.