mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-16 13:03:31 +08:00
fix(security): add rate limiting, default-closed mode, extract isAllowedUser
- Rate limit: max 5 failed pairing attempts per user per 5 minutes - Default closed: reject all users when no allowedUsers/pairedUsers configured - Extract isAllowedUser() to common/pairing.ts to eliminate duplication Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
bf5cd6e3b8
commit
158f2de13e
@ -13,6 +13,32 @@ import * as crypto from 'node:crypto'
|
||||
import type { PairedUser, PairingState } from './config.js'
|
||||
|
||||
const SAFE_ALPHABET = 'ABCDEFGHJKMNPQRSTUVWXYZ23456789' // 排除 0/O/1/I/L
|
||||
|
||||
// 速率限制:每个 userId 在 RATE_LIMIT_WINDOW_MS 内最多 RATE_LIMIT_MAX_ATTEMPTS 次失败尝试
|
||||
const RATE_LIMIT_WINDOW_MS = 5 * 60 * 1000 // 5 minutes
|
||||
const RATE_LIMIT_MAX_ATTEMPTS = 5
|
||||
const failedAttempts = new Map<string, { count: number; firstAttempt: number }>()
|
||||
|
||||
function isRateLimited(userId: string | number): boolean {
|
||||
const key = String(userId)
|
||||
const record = failedAttempts.get(key)
|
||||
if (!record) return false
|
||||
if (Date.now() - record.firstAttempt > RATE_LIMIT_WINDOW_MS) {
|
||||
failedAttempts.delete(key)
|
||||
return false
|
||||
}
|
||||
return record.count >= RATE_LIMIT_MAX_ATTEMPTS
|
||||
}
|
||||
|
||||
function recordFailedAttempt(userId: string | number): void {
|
||||
const key = String(userId)
|
||||
const record = failedAttempts.get(key)
|
||||
if (!record || Date.now() - record.firstAttempt > RATE_LIMIT_WINDOW_MS) {
|
||||
failedAttempts.set(key, { count: 1, firstAttempt: Date.now() })
|
||||
} else {
|
||||
record.count++
|
||||
}
|
||||
}
|
||||
const CODE_LENGTH = 6
|
||||
const CODE_TTL_MS = 60 * 60 * 1000 // 60 minutes
|
||||
|
||||
@ -58,8 +84,8 @@ export function isPaired(
|
||||
|
||||
// allowedUsers 非空时检查
|
||||
if (allowedUsers.length > 0 && allowedUsers.includes(userId)) return true
|
||||
// allowedUsers 为空且 pairedUsers 也为空 => 旧行为(允许所有人)
|
||||
if (pairedUsers.length === 0 && allowedUsers.length === 0) return true
|
||||
// 默认关闭:没有配置任何用户时拒绝访问(需要先配对)
|
||||
if (pairedUsers.length === 0 && allowedUsers.length === 0) return false
|
||||
|
||||
return pairedUsers.some((p) => String(p.userId) === String(userId))
|
||||
}
|
||||
@ -76,13 +102,19 @@ export function tryPair(
|
||||
const file = readConfigFile()
|
||||
const pairing: PairingState = file.pairing ?? { code: null, expiresAt: null, createdAt: null }
|
||||
|
||||
// 速率限制检查
|
||||
if (isRateLimited(senderInfo.userId)) return false
|
||||
|
||||
// 检查配对码是否有效
|
||||
if (!pairing.code || !pairing.expiresAt) return false
|
||||
if (Date.now() > pairing.expiresAt) return false
|
||||
|
||||
// 比较(忽略大小写和空格)
|
||||
const input = messageText.trim().toUpperCase()
|
||||
if (input !== pairing.code.toUpperCase()) return false
|
||||
if (input !== pairing.code.toUpperCase()) {
|
||||
recordFailedAttempt(senderInfo.userId)
|
||||
return false
|
||||
}
|
||||
|
||||
// 配对成功:写入 pairedUsers
|
||||
const platformConfig = file[platform] ?? {}
|
||||
@ -105,3 +137,13 @@ export function tryPair(
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/** 统一的用户授权检查(供各 adapter 调用) */
|
||||
export function isAllowedUser(platform: 'telegram' | 'feishu', userId: string | number): boolean {
|
||||
try {
|
||||
const cfgFile = readConfigFile()
|
||||
return isPaired(platform, userId, cfgFile)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,7 +16,7 @@ import { loadConfig } from '../common/config.js'
|
||||
import { splitMessage, formatToolUse, formatPermissionRequest, truncateInput } from '../common/format.js'
|
||||
import { SessionStore } from '../common/session-store.js'
|
||||
import { AdapterHttpClient } from '../common/http-client.js'
|
||||
import { isPaired, tryPair } from '../common/pairing.js'
|
||||
import { isAllowedUser, tryPair } from '../common/pairing.js'
|
||||
|
||||
// ---------- init ----------
|
||||
|
||||
@ -56,23 +56,6 @@ let wsClient: InstanceType<typeof Lark.WSClient> | null = null
|
||||
|
||||
// ---------- helpers ----------
|
||||
|
||||
function isAllowedUser(openId: string): boolean {
|
||||
try {
|
||||
const cfgFile = JSON.parse(
|
||||
require('node:fs').readFileSync(
|
||||
require('node:path').join(
|
||||
process.env.CLAUDE_CONFIG_DIR || require('node:path').join(require('node:os').homedir(), '.claude'),
|
||||
'adapters.json'
|
||||
),
|
||||
'utf-8'
|
||||
)
|
||||
)
|
||||
return isPaired('feishu', openId, cfgFile)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function getChatState(chatId: string): ChatState {
|
||||
let state = chatStates.get(chatId)
|
||||
if (!state) {
|
||||
@ -400,7 +383,7 @@ async function handleMessage(data: any): Promise<void> {
|
||||
|
||||
// 只处理私聊
|
||||
if (chatType === 'p2p') {
|
||||
if (!isAllowedUser(senderOpenId)) {
|
||||
if (!isAllowedUser('feishu', senderOpenId)) {
|
||||
// 尝试配对
|
||||
const pairText = extractText(content, msgType)
|
||||
if (pairText) {
|
||||
|
||||
@ -14,7 +14,7 @@ import { loadConfig } from '../common/config.js'
|
||||
import { splitMessage, formatToolUse, formatPermissionRequest } from '../common/format.js'
|
||||
import { SessionStore } from '../common/session-store.js'
|
||||
import { AdapterHttpClient } from '../common/http-client.js'
|
||||
import { isPaired, tryPair } from '../common/pairing.js'
|
||||
import { isAllowedUser, tryPair } from '../common/pairing.js'
|
||||
|
||||
const TELEGRAM_TEXT_LIMIT = 4000 // leave margin below 4096
|
||||
|
||||
@ -43,23 +43,6 @@ const pendingProjectSelection = new Map<string, boolean>()
|
||||
|
||||
// ---------- helpers ----------
|
||||
|
||||
function isAllowedUser(userId: number): boolean {
|
||||
try {
|
||||
const cfgFile = JSON.parse(
|
||||
require('node:fs').readFileSync(
|
||||
require('node:path').join(
|
||||
process.env.CLAUDE_CONFIG_DIR || require('node:path').join(require('node:os').homedir(), '.claude'),
|
||||
'adapters.json'
|
||||
),
|
||||
'utf-8'
|
||||
)
|
||||
)
|
||||
return isPaired('telegram', userId, cfgFile)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function getBuffer(chatId: string): MessageBuffer {
|
||||
let buf = buffers.get(chatId)
|
||||
if (!buf) {
|
||||
@ -289,7 +272,7 @@ bot.on('message:text', (ctx) => {
|
||||
const text = ctx.message.text
|
||||
|
||||
// 检查配对状态
|
||||
if (!isAllowedUser(userId)) {
|
||||
if (!isAllowedUser('telegram', userId)) {
|
||||
// 尝试配对
|
||||
const displayName = [ctx.from.first_name, ctx.from.last_name].filter(Boolean).join(' ')
|
||||
const success = tryPair(text.trim(), { userId, displayName }, 'telegram')
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user