feat(security): API 키 보호, CORS 강화, Rate Limiting KV 전환
보안 개선: - API 키 하드코딩 제거 (NAMECHEAP_API_KEY_INTERNAL) - CORS 정책: * → hosting.anvil.it.com 제한 - /health 엔드포인트 DB 정보 노출 방지 - Rate Limiting 인메모리 Map → Cloudflare KV 전환 - 분산 환경 일관성 보장 - 재시작 후에도 유지 - 자동 만료 (TTL) 문서: - CLAUDE.md Security 섹션 추가 - KV Namespace 설정 가이드 추가 - 배포/마이그레이션 가이드 추가 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -118,36 +118,56 @@ export async function validateWebhookRequest(
|
||||
return { valid: true, update: body };
|
||||
}
|
||||
|
||||
// Rate Limiting
|
||||
const rateLimitMap = new Map<string, { count: number; resetAt: number }>();
|
||||
// Rate Limiting (Cloudflare KV 기반)
|
||||
interface RateLimitData {
|
||||
count: number;
|
||||
resetAt: number;
|
||||
}
|
||||
|
||||
export function checkRateLimit(
|
||||
export async function checkRateLimit(
|
||||
kv: KVNamespace,
|
||||
userId: string,
|
||||
maxRequests: number = 30,
|
||||
windowMs: number = 60000
|
||||
): boolean {
|
||||
): Promise<boolean> {
|
||||
const key = `ratelimit:${userId}`;
|
||||
const now = Date.now();
|
||||
const userLimit = rateLimitMap.get(userId);
|
||||
|
||||
if (!userLimit || now > userLimit.resetAt) {
|
||||
rateLimitMap.set(userId, { count: 1, resetAt: now + windowMs });
|
||||
try {
|
||||
// KV에서 기존 데이터 조회
|
||||
const dataStr = await kv.get(key);
|
||||
const data: RateLimitData | null = dataStr ? JSON.parse(dataStr) : null;
|
||||
|
||||
// 윈도우 만료 또는 첫 요청
|
||||
if (!data || now > data.resetAt) {
|
||||
const newData: RateLimitData = {
|
||||
count: 1,
|
||||
resetAt: now + windowMs,
|
||||
};
|
||||
await kv.put(key, JSON.stringify(newData), {
|
||||
expirationTtl: Math.ceil(windowMs / 1000), // 초 단위
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// Rate limit 초과
|
||||
if (data.count >= maxRequests) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 카운트 증가
|
||||
const updatedData: RateLimitData = {
|
||||
count: data.count + 1,
|
||||
resetAt: data.resetAt,
|
||||
};
|
||||
const remainingTtl = Math.ceil((data.resetAt - now) / 1000);
|
||||
await kv.put(key, JSON.stringify(updatedData), {
|
||||
expirationTtl: Math.max(remainingTtl, 1), // 최소 1초
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('[RateLimit] KV 오류:', error);
|
||||
// KV 오류 시 허용 (서비스 가용성 우선)
|
||||
return true;
|
||||
}
|
||||
|
||||
if (userLimit.count >= maxRequests) {
|
||||
return false;
|
||||
}
|
||||
|
||||
userLimit.count++;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Rate limit 정리 (메모리 관리)
|
||||
export function cleanupRateLimits(): void {
|
||||
const now = Date.now();
|
||||
for (const [key, value] of rateLimitMap.entries()) {
|
||||
if (now > value.resetAt) {
|
||||
rateLimitMap.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user