refactor: comprehensive code review fixes and security hardening

Security:
- Add CSP headers for HTML reports (style-src 'unsafe-inline')
- Restrict origin validation to specific .kappa-d8e.workers.dev domain
- Add base64 size limit (100KB) for report data parameter
- Implement rejection sampling for unbiased password generation
- Add SQL LIKE pattern escaping for tech specs query
- Add security warning for plaintext password storage (TODO: encrypt)

Performance:
- Add Telegram API timeout (10s) with AbortController
- Fix rate limiter sorting by resetTime for proper cleanup
- Use centralized TIMEOUTS config for VPS provider APIs

Features:
- Add admin SSH key support for server recovery access
  - ADMIN_SSH_PUBLIC_KEY for Linode (public key string)
  - ADMIN_SSH_KEY_ID_VULTR for Vultr (pre-registered key ID)
- Add origin validation middleware
- Add idempotency key migration

Code Quality:
- Return 404 status when no servers found
- Consolidate error logging to single JSON.stringify call
- Import TECH_CATEGORY_WEIGHTS from config.ts
- Add escapeLikePattern utility function

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
kappa
2026-01-29 11:36:08 +09:00
parent d41f1ee841
commit 5319bf3e4c
27 changed files with 965 additions and 530 deletions

View File

@@ -82,6 +82,18 @@ export function generateCacheKey(req: RecommendRequest): string {
// In-memory fallback for rate limiting when CACHE KV is unavailable
const inMemoryRateLimit = new Map<string, { count: number; resetTime: number }>();
const MAX_IN_MEMORY_ENTRIES = 10000;
/**
* Clean up expired entries from in-memory rate limit map
*/
function cleanupExpiredEntries(now: number): void {
for (const [key, record] of inMemoryRateLimit) {
if (record.resetTime < now) {
inMemoryRateLimit.delete(key);
}
}
}
/**
* Rate limiting check using KV storage with in-memory fallback
@@ -94,6 +106,18 @@ export async function checkRateLimit(clientIP: string, env: Env): Promise<{ allo
// Use in-memory fallback if CACHE unavailable
if (!env.CACHE) {
// Cleanup expired entries if map is getting too large
if (inMemoryRateLimit.size >= MAX_IN_MEMORY_ENTRIES) {
cleanupExpiredEntries(now);
// If still too large after cleanup, remove oldest 10% by resetTime
if (inMemoryRateLimit.size >= MAX_IN_MEMORY_ENTRIES) {
const entries = Array.from(inMemoryRateLimit.entries())
.sort((a, b) => a[1].resetTime - b[1].resetTime)
.slice(0, Math.floor(MAX_IN_MEMORY_ENTRIES * 0.1));
entries.forEach(([key]) => inMemoryRateLimit.delete(key));
}
}
const record = inMemoryRateLimit.get(clientIP);
if (!record || record.resetTime < now) {