fix: Critical 보안 이슈 4건 수정

1. SQL injection 취약점 수정 (currency 직접 삽입 제거)
   - SQL 쿼리에서 currency 제거, 결과 매핑에서 추가

2. 에러 메시지 정보 노출 수정
   - 클라이언트에 내부 에러 상세 숨김
   - 서버 로그에만 기록

3. API 키 로깅 제거
   - sk-*** 형식만 표시, 실제 값 노출 안함

4. Rate limit fail-closed 정책 적용
   - KV 오류 시 요청 거부 (보안 강화)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
kappa
2026-01-25 16:11:45 +09:00
parent 1d0cbdd7cc
commit efb5dc70e7

View File

@@ -635,8 +635,8 @@ async function checkRateLimit(clientIP: string, env: Env): Promise<{ allowed: bo
return { allowed: true, requestId }; return { allowed: true, requestId };
} catch (error) { } catch (error) {
console.error('[RateLimit] KV error:', error); console.error('[RateLimit] KV error:', error);
// On error, allow the request (fail open) // On error, deny the request (fail closed) for security
return { allowed: true, requestId }; return { allowed: false, requestId };
} }
} }
@@ -1077,10 +1077,10 @@ async function handleRecommend(
} catch (error) { } catch (error) {
console.error('[Recommend] Error:', error); console.error('[Recommend] Error:', error);
console.error('[Recommend] Error stack:', error instanceof Error ? error.stack : 'No stack'); console.error('[Recommend] Error stack:', error instanceof Error ? error.stack : 'No stack');
console.error('[Recommend] Error details:', error instanceof Error ? error.message : 'Unknown error');
return jsonResponse( return jsonResponse(
{ {
error: 'Failed to generate recommendations', error: 'Failed to generate recommendations',
details: error instanceof Error ? error.message : 'Unknown error',
request_id: requestId, request_id: requestId,
}, },
500, 500,
@@ -1303,7 +1303,6 @@ async function queryCandidateServers(
it.gpu_count, it.gpu_count,
it.gpu_type, it.gpu_type,
MIN(${priceColumn}) as monthly_price, MIN(${priceColumn}) as monthly_price,
'${currency}' as currency,
r.region_name as region_name, r.region_name as region_name,
r.region_code as region_code, r.region_code as region_code,
r.country_code as country_code r.country_code as country_code
@@ -1436,8 +1435,14 @@ async function queryCandidateServers(
throw new Error('Failed to query candidate servers'); throw new Error('Failed to query candidate servers');
} }
// Validate each result with type guard // Add currency to each result and validate with type guard
const validServers = (result.results as unknown[]).filter(isValidServer); const serversWithCurrency = (result.results as unknown[]).map(server => {
if (typeof server === 'object' && server !== null) {
return { ...server, currency };
}
return server;
});
const validServers = serversWithCurrency.filter(isValidServer);
const invalidCount = result.results.length - validServers.length; const invalidCount = result.results.length - validServers.length;
if (invalidCount > 0) { if (invalidCount > 0) {
console.warn(`[Candidates] Filtered out ${invalidCount} invalid server records`); console.warn(`[Candidates] Filtered out ${invalidCount} invalid server records`);
@@ -1851,7 +1856,7 @@ async function getAIRecommendations(
console.error('[AI] OPENAI_API_KEY has invalid format (should start with sk-)'); console.error('[AI] OPENAI_API_KEY has invalid format (should start with sk-)');
throw new Error('Invalid OPENAI_API_KEY format'); throw new Error('Invalid OPENAI_API_KEY format');
} }
console.log('[AI] API key validated (sk-***' + apiKey.slice(-4) + ')'); console.log('[AI] API key validated (format: sk-***)');
// Build dynamic tech specs prompt from database // Build dynamic tech specs prompt from database
const techSpecsPrompt = formatTechSpecsForPrompt(techSpecs); const techSpecsPrompt = formatTechSpecsForPrompt(techSpecs);