mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 00:53:33 +00:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a345812cd7 | ||
|
|
a0cbafd759 | ||
|
|
3c64038fa7 | ||
|
|
45b81bd478 | ||
|
|
fc57133230 | ||
|
|
1f06af4a56 | ||
|
|
6165fad090 | ||
|
|
d53a399d41 | ||
|
|
982cca1020 |
@@ -1,5 +1,10 @@
|
||||
# Claude Relay Service
|
||||
|
||||
> [!CAUTION]
|
||||
> **安全更新通知**:v1.1.248 及以下版本存在严重的管理员认证绕过漏洞,攻击者可未授权访问管理面板。
|
||||
>
|
||||
> **请立即更新到 v1.1.249+ 版本**,或迁移到新一代项目 **[CRS 2.0 (sub2api)](https://github.com/Wei-Shaw/sub2api)**
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
# 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">
|
||||
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
|
||||
21
SECURITY.md
Normal file
21
SECURITY.md
Normal 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.
|
||||
52
src/app.js
52
src/app.js
@@ -68,6 +68,10 @@ class Application {
|
||||
logger.info('🔄 Initializing admin credentials...')
|
||||
await this.initializeAdmin()
|
||||
|
||||
// 🔒 安全启动:清理无效/伪造的管理员会话
|
||||
logger.info('🔒 Cleaning up invalid admin sessions...')
|
||||
await this.cleanupInvalidSessions()
|
||||
|
||||
// 💰 初始化费用数据
|
||||
logger.info('💰 Checking cost data initialization...')
|
||||
const costInitService = require('./services/costInitService')
|
||||
@@ -426,6 +430,54 @@ class Application {
|
||||
}
|
||||
}
|
||||
|
||||
// 🔒 清理无效/伪造的管理员会话(安全启动检查)
|
||||
async cleanupInvalidSessions() {
|
||||
try {
|
||||
const client = redis.getClient()
|
||||
|
||||
// 获取所有 session:* 键
|
||||
const sessionKeys = await client.keys('session:*')
|
||||
|
||||
let validCount = 0
|
||||
let invalidCount = 0
|
||||
|
||||
for (const key of sessionKeys) {
|
||||
// 跳过 admin_credentials(系统凭据)
|
||||
if (key === 'session:admin_credentials') {
|
||||
continue
|
||||
}
|
||||
|
||||
const sessionData = await client.hgetall(key)
|
||||
|
||||
// 检查会话完整性:必须有 username 和 loginTime
|
||||
const hasUsername = !!sessionData.username
|
||||
const hasLoginTime = !!sessionData.loginTime
|
||||
|
||||
if (!hasUsername || !hasLoginTime) {
|
||||
// 无效会话 - 可能是漏洞利用创建的伪造会话
|
||||
invalidCount++
|
||||
logger.security(
|
||||
`🔒 Removing invalid session: ${key} (username: ${hasUsername}, loginTime: ${hasLoginTime})`
|
||||
)
|
||||
await client.del(key)
|
||||
} else {
|
||||
validCount++
|
||||
}
|
||||
}
|
||||
|
||||
if (invalidCount > 0) {
|
||||
logger.security(`🔒 Startup security check: Removed ${invalidCount} invalid sessions`)
|
||||
}
|
||||
|
||||
logger.success(
|
||||
`✅ Session cleanup completed: ${validCount} valid, ${invalidCount} invalid removed`
|
||||
)
|
||||
} catch (error) {
|
||||
// 清理失败不应阻止服务启动
|
||||
logger.error('❌ Failed to cleanup invalid sessions:', error.message)
|
||||
}
|
||||
}
|
||||
|
||||
// 🔍 Redis健康检查
|
||||
async checkRedisHealth() {
|
||||
try {
|
||||
|
||||
@@ -1389,6 +1389,18 @@ const authenticateAdmin = async (req, res, next) => {
|
||||
})
|
||||
}
|
||||
|
||||
// 🔒 安全修复:验证会话必须字段(防止伪造会话绕过认证)
|
||||
if (!adminSession.username || !adminSession.loginTime) {
|
||||
logger.security(
|
||||
`🔒 Corrupted admin session from ${req.ip || 'unknown'} - missing required fields (username: ${!!adminSession.username}, loginTime: ${!!adminSession.loginTime})`
|
||||
)
|
||||
await redis.deleteSession(token) // 清理无效/伪造的会话
|
||||
return res.status(401).json({
|
||||
error: 'Invalid session',
|
||||
message: 'Session data corrupted or incomplete'
|
||||
})
|
||||
}
|
||||
|
||||
// 检查会话活跃性(可选:检查最后活动时间)
|
||||
const now = new Date()
|
||||
const lastActivity = new Date(adminSession.lastActivity || adminSession.loginTime)
|
||||
@@ -1422,7 +1434,6 @@ const authenticateAdmin = async (req, res, next) => {
|
||||
|
||||
// 设置管理员信息(只包含必要信息)
|
||||
req.admin = {
|
||||
id: adminSession.adminId || 'admin',
|
||||
username: adminSession.username,
|
||||
sessionId: token,
|
||||
loginTime: adminSession.loginTime
|
||||
@@ -1555,17 +1566,25 @@ const authenticateUserOrAdmin = async (req, res, next) => {
|
||||
try {
|
||||
const adminSession = await redis.getSession(adminToken)
|
||||
if (adminSession && Object.keys(adminSession).length > 0) {
|
||||
req.admin = {
|
||||
id: adminSession.adminId || 'admin',
|
||||
username: adminSession.username,
|
||||
sessionId: adminToken,
|
||||
loginTime: adminSession.loginTime
|
||||
}
|
||||
req.userType = 'admin'
|
||||
// 🔒 安全修复:验证会话必须字段(与 authenticateAdmin 保持一致)
|
||||
if (!adminSession.username || !adminSession.loginTime) {
|
||||
logger.security(
|
||||
`🔒 Corrupted admin session in authenticateUserOrAdmin from ${req.ip || 'unknown'} - missing required fields (username: ${!!adminSession.username}, loginTime: ${!!adminSession.loginTime})`
|
||||
)
|
||||
await redis.deleteSession(adminToken) // 清理无效/伪造的会话
|
||||
// 不返回 401,继续尝试用户认证
|
||||
} else {
|
||||
req.admin = {
|
||||
username: adminSession.username,
|
||||
sessionId: adminToken,
|
||||
loginTime: adminSession.loginTime
|
||||
}
|
||||
req.userType = 'admin'
|
||||
|
||||
const authDuration = Date.now() - startTime
|
||||
logger.security(`🔐 Admin authenticated: ${adminSession.username} in ${authDuration}ms`)
|
||||
return next()
|
||||
const authDuration = Date.now() - startTime
|
||||
logger.security(`🔐 Admin authenticated: ${adminSession.username} in ${authDuration}ms`)
|
||||
return next()
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.debug('Admin authentication failed, trying user authentication:', error.message)
|
||||
|
||||
@@ -164,13 +164,27 @@ router.post('/auth/change-password', async (req, res) => {
|
||||
|
||||
// 获取当前会话
|
||||
const sessionData = await redis.getSession(token)
|
||||
if (!sessionData) {
|
||||
|
||||
// 🔒 安全修复:检查空对象
|
||||
if (!sessionData || Object.keys(sessionData).length === 0) {
|
||||
return res.status(401).json({
|
||||
error: 'Invalid token',
|
||||
message: 'Session expired or invalid'
|
||||
})
|
||||
}
|
||||
|
||||
// 🔒 安全修复:验证会话完整性
|
||||
if (!sessionData.username || !sessionData.loginTime) {
|
||||
logger.security(
|
||||
`🔒 Invalid session structure in /auth/change-password from ${req.ip || 'unknown'}`
|
||||
)
|
||||
await redis.deleteSession(token)
|
||||
return res.status(401).json({
|
||||
error: 'Invalid session',
|
||||
message: 'Session data corrupted or incomplete'
|
||||
})
|
||||
}
|
||||
|
||||
// 获取当前管理员信息
|
||||
const adminData = await redis.getSession('admin_credentials')
|
||||
if (!adminData) {
|
||||
@@ -269,13 +283,25 @@ router.get('/auth/user', async (req, res) => {
|
||||
|
||||
// 获取当前会话
|
||||
const sessionData = await redis.getSession(token)
|
||||
if (!sessionData) {
|
||||
|
||||
// 🔒 安全修复:检查空对象
|
||||
if (!sessionData || Object.keys(sessionData).length === 0) {
|
||||
return res.status(401).json({
|
||||
error: 'Invalid token',
|
||||
message: 'Session expired or invalid'
|
||||
})
|
||||
}
|
||||
|
||||
// 🔒 安全修复:验证会话完整性
|
||||
if (!sessionData.username || !sessionData.loginTime) {
|
||||
logger.security(`🔒 Invalid session structure in /auth/user from ${req.ip || 'unknown'}`)
|
||||
await redis.deleteSession(token)
|
||||
return res.status(401).json({
|
||||
error: 'Invalid session',
|
||||
message: 'Session data corrupted or incomplete'
|
||||
})
|
||||
}
|
||||
|
||||
// 获取管理员信息
|
||||
const adminData = await redis.getSession('admin_credentials')
|
||||
if (!adminData) {
|
||||
@@ -316,13 +342,24 @@ router.post('/auth/refresh', async (req, res) => {
|
||||
|
||||
const sessionData = await redis.getSession(token)
|
||||
|
||||
if (!sessionData) {
|
||||
// 🔒 安全修复:检查空对象(hgetall 对不存在的 key 返回 {})
|
||||
if (!sessionData || Object.keys(sessionData).length === 0) {
|
||||
return res.status(401).json({
|
||||
error: 'Invalid token',
|
||||
message: 'Session expired or invalid'
|
||||
})
|
||||
}
|
||||
|
||||
// 🔒 安全修复:验证会话完整性(必须有 username 和 loginTime)
|
||||
if (!sessionData.username || !sessionData.loginTime) {
|
||||
logger.security(`🔒 Invalid session structure detected from ${req.ip || 'unknown'}`)
|
||||
await redis.deleteSession(token) // 清理无效/伪造的会话
|
||||
return res.status(401).json({
|
||||
error: 'Invalid session',
|
||||
message: 'Session data corrupted or incomplete'
|
||||
})
|
||||
}
|
||||
|
||||
// 更新最后活动时间
|
||||
sessionData.lastActivity = new Date().toISOString()
|
||||
await redis.setSession(token, sessionData, config.security.adminSessionTimeout)
|
||||
|
||||
Reference in New Issue
Block a user