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
3 changes: 2 additions & 1 deletion containers/api-proxy/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ USER apiproxy
# 10000 - OpenAI API proxy (also serves as health check endpoint)
# 10001 - Anthropic API proxy
# 10002 - GitHub Copilot API proxy
EXPOSE 10000 10001 10002
# 10004 - OpenCode API proxy (routes to Anthropic)
EXPOSE 10000 10001 10002 10004

# Redirect stdout/stderr to log file for persistence
# Use shell form to enable redirection and tee for both file and console
Expand Down
29 changes: 29 additions & 0 deletions containers/api-proxy/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,35 @@
console.log('[API Proxy] GitHub Copilot proxy listening on port 10002');
});
}

// OpenCode API proxy (port 10004) — routes to Anthropic (default BYOK provider)
// OpenCode gets a separate port from Claude (10001) for per-engine rate limiting,
// metrics isolation, and future provider routing (OpenCode is BYOK and may route
// to different providers in the future based on model prefix).
if (ANTHROPIC_API_KEY) {
const opencodeServer = 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: 'opencode-proxy' }));
return;
}

const logMethod = sanitizeForLog(req.method);
const logUrl = sanitizeForLog(req.url);
console.log(`[OpenCode Proxy] ${logMethod} ${logUrl}`);

Check warning

Code scanning / CodeQL

Log injection Medium

Log entry depends on a
user-provided value
.

Copilot Autofix

AI 1 day ago

In general, to fix log injection, all user-controlled data should be sanitized before logging: remove or normalize line breaks and control characters, optionally collapse or escape other whitespace that could visually obscure boundaries, and clearly delimit user input in log messages so operators can see what part is untrusted.

For this specific code, the best low-impact fix is:

  1. Enhance sanitizeForLog to also strip Unicode line/paragraph separators and normalize tabs to spaces, while still removing ASCII control chars and limiting length to 200 characters.
  2. Keep using sanitizeForLog on req.method and req.url as already done.
  3. Slightly adjust the log format to clearly bracket user-controlled fields, e.g. "[OpenCode Proxy] method=%s url=%s" where the method and URL are enclosed in quotes. This doesn’t change functionality but makes the boundary between fixed text and user data explicit.

Concretely in containers/api-proxy/server.js:

  • Modify the sanitizeForLog implementation around lines 38–43 to:
    • Convert input to string safely.
    • Remove ASCII control chars as before.
    • Remove Unicode line (\u2028) and paragraph (\u2029) separators.
    • Replace tab characters with a single space.
  • Update the logging line around 276 to include clear delimiters around the sanitized values, e.g.:
    • From: console.log(`[OpenCode Proxy] ${logMethod} ${logUrl}`);
    • To: console.log(`[OpenCode Proxy] method="${logMethod}" url="${logUrl}"`);

No new imports are needed; all changes are inside the already-present helper and log call.


Suggested changeset 1
containers/api-proxy/server.js

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/containers/api-proxy/server.js b/containers/api-proxy/server.js
--- a/containers/api-proxy/server.js
+++ b/containers/api-proxy/server.js
@@ -37,9 +37,16 @@
 
 /** Sanitize a string for safe logging (strip control chars, limit length). */
 function sanitizeForLog(str) {
-  if (typeof str !== 'string') return '';
+  if (str == null) return '';
+  const s = String(str);
+  // Remove ASCII control chars (including \n, \r, \t, etc.) and DEL
   // eslint-disable-next-line no-control-regex
-  return str.replace(/[\x00-\x1f\x7f]/g, '').slice(0, 200);
+  const noAsciiControls = s.replace(/[\x00-\x1f\x7f]/g, '');
+  // Also remove Unicode line and paragraph separators to prevent multi-line logs
+  const noUnicodeSeparators = noAsciiControls.replace(/[\u2028\u2029]/g, '');
+  // Normalize any remaining tabs to a single space for readability
+  const normalizedWhitespace = noUnicodeSeparators.replace(/\t+/g, ' ');
+  return normalizedWhitespace.slice(0, 200);
 }
 
 // Read API keys from environment (set by docker-compose)
@@ -273,7 +279,7 @@
 
     const logMethod = sanitizeForLog(req.method);
     const logUrl = sanitizeForLog(req.url);
