Merge pull request #923 from DaydreamCoding/feature/fix_api_auth

fix(auth): 修复客户端限制绕过漏洞,添加路径白名单检查
This commit is contained in:
Wesley Liddick
2026-01-24 20:21:52 +08:00
committed by GitHub
2 changed files with 96 additions and 22 deletions

View File

@@ -1,6 +1,10 @@
/** /**
* 客户端定义配置 * 客户端定义配置
* 定义所有支持的客户端类型和它们的属性 * 定义所有支持的客户端类型和它们的属性
*
* allowedPathPrefixes: 允许访问的路径前缀白名单
* - 当启用客户端限制时,只有匹配白名单的路径才允许访问
* - 防止通过其他兼容端点(如 /v1/chat/completions绕过客户端限制
*/ */
const CLIENT_DEFINITIONS = { const CLIENT_DEFINITIONS = {
@@ -9,7 +13,27 @@ const CLIENT_DEFINITIONS = {
name: 'Claude Code', name: 'Claude Code',
displayName: 'Claude Code CLI', displayName: 'Claude Code CLI',
description: 'Claude Code command-line interface', description: 'Claude Code command-line interface',
icon: '🤖' icon: '🤖',
// Claude Code 仅允许访问 Claude 原生端点,禁止访问 OpenAI 兼容端点
allowedPathPrefixes: [
'/api/v1/messages',
'/api/v1/models',
'/api/v1/me',
'/api/v1/usage',
'/api/v1/key-info',
'/api/v1/organizations',
'/claude/v1/messages',
'/claude/v1/models',
'/antigravity/api/',
'/gemini-cli/api/',
'/api/event_logging',
'/v1/messages',
'/v1/models',
'/v1/me',
'/v1/usage',
'/v1/key-info',
'/v1/organizations'
]
}, },
GEMINI_CLI: { GEMINI_CLI: {
@@ -17,7 +41,9 @@ const CLIENT_DEFINITIONS = {
name: 'Gemini CLI', name: 'Gemini CLI',
displayName: 'Gemini Command Line Tool', displayName: 'Gemini Command Line Tool',
description: 'Google Gemini API command-line interface', description: 'Google Gemini API command-line interface',
icon: '💎' icon: '💎',
// Gemini CLI 仅允许访问 Gemini 端点
allowedPathPrefixes: ['/gemini/']
}, },
CODEX_CLI: { CODEX_CLI: {
@@ -25,7 +51,9 @@ const CLIENT_DEFINITIONS = {
name: 'Codex CLI', name: 'Codex CLI',
displayName: 'Codex Command Line Tool', displayName: 'Codex Command Line Tool',
description: 'Cursor/Codex command-line interface', description: 'Cursor/Codex command-line interface',
icon: '🔷' icon: '🔷',
// Codex CLI 仅允许访问 OpenAI Responses 和 Azure 端点
allowedPathPrefixes: ['/openai/responses', '/openai/v1/responses', '/azure/']
}, },
DROID_CLI: { DROID_CLI: {
@@ -33,7 +61,9 @@ const CLIENT_DEFINITIONS = {
name: 'Droid CLI', name: 'Droid CLI',
displayName: 'Factory Droid CLI', displayName: 'Factory Droid CLI',
description: 'Factory Droid platform command-line interface', description: 'Factory Droid platform command-line interface',
icon: '🤖' icon: '🤖',
// Droid CLI 仅允许访问 Droid 端点
allowedPathPrefixes: ['/droid/']
} }
} }
@@ -60,10 +90,34 @@ function isValidClientId(clientId) {
return Object.values(CLIENT_IDS).includes(clientId) return Object.values(CLIENT_IDS).includes(clientId)
} }
/**
* 检查路径是否允许指定客户端访问
* @param {string} clientId - 客户端ID
* @param {string} path - 请求路径 (originalUrl 或 path)
* @returns {boolean} 是否允许
*/
function isPathAllowedForClient(clientId, path) {
const definition = getClientDefinitionById(clientId)
if (!definition) {
return false
}
// 如果没有定义 allowedPathPrefixes则不限制路径向后兼容
if (!definition.allowedPathPrefixes || definition.allowedPathPrefixes.length === 0) {
return true
}
const normalizedPath = (path || '').toLowerCase()
return definition.allowedPathPrefixes.some((prefix) =>
normalizedPath.startsWith(prefix.toLowerCase())
)
}
module.exports = { module.exports = {
CLIENT_DEFINITIONS, CLIENT_DEFINITIONS,
CLIENT_IDS, CLIENT_IDS,
getAllClientDefinitions, getAllClientDefinitions,
getClientDefinitionById, getClientDefinitionById,
isValidClientId isValidClientId,
isPathAllowedForClient
} }

View File

@@ -4,12 +4,25 @@
*/ */
const logger = require('../utils/logger') const logger = require('../utils/logger')
const { CLIENT_DEFINITIONS, getAllClientDefinitions } = require('./clientDefinitions') const {
CLIENT_IDS,
getAllClientDefinitions,
getClientDefinitionById,
isPathAllowedForClient
} = require('./clientDefinitions')
const ClaudeCodeValidator = require('./clients/claudeCodeValidator') const ClaudeCodeValidator = require('./clients/claudeCodeValidator')
const GeminiCliValidator = require('./clients/geminiCliValidator') const GeminiCliValidator = require('./clients/geminiCliValidator')
const CodexCliValidator = require('./clients/codexCliValidator') const CodexCliValidator = require('./clients/codexCliValidator')
const DroidCliValidator = require('./clients/droidCliValidator') const DroidCliValidator = require('./clients/droidCliValidator')
// 客户端ID到验证器的映射表
const VALIDATOR_MAP = {
[CLIENT_IDS.CLAUDE_CODE]: ClaudeCodeValidator,
[CLIENT_IDS.GEMINI_CLI]: GeminiCliValidator,
[CLIENT_IDS.CODEX_CLI]: CodexCliValidator,
[CLIENT_IDS.DROID_CLI]: DroidCliValidator
}
/** /**
* 客户端验证器类 * 客户端验证器类
*/ */
@@ -20,19 +33,12 @@ class ClientValidator {
* @returns {Object|null} 验证器实例 * @returns {Object|null} 验证器实例
*/ */
static getValidator(clientId) { static getValidator(clientId) {
switch (clientId) { const validator = VALIDATOR_MAP[clientId]
case 'claude_code': if (!validator) {
return ClaudeCodeValidator logger.warn(`Unknown client ID: ${clientId}`)
case 'gemini_cli': return null
return GeminiCliValidator
case 'codex_cli':
return CodexCliValidator
case 'droid_cli':
return DroidCliValidator
default:
logger.warn(`Unknown client ID: ${clientId}`)
return null
} }
return validator
} }
/** /**
@@ -40,7 +46,7 @@ class ClientValidator {
* @returns {Array<string>} 客户端ID列表 * @returns {Array<string>} 客户端ID列表
*/ */
static getSupportedClients() { static getSupportedClients() {
return ['claude_code', 'gemini_cli', 'codex_cli', 'droid_cli'] return Object.keys(VALIDATOR_MAP)
} }
/** /**
@@ -67,6 +73,7 @@ class ClientValidator {
/** /**
* 验证请求是否来自允许的客户端列表中的任一客户端 * 验证请求是否来自允许的客户端列表中的任一客户端
* 包含路径白名单检查,防止通过其他兼容端点绕过客户端限制
* @param {Array<string>} allowedClients - 允许的客户端ID列表 * @param {Array<string>} allowedClients - 允许的客户端ID列表
* @param {Object} req - Express请求对象 * @param {Object} req - Express请求对象
* @returns {Object} 验证结果对象 * @returns {Object} 验证结果对象
@@ -74,10 +81,12 @@ class ClientValidator {
static validateRequest(allowedClients, req) { static validateRequest(allowedClients, req) {
const userAgent = req.headers['user-agent'] || '' const userAgent = req.headers['user-agent'] || ''
const clientIP = req.ip || req.connection?.remoteAddress || 'unknown' const clientIP = req.ip || req.connection?.remoteAddress || 'unknown'
const requestPath = req.originalUrl || req.path || ''
// 记录验证开始 // 记录验证开始
logger.api(`🔍 Starting client validation for User-Agent: "${userAgent}"`) logger.api(`🔍 Starting client validation for User-Agent: "${userAgent}"`)
logger.api(` Allowed clients: ${allowedClients.join(', ')}`) logger.api(` Allowed clients: ${allowedClients.join(', ')}`)
logger.api(` Request path: ${requestPath}`)
logger.api(` Request from IP: ${clientIP}`) logger.api(` Request from IP: ${clientIP}`)
// 遍历所有允许的客户端进行验证 // 遍历所有允许的客户端进行验证
@@ -89,6 +98,12 @@ class ClientValidator {
continue continue
} }
// 路径白名单检查:先检查路径是否允许该客户端访问
if (!isPathAllowedForClient(clientId, requestPath)) {
logger.debug(`Path "${requestPath}" not allowed for ${validator.getName()}, skipping`)
continue
}
logger.debug(`Checking against ${validator.getName()}...`) logger.debug(`Checking against ${validator.getName()}...`)
try { try {
@@ -96,12 +111,13 @@ class ClientValidator {
// 验证成功 // 验证成功
logger.api(`✅ Client validated: ${validator.getName()} (${clientId})`) logger.api(`✅ Client validated: ${validator.getName()} (${clientId})`)
logger.api(` Matched User-Agent: "${userAgent}"`) logger.api(` Matched User-Agent: "${userAgent}"`)
logger.api(` Allowed path: "${requestPath}"`)
return { return {
allowed: true, allowed: true,
matchedClient: clientId, matchedClient: clientId,
clientName: validator.getName(), clientName: validator.getName(),
clientInfo: Object.values(CLIENT_DEFINITIONS).find((def) => def.id === clientId) clientInfo: getClientDefinitionById(clientId)
} }
} }
} catch (error) { } catch (error) {
@@ -111,11 +127,15 @@ class ClientValidator {
} }
// 没有匹配的客户端 // 没有匹配的客户端
logger.api(`❌ No matching client found for User-Agent: "${userAgent}"`) logger.api(
`❌ No matching client found for User-Agent: "${userAgent}" and path: "${requestPath}"`
)
return { return {
allowed: false, allowed: false,
matchedClient: null, matchedClient: null,
reason: 'No matching client found' reason: 'No matching client found or path not allowed',
userAgent,
requestPath
} }
} }