Skip to content

Commit 202c143

Browse files
committed
fix: tighten webhook auth and session parsing
1 parent 5a8dde8 commit 202c143

6 files changed

Lines changed: 158 additions & 10 deletions

File tree

cli.js

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ const HTTPS_KEEP_ALIVE_AGENT = new https.Agent({ keepAlive: true });
285285

286286
const openaiBridgeHandler = createOpenaiBridgeHttpHandler({
287287
settingsFile: OPENAI_BRIDGE_SETTINGS_FILE,
288-
expectedToken: 'codexmate',
288+
expectedToken: typeof process.env.CODEXMATE_HTTP_TOKEN === 'string' ? process.env.CODEXMATE_HTTP_TOKEN.trim() : '',
289289
maxBodySize: MAX_API_BODY_SIZE,
290290
httpAgent: HTTP_KEEP_ALIVE_AGENT,
291291
httpsAgent: HTTPS_KEEP_ALIVE_AGENT
@@ -7964,7 +7964,7 @@ function writeJsonResponse(res, statusCode, payload) {
79647964
function readJsonRequestBody(req, res, options = {}) {
79657965
const maxBytes = Number.isFinite(options.maxBytes) ? Math.max(1024, Math.floor(options.maxBytes)) : MAX_API_BODY_SIZE;
79667966
return new Promise((resolve) => {
7967-
let body = '';
7967+
const chunks = [];
79687968
let bodySize = 0;
79697969
let bodyTooLarge = false;
79707970
req.on('data', (chunk) => {
@@ -7979,12 +7979,14 @@ function readJsonRequestBody(req, res, options = {}) {
79797979
resolve({ ok: false, error: 'payload-too-large' });
79807980
return;
79817981
}
7982-
body += chunk;
7982+
chunks.push(chunk);
79837983
});
79847984
req.on('end', () => {
79857985
if (bodyTooLarge) return;
7986+
const rawBuffer = chunks.length ? Buffer.concat(chunks) : Buffer.alloc(0);
7987+
const rawText = rawBuffer.length ? rawBuffer.toString('utf-8') : '';
79867988
try {
7987-
resolve({ ok: true, body: JSON.parse(body || '{}') });
7989+
resolve({ ok: true, body: JSON.parse(rawText || '{}'), rawText, rawBuffer });
79887990
} catch (error) {
79897991
resolve({ ok: false, error: error && error.message ? error.message : 'invalid json' });
79907992
}
@@ -8064,6 +8066,25 @@ function rememberWebhookDeliveryId(value, ttlMs = 10 * 60 * 1000) {
80648066
return { ok: true, seen: false };
80658067
}
80668068

8069+
function safeTimingEqual(a, b) {
8070+
try {
8071+
const ba = Buffer.isBuffer(a) ? a : Buffer.from(String(a || ''), 'utf-8');
8072+
const bb = Buffer.isBuffer(b) ? b : Buffer.from(String(b || ''), 'utf-8');
8073+
if (ba.length !== bb.length) return false;
8074+
return crypto.timingSafeEqual(ba, bb);
8075+
} catch (_) {
8076+
return false;
8077+
}
8078+
}
8079+
8080+
function verifyGithubWebhookSignature(secret, signatureHeader, rawBuffer) {
8081+
const key = typeof secret === 'string' ? secret : '';
8082+
const signature = typeof signatureHeader === 'string' ? signatureHeader.trim() : '';
8083+
if (!key || !signature || !signature.startsWith('sha256=')) return false;
8084+
const expected = 'sha256=' + crypto.createHmac('sha256', key).update(rawBuffer || Buffer.alloc(0)).digest('hex');
8085+
return safeTimingEqual(signature, expected);
8086+
}
8087+
80678088
async function handleAutomationHook(req, res, source) {
80688089
const method = (req.method || 'GET').toUpperCase();
80698090
if (method !== 'POST') {
@@ -8085,6 +8106,42 @@ async function handleAutomationHook(req, res, source) {
80858106
}
80868107
return;
80878108
}
8109+
const remoteAddr = req && req.socket ? req.socket.remoteAddress : '';
8110+
const isLoopback = !remoteAddr || isLoopbackRemoteAddress(remoteAddr);
8111+
const normalizedSource = typeof source === 'string' ? source.trim().toLowerCase() : '';
8112+
if (normalizedSource === 'github') {
8113+
const secret = typeof process.env.CODEXMATE_GITHUB_WEBHOOK_SECRET === 'string'
8114+
? process.env.CODEXMATE_GITHUB_WEBHOOK_SECRET
8115+
: '';
8116+
if (!secret && !isLoopback) {
8117+
writeJsonResponse(res, 403, { error: 'Remote GitHub webhook is disabled (set CODEXMATE_GITHUB_WEBHOOK_SECRET)' });
8118+
return;
8119+
}
8120+
if (secret) {
8121+
const signature = (req.headers || {})['x-hub-signature-256'];
8122+
if (!verifyGithubWebhookSignature(secret, signature, parsedBody.rawBuffer)) {
8123+
writeJsonResponse(res, 401, { error: 'Invalid webhook signature' });
8124+
return;
8125+
}
8126+
}
8127+
} else if (normalizedSource === 'gitlab') {
8128+
const secret = typeof process.env.CODEXMATE_GITLAB_WEBHOOK_SECRET === 'string'
8129+
? process.env.CODEXMATE_GITLAB_WEBHOOK_SECRET.trim()
8130+
: '';
8131+
if (!secret && !isLoopback) {
8132+
writeJsonResponse(res, 403, { error: 'Remote GitLab webhook is disabled (set CODEXMATE_GITLAB_WEBHOOK_SECRET)' });
8133+
return;
8134+
}
8135+
if (secret) {
8136+
const tokenHeader = typeof (req.headers || {})['x-gitlab-token'] === 'string'
8137+
? String(req.headers['x-gitlab-token']).trim()
8138+
: '';
8139+
if (!tokenHeader || tokenHeader !== secret) {
8140+
writeJsonResponse(res, 401, { error: 'Invalid webhook token' });
8141+
return;
8142+
}
8143+
}
8144+
}
80888145
const payload = parsedBody.body && typeof parsedBody.body === 'object' ? parsedBody.body : {};
80898146
const eventKey = buildAutomationEventKey(source, req.headers || {}, payload);
80908147
if (!eventKey) {

cli/builtin-proxy.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -674,6 +674,41 @@ function createBuiltinProxyRuntimeController(deps = {}) {
674674
return;
675675
}
676676

677+
const remoteAddr = req && req.socket ? req.socket.remoteAddress : '';
678+
const isLoopback = !remoteAddr
679+
|| remoteAddr === '127.0.0.1'
680+
|| remoteAddr === '::1'
681+
|| remoteAddr === '::ffff:127.0.0.1';
682+
if (!isLoopback) {
683+
const expected = typeof process.env.CODEXMATE_HTTP_TOKEN === 'string'
684+
? process.env.CODEXMATE_HTTP_TOKEN.trim()
685+
: '';
686+
if (!expected) {
687+
const body = JSON.stringify({ error: 'Remote access is disabled (set CODEXMATE_HTTP_TOKEN)' });
688+
res.writeHead(403, {
689+
'Content-Type': 'application/json; charset=utf-8',
690+
'Content-Length': Buffer.byteLength(body, 'utf-8')
691+
});
692+
res.end(body, 'utf-8');
693+
return;
694+
}
695+
const headers = req && req.headers && typeof req.headers === 'object' ? req.headers : {};
696+
const rawAuth = typeof headers.authorization === 'string' ? headers.authorization.trim() : '';
697+
const match = rawAuth ? rawAuth.match(/^bearer\s+(.+)$/i) : null;
698+
const actual = match && match[1]
699+
? match[1].trim()
700+
: (rawAuth ? rawAuth : (typeof headers['x-codexmate-token'] === 'string' ? String(headers['x-codexmate-token']).trim() : ''));
701+
if (!actual || actual !== expected) {
702+
const body = JSON.stringify({ error: 'Unauthorized' });
703+
res.writeHead(401, {
704+
'Content-Type': 'application/json; charset=utf-8',
705+
'Content-Length': Buffer.byteLength(body, 'utf-8')
706+
});
707+
res.end(body, 'utf-8');
708+
return;
709+
}
710+
}
711+
677712
const incomingPath = parsedIncoming.pathname || '/';
678713
if (incomingPath === '/health' || incomingPath === '/status') {
679714
const body = JSON.stringify({

cli/claude-proxy.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -864,6 +864,30 @@ function createBuiltinClaudeProxyRuntimeController(deps = {}) {
864864
function createBuiltinClaudeProxyServer(settings, upstream) {
865865
const connections = new Set();
866866
const server = http.createServer((req, res) => {
867+
const remoteAddr = req && req.socket ? req.socket.remoteAddress : '';
868+
const isLoopback = !remoteAddr
869+
|| remoteAddr === '127.0.0.1'
870+
|| remoteAddr === '::1'
871+
|| remoteAddr === '::ffff:127.0.0.1';
872+
if (!isLoopback) {
873+
const expected = typeof process.env.CODEXMATE_HTTP_TOKEN === 'string'
874+
? process.env.CODEXMATE_HTTP_TOKEN.trim()
875+
: '';
876+
if (!expected) {
877+
writeAnthropicProxyError(res, 403, 'Remote access is disabled (set CODEXMATE_HTTP_TOKEN)', 'authentication_error');
878+
return;
879+
}
880+
const headers = req && req.headers && typeof req.headers === 'object' ? req.headers : {};
881+
const rawAuth = typeof headers.authorization === 'string' ? headers.authorization.trim() : '';
882+
const match = rawAuth ? rawAuth.match(/^bearer\s+(.+)$/i) : null;
883+
const actual = match && match[1]
884+
? match[1].trim()
885+
: (rawAuth ? rawAuth : (typeof headers['x-codexmate-token'] === 'string' ? String(headers['x-codexmate-token']).trim() : ''));
886+
if (!actual || actual !== expected) {
887+
writeAnthropicProxyError(res, 401, 'Unauthorized', 'authentication_error');
888+
return;
889+
}
890+
}
867891
handleBuiltinClaudeProxyRequest(req, res, settings, upstream).catch((err) => {
868892
if (res.headersSent) {
869893
try { res.destroy(err); } catch (_) {}

cli/openai-bridge.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -707,9 +707,10 @@ async function proxyRequestJson(targetUrl, options = {}) {
707707

708708
function createOpenaiBridgeHttpHandler(options = {}) {
709709
const settingsFile = options.settingsFile;
710-
const expectedToken = typeof options.expectedToken === 'string' && options.expectedToken.trim()
711-
? options.expectedToken.trim()
712-
: DEFAULT_BRIDGE_TOKEN;
710+
const expectedTokenRaw = typeof options.expectedToken === 'string' ? options.expectedToken.trim() : '';
711+
const expectedToken = Object.prototype.hasOwnProperty.call(options, 'expectedToken')
712+
? expectedTokenRaw
713+
: (expectedTokenRaw || DEFAULT_BRIDGE_TOKEN);
713714
const maxBodySize = Number.isFinite(options.maxBodySize) ? options.maxBodySize : 0;
714715
const httpAgent = options.httpAgent;
715716
const httpsAgent = options.httpsAgent;
@@ -748,6 +749,11 @@ function createOpenaiBridgeHttpHandler(options = {}) {
748749
// 为避免在 LAN 暴露无鉴权的代理,这里仅允许 loopback 连接缺省 token。
749750
const remoteAddr = req && req.socket ? req.socket.remoteAddress : '';
750751
const isLoopback = isLoopbackAddress(remoteAddr);
752+
if (!isLoopback && !expectedToken) {
753+
res.writeHead(403, { 'Content-Type': 'application/json; charset=utf-8' });
754+
res.end(JSON.stringify({ error: 'Remote access is disabled (set CODEXMATE_HTTP_TOKEN)' }));
755+
return;
756+
}
751757
if (!token && !isLoopback) {
752758
res.writeHead(401, { 'Content-Type': 'application/json; charset=utf-8' });
753759
res.end(JSON.stringify({ error: 'Unauthorized' }));

lib/cli-path-utils.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,16 @@ function resolveCopyTargetRoot(targetDir) {
5252
}
5353

5454
function commandExists(command, args = '') {
55+
const cmd = typeof command === 'string' ? command.trim() : '';
56+
const argText = typeof args === 'string' ? args.trim() : '';
57+
if (!cmd || !/^[A-Za-z0-9._-]+$/.test(cmd)) {
58+
return false;
59+
}
60+
if (argText && /[\r\n;&|<>`$]/.test(argText)) {
61+
return false;
62+
}
5563
try {
56-
execSync(`${command} ${args}`, { stdio: 'ignore', shell: process.platform === 'win32' });
64+
execSync(`${cmd}${argText ? ` ${argText}` : ''}`, { stdio: 'ignore', shell: process.platform === 'win32' });
5765
return true;
5866
} catch (e) {
5967
return false;
@@ -66,4 +74,3 @@ module.exports = {
6674
resolveCopyTargetRoot,
6775
commandExists
6876
};
69-

lib/cli-sessions.js

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,25 @@ function isBootstrapLikeText(text) {
3838
return false;
3939
}
4040

41-
return BOOTSTRAP_TEXT_MARKERS.some(marker => normalized.includes(marker));
41+
if (normalized.length < 80) {
42+
return false;
43+
}
44+
let hits = 0;
45+
for (const marker of BOOTSTRAP_TEXT_MARKERS) {
46+
if (normalized.includes(marker)) {
47+
hits += 1;
48+
}
49+
}
50+
if (hits >= 2) {
51+
return true;
52+
}
53+
if (normalized.includes('<environment_context>')) {
54+
return true;
55+
}
56+
if (normalized.includes('agents.md instructions')) {
57+
return true;
58+
}
59+
return false;
4260
}
4361

4462
function removeLeadingSystemMessage(messages) {
@@ -300,6 +318,7 @@ function extractSessionDetailPreviewFromTailText(text, source, messageLimit) {
300318
});
301319
}
302320

321+
state.messages = removeLeadingSystemMessage(state.messages);
303322
return state;
304323
}
305324

0 commit comments

Comments
 (0)