-    console.log(`[OpenCode Proxy] ${logMethod} ${logUrl}`);
+    console.log(`[OpenCode Proxy] method="${logMethod}" url="${logUrl}"`);
     console.log('[OpenCode Proxy] Injecting x-api-key header with ANTHROPIC_API_KEY');
     const anthropicHeaders = { 'x-api-key': ANTHROPIC_API_KEY };
     if (!req.headers['anthropic-version']) {
EOF
@@ -37,9 +37,16 @@

/** Sanitize a string for safe logging (strip control chars, limit length). */
function sanitizeForLog(str) {
if (typeof str !== 'string') return '';
if (str == null) return '';
const s = String(str);
// Remove ASCII control chars (including \n, \r, \t, etc.) and DEL
// eslint-disable-next-line no-control-regex
return str.replace(/[\x00-\x1f\x7f]/g, '').slice(0, 200);
const noAsciiControls = s.replace(/[\x00-\x1f\x7f]/g, '');
// Also remove Unicode line and paragraph separators to prevent multi-line logs
const noUnicodeSeparators = noAsciiControls.replace(/[\u2028\u2029]/g, '');
// Normalize any remaining tabs to a single space for readability
const normalizedWhitespace = noUnicodeSeparators.replace(/\t+/g, ' ');
return normalizedWhitespace.slice(0, 200);
}

// Read API keys from environment (set by docker-compose)
@@ -273,7 +279,7 @@

const logMethod = sanitizeForLog(req.method);
const logUrl = sanitizeForLog(req.url);
console.log(`[OpenCode Proxy] ${logMethod} ${logUrl}`);
console.log(`[OpenCode Proxy] method="${logMethod}" url="${logUrl}"`);
console.log('[OpenCode Proxy] Injecting x-api-key header with ANTHROPIC_API_KEY');
const anthropicHeaders = { 'x-api-key': ANTHROPIC_API_KEY };
if (!req.headers['anthropic-version']) {
Copilot is powered by AI and may make mistakes. Always verify output.
console.log('[OpenCode Proxy] Injecting x-api-key header with ANTHROPIC_API_KEY');
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);
});

opencodeServer.listen(10004, '0.0.0.0', () => {
console.log('[API Proxy] OpenCode proxy listening on port 10004 (-> Anthropic)');
});
}

// Graceful shutdown
process.on('SIGTERM', () => {
console.log('[API Proxy] Received SIGTERM, shutting down gracefully...');
Expand Down
7 changes: 4 additions & 3 deletions src/host-iptables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -443,11 +443,12 @@ export async function setupHostIptables(squidIp: string, squidPort: number, dnsS
]);

// 5b. Allow traffic to API proxy sidecar (when enabled)
// Allow all API proxy ports (OpenAI, Anthropic, GitHub Copilot).
// Allow all API proxy ports (OpenAI, Anthropic, GitHub Copilot, OpenCode).
// The sidecar itself routes through Squid, so domain whitelisting is still enforced.
if (apiProxyIp) {
const minPort = Math.min(API_PROXY_PORTS.OPENAI, API_PROXY_PORTS.ANTHROPIC, API_PROXY_PORTS.COPILOT);
const maxPort = Math.max(API_PROXY_PORTS.OPENAI, API_PROXY_PORTS.ANTHROPIC, API_PROXY_PORTS.COPILOT);
const allPorts = Object.values(API_PROXY_PORTS);
const minPort = Math.min(...allPorts);
const maxPort = Math.max(...allPorts);
logger.debug(`Allowing traffic to API proxy sidecar at ${apiProxyIp}:${minPort}-${maxPort}`);
await execa('iptables', [
'-t', 'filter', '-A', CHAIN_NAME,
Expand Down
8 changes: 8 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ export const API_PROXY_PORTS = {
* @see containers/api-proxy/server.js
*/
COPILOT: 10002,

/**
* OpenCode API proxy port (routes to Anthropic by default)
* OpenCode is BYOK — defaults to Anthropic as the primary provider
* @see containers/api-proxy/server.js
*/
OPENCODE: 10004,
} as const;

/**
Expand Down Expand Up @@ -432,6 +439,7 @@ export interface WrapperConfig {
* - http://api-proxy:10000 - OpenAI API proxy (for Codex) {@link API_PROXY_PORTS.OPENAI}
* - http://api-proxy:10001 - Anthropic API proxy (for Claude) {@link API_PROXY_PORTS.ANTHROPIC}
* - http://api-proxy:10002 - GitHub Copilot API proxy {@link API_PROXY_PORTS.COPILOT}
* - http://api-proxy:10004 - OpenCode API proxy (routes to Anthropic) {@link API_PROXY_PORTS.OPENCODE}
*
* When the corresponding API key is provided, the following environment
* variables are set in the agent container:
Expand Down
Loading