mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-27 01:47:46 +00:00
Merge pull request #923 from DaydreamCoding/feature/fix_api_auth
fix(auth): 修复客户端限制绕过漏洞,添加路径白名单检查
This commit is contained in:
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user