Compare commits

...

10 Commits

Author SHA1 Message Date
github-actions[bot]
0a59a0f9d4 chore: sync VERSION file with release v1.1.251 [skip ci] 2026-01-01 05:57:53 +00:00
Chapoly1305
c4448db6ab fix: 防止客户端断开连接时服务崩溃
当客户端在流式响应过程中断开连接时,catch 块尝试发送 JSON 错误响应
会触发 ERR_HTTP_HEADERS_SENT 错误,导致 unhandledRejection 使服务崩溃。

修复文件:
- src/routes/openaiClaudeRoutes.js
- src/routes/openaiGeminiRoutes.js

修复内容:
- 添加 res.headersSent 检查,避免在响应已发送后再次尝试发送
- 客户端断开连接使用 INFO 级别日志(不是 ERROR)
- 客户端断开使用 499 状态码 (Client Closed Request)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 01:18:06 -05:00
github-actions[bot]
a345812cd7 chore: sync VERSION file with release v1.1.250 [skip ci] 2025-12-29 05:46:39 +00:00
shaw
a0cbafd759 Merge branch 'fix-authenticateUserOrAdmin-bypass' 2025-12-29 13:45:44 +08:00
Wesley Liddick
3c64038fa7 Create SECURITY.md for security policy [skip ci]
Add a security policy document outlining supported versions and vulnerability reporting.
2025-12-29 13:37:15 +08:00
Junming Chen
45b81bd478 fix: 修复 authenticateUserOrAdmin 认证绕过漏洞
- 添加 username 和 loginTime 字段验证(与 authenticateAdmin 保持一致)
- 无效/伪造会话自动删除并记录安全日志
- 删除未使用的 id 字段(死代码清理)

漏洞详情:
- 位置:src/middleware/auth.js:1569-1581
- 原因:只检查 Object.keys(session).length > 0,未验证必须字段
- 影响:攻击者可通过注入最小会话 {foo:'bar'} 绕过认证

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 23:56:05 -05:00
github-actions[bot]
fc57133230 chore: sync VERSION file with release v1.1.249 [skip ci] 2025-12-26 11:26:14 +00:00
shaw
1f06af4a56 chore: trigger release [force release] 2025-12-26 19:25:53 +08:00
shaw
6165fad090 docs: 添加安全漏洞警告 2025-12-26 19:22:08 +08:00
shaw
d53a399d41 revert: 回退到安全漏洞修复版本 2025-12-26 19:15:50 +08:00
7 changed files with 93 additions and 30 deletions

View File

@@ -1,5 +1,10 @@
# Claude Relay Service # Claude Relay Service
> [!CAUTION]
> **安全更新通知**v1.1.248 及以下版本存在严重的管理员认证绕过漏洞,攻击者可未授权访问管理面板。
>
> **请立即更新到 v1.1.249+ 版本**,或迁移到新一代项目 **[CRS 2.0 (sub2api)](https://github.com/Wei-Shaw/sub2api)**
<div align="center"> <div align="center">
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

View File

@@ -1,5 +1,10 @@
# Claude Relay Service # Claude Relay Service
> [!CAUTION]
> **Security Update**: v1.1.248 and below contain a critical admin authentication bypass vulnerability allowing unauthorized access to the admin panel.
>
> **Please update to v1.1.249+ immediately**, or migrate to the next-generation project **[CRS 2.0 (sub2api)](https://github.com/Wei-Shaw/sub2api)**
<div align="center"> <div align="center">
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

21
SECURITY.md Normal file
View File

@@ -0,0 +1,21 @@
# Security Policy
## Supported Versions
Use this section to tell people about which versions of your project are
currently being supported with security updates.
| Version | Supported |
| ------- | ------------------ |
| 5.1.x | :white_check_mark: |
| 5.0.x | :x: |
| 4.0.x | :white_check_mark: |
| < 4.0 | :x: |
## Reporting a Vulnerability
Use this section to tell people how to report a vulnerability.
Tell them where to go, how often they can expect to get an update on a
reported vulnerability, what to expect if the vulnerability is accepted or
declined, etc.

View File

@@ -1 +1 @@
1.1.240 1.1.251

View File

@@ -1434,7 +1434,6 @@ const authenticateAdmin = async (req, res, next) => {
// 设置管理员信息(只包含必要信息) // 设置管理员信息(只包含必要信息)
req.admin = { req.admin = {
id: adminSession.adminId || 'admin',
username: adminSession.username, username: adminSession.username,
sessionId: token, sessionId: token,
loginTime: adminSession.loginTime loginTime: adminSession.loginTime
@@ -1567,17 +1566,25 @@ const authenticateUserOrAdmin = async (req, res, next) => {
try { try {
const adminSession = await redis.getSession(adminToken) const adminSession = await redis.getSession(adminToken)
if (adminSession && Object.keys(adminSession).length > 0) { if (adminSession && Object.keys(adminSession).length > 0) {
req.admin = { // 🔒 安全修复:验证会话必须字段(与 authenticateAdmin 保持一致)
id: adminSession.adminId || 'admin', if (!adminSession.username || !adminSession.loginTime) {
username: adminSession.username, logger.security(
sessionId: adminToken, `🔒 Corrupted admin session in authenticateUserOrAdmin from ${req.ip || 'unknown'} - missing required fields (username: ${!!adminSession.username}, loginTime: ${!!adminSession.loginTime})`
loginTime: adminSession.loginTime )
} await redis.deleteSession(adminToken) // 清理无效/伪造的会话
req.userType = 'admin' // 不返回 401继续尝试用户认证
} else {
req.admin = {
username: adminSession.username,
sessionId: adminToken,
loginTime: adminSession.loginTime
}
req.userType = 'admin'
const authDuration = Date.now() - startTime const authDuration = Date.now() - startTime
logger.security(`🔐 Admin authenticated: ${adminSession.username} in ${authDuration}ms`) logger.security(`🔐 Admin authenticated: ${adminSession.username} in ${authDuration}ms`)
return next() return next()
}
} }
} catch (error) { } catch (error) {
logger.debug('Admin authentication failed, trying user authentication:', error.message) logger.debug('Admin authentication failed, trying user authentication:', error.message)

View File

@@ -402,16 +402,29 @@ async function handleChatCompletion(req, res, apiKeyData) {
const duration = Date.now() - startTime const duration = Date.now() - startTime
logger.info(`✅ OpenAI-Claude request completed in ${duration}ms`) logger.info(`✅ OpenAI-Claude request completed in ${duration}ms`)
} catch (error) { } catch (error) {
logger.error('❌ OpenAI-Claude request error:', error) // 客户端主动断开连接是正常情况,使用 INFO 级别
if (error.message === 'Client disconnected') {
logger.info('🔌 OpenAI-Claude stream ended: Client disconnected')
} else {
logger.error('❌ OpenAI-Claude request error:', error)
}
const status = error.status || 500 // 检查响应是否已发送(流式响应场景),避免 ERR_HTTP_HEADERS_SENT
res.status(status).json({ if (!res.headersSent) {
error: { // 客户端断开使用 499 状态码 (Client Closed Request)
message: error.message || 'Internal server error', if (error.message === 'Client disconnected') {
type: 'server_error', res.status(499).end()
code: 'internal_error' } else {
const status = error.status || 500
res.status(status).json({
error: {
message: error.message || 'Internal server error',
type: 'server_error',
code: 'internal_error'
}
})
} }
}) }
} finally { } finally {
// 清理资源 // 清理资源
if (abortController) { if (abortController) {

View File

@@ -604,7 +604,12 @@ router.post('/v1/chat/completions', authenticateApiKey, async (req, res) => {
const duration = Date.now() - startTime const duration = Date.now() - startTime
logger.info(`OpenAI-Gemini request completed in ${duration}ms`) logger.info(`OpenAI-Gemini request completed in ${duration}ms`)
} catch (error) { } catch (error) {
logger.error('OpenAI-Gemini request error:', error) // 客户端主动断开连接是正常情况,使用 INFO 级别
if (error.message === 'Client disconnected') {
logger.info('🔌 OpenAI-Gemini stream ended: Client disconnected')
} else {
logger.error('OpenAI-Gemini request error:', error)
}
// 处理速率限制 // 处理速率限制
if (error.status === 429) { if (error.status === 429) {
@@ -613,17 +618,24 @@ router.post('/v1/chat/completions', authenticateApiKey, async (req, res) => {
} }
} }
// 返回 OpenAI 格式的错误响应 // 检查响应是否已发送(流式响应场景),避免 ERR_HTTP_HEADERS_SENT
const status = error.status || 500 if (!res.headersSent) {
const errorResponse = { // 客户端断开使用 499 状态码 (Client Closed Request)
error: error.error || { if (error.message === 'Client disconnected') {
message: error.message || 'Internal server error', res.status(499).end()
type: 'server_error', } else {
code: 'internal_error' // 返回 OpenAI 格式的错误响应
const status = error.status || 500
const errorResponse = {
error: error.error || {
message: error.message || 'Internal server error',
type: 'server_error',
code: 'internal_error'
}
}
res.status(status).json(errorResponse)
} }
} }
res.status(status).json(errorResponse)
} finally { } finally {
// 清理资源 // 清理资源
if (abortController) { if (abortController) {