- P&ID: 연결 분석 API, Prefix 규칙 관리, 카테고리 분류, DXF 그래프 빌드 - LLM: 대화 요약, tool card 영구 보존, 시계열 차트(uPlot), 에이전트 모드 - KB: 청크 미리보기, Field Instrument Inference, 인증/Qdrant 클라이언트 - MCP: 서버 기능 확장, 파이프라인 수정, timeout 개선 - Frontend: P&ID UI, LLM UI, KB UI, OPC UA Write 탭 추가 - 설정: AGENTS.md, plant_context, README, opencode.json 업데이트 - 정리: 진단 체크리스트 문서 삭제
7.5 KiB
ExperionCrawler 코드 진단 보고서
분석일: 2026-05-14 | 기준:
.roo/rules-code/diagnosis-checklist.md8단계 CLAUDE.md 작업 이력(2026-04-14 ~ 2026-05-14)을 반영하여 이미 수정된 항목은 제외
🔴 HIGH (2건)
1. SQL 인젝션 — CreateHypertableAsync의 RetentionPeriod/CompressionPeriod 미검증 (HIGH)
문제: CreateHypertableAsync에서 request.RetentionPeriod와 request.CompressionPeriod가 IsValidSqlIdentifier 검증 없이 SQL 문자열에 직접 보간됨. TableName/TimeColumn은 검증되지만, INTERVAL 인자 두 개는 검증 사각지대.
근거: src/Infrastructure/Database/ExperionDbContext.cs:1736 — INTERVAL '{request.RetentionPeriod}', :1746 — INTERVAL '{request.CompressionPeriod}'. IsValidSqlIdentifier (line 1660)는 ^[a-zA-Z0-9_\-\.]+$ 패턴으로 식별자만 검증.
영향: 악의적 RetentionPeriod 값(예: '::text; DROP TABLE history_table;--)로 SQL 인젝션 가능.
수정: INTERVAL 인자에 대해 별도 검증 함수 추가 (예: ^[0-9]+[smhdw]$ 패턴) 또는 NpgsqlParameter 사용.
2. 스택 트레이스 노출 — HistoryController 에러 응답 (HIGH)
문제: QueryHistoryAsync 예외 시 InnerException.Message 또는 StackTrace가 HTTP 응답 본문에 포함됨.
근거: src/Web/Controllers/ExperionControllers.cs:651 — detail = ex.InnerException?.Message ?? ex.StackTrace
영향: 내부 DB 구조, 쿼리, 파일 경로 등 민감 정보가 외부 클라이언트에 노출.
수정: detail = "이력 조회 중 오류가 발생했습니다." 등 고정 메시지. 실제 예체는 ILogger로 로깅.
🟠 MED (5건)
3. Console.WriteLine 잔존 — ExperionDbContext.cs (MED)
문제: CreateHypertableAsync 내에서 14개 이상의 Console.WriteLine이 [DEBUG]/[ERROR] 태그로 출력. 일부는 전체 스택 트레이스 포함.
근거: src/Infrastructure/Database/ExperionDbContext.cs:1714, 1721, 1728, 1737, 1747, 1771, 1779, 1797, 1800, 1802, 1808, 1820, 1823, 1824, 1826
영향: 프로덕션에서 systemd journal에 [DEBUG]/[ERROR] 출력이 ILogger 파이프라인을 우회해 출력. 스택 트레이스 노출.
수정: _logger.LogInformation/LogDebug/LogError로 교체.
4. Blocking .Open() — CreateHypertableAsync (MED)
문제: _ctx.Database.GetDbConnection().Open()이 동기 호출됨. async 메서드 내에서 이벤트루프 블로킹 가능.
근거: src/Infrastructure/Database/ExperionDbContext.cs:1678
영향: 고부하 시 ASP.NET 요청 스레드 블로킹 가능. TimescaleDB 확장이 이미 설치된 환경에서는 이 코드가 자주 실행되지 않지만, retry 시나리오에서 문제 발생 가능.
수정: await _ctx.Database.OpenConnectionAsync()로 교체, finally에 CloseConnectionAsync().
5. Blocking .GetResult() — ExperionRealtimeService.Dispose (MED)
문제: Dispose()에서 CleanupSessionAsync().GetAwaiter().GetResult()로 동기 블로킹.
근거: src/Infrastructure/OpcUa/ExperionRealtimeService.cs:585
영향: StopAsync()가 먼저 호출된 정상 종료 경로에서는 문제 없음. 하지만 StopAsync 없이 Dispose가 호출되면 데드락 가능. empty catch로 예외가 삼켜짐.
수정: IAsyncDisposable의 DisposeAsync()에서 async로 처리하고, Dispose()에서는 실제 정리가 아닌 DisposeAsync().GetAwaiter().GetResult() 대신 try { await this; } catch { } 패턴 사용.
6. CORS AllowAnyOrigin — Program.cs (MED)
문제: AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader()로 완전 개방.
근거: src/Web/Program.cs:154
영향: 공장 내부망에서는 위험도가 낮으나, 외부 노출 시 CSRF 등 공격 가능.
수정: WithOrigins("http://factory-internal:5000") 등 명시적 origin 설정.
7. Brute-force 미방어 — KB 로그인 (MED)
문제: /api/kb/auth/login에 rate limiting, 계정 잠금, attempt 카운팅 없음.
근거: src/Web/Controllers/KbAuthController.cs:23-34 — 예외적 로그인 시도 로깅만 있으며, 제한 로직 없음.
영향: 내부망에서는 위험도가 낮으나, Argon2id 해시 검증이 계산 집약적이므로 DoS로 서비스 감속 가능.
수정: ASP.NET Rate Limiting 미들웨어 적용 또는 IDistributedRateLimiter 기반 throttling.
🟡 LOW (4건)
8. Console.WriteLine — AssetLoader.cs (LOW)
문제: 7개의 Console.WriteLine (이모지 포함). ILogger 미사용.
근거: src/Infrastructure/Csv/AssetLoader.cs:22, 26, 32, 41, 74, 78, 82
영향: AssetLoader는 일회성 CSV 가져오기 유틸리티. 프로덕션 영향은 낮으나, journal 로그 오염.
수정: ILogger 의존성 주입 후 교체.
9. Process.Dispose 누락 — McpServerHostedService (LOW)
문제: Process 인스턴스가 Kill() 후 Dispose()되지 않음.
근거: src/Infrastructure/Mcp/McpServerHostedService.cs:43 — _process = new Process, :93 — _process.Kill(entireProcessTree: true), Dispose 없음.
영향: 언매니지드 핸들 누수. 하지만 앱 종료 시 프로세스가 함께 종료되므로 실제 영향은 미미.
수정: using var process = new Process { ... } 또는 StopAsync 끝에서 _process?.Dispose().
10. HttpClient 직접 생성 — McpClient (LOW)
문제: new HttpClient { Timeout = TimeSpan.FromSeconds(1800) }로 직접 생성 (IHttpClientFactory 경로의 fallback).
근거: src/Infrastructure/Mcp/McpClient.cs:23-27
영향: DI 주입 경로에서는 null이 전달되어 fallback 경로가 활성화됨 (Program.cs:92 확인 필요). 소켓 고갈 가능성.
수정: Program.cs에서 AddHttpClient("McpClient") 등록 후 McpClient 생성자에 주입.
11. McpClient Timeout 30분 (LOW)
문제: 기본 타임아웃 1800초 (30분).
근거: src/Infrastructure/Mcp/McpClient.cs:26
영향: MCP 서버 응답 지연 시 30분간 연결 유지. 하지만 개별 CallToolAsync는 CancellationToken을 받으므로 컨트롤러 레이어에서 취소 가능.
수정: 기본값을 60~120초로 낮추고, LLM 호출용 별도 HttpClient 등록.
📊 요약
| 등급 | 건수 | 항목 |
|---|---|---|
| 🔴 HIGH | 2 | SQL 인젝션 (#1), 스택 트레이스 노출 (#2) |
| 🟠 MED | 5 | Console.WriteLine (#3), blocking async (#4, #5), CORS (#6), brute-force (#7) |
| 🟡 LOW | 4 | Console.WriteLine (#8), Process dispose (#9), HttpClient (#10, #11) |
교차 검증 결과 — 제외된 항목
| 항목 | 제외 사유 | 교차 검증 질문 |
|---|---|---|
_restarting 레이스 컨디션 |
실제로 volatile 선언됨. UI 단일 사용자 환경에서 동시 호출 시나리오 없음 |
Q4 (재현 시나리오 부재) |
ctx.Cancel non-volatile |
ConcurrentDictionary에서 세션 제거 후 확인 → 최대 1회 stale read만 발생 |
Q2 (다른 레이어 처리) |
_flushTask 미대기 |
StopAsync에서 Task.WhenAll로 적절히 처리됨 |
Q2 (다른 레이어 처리) |
정적 캐시 (_digitalTagCache, _plantContextCached) |
lock으로 동기화됨. TTL 기반 무효화 적용 |
Q3 (의도적 설계) |
즉시 수정 우선순위
- #1 (SQL 인젝션) —
RetentionPeriod/CompressionPeriod검증 추가 - #2 (스택 트레이스 노출) — 고정 에러 메시지로 교체
- #3 (Console.WriteLine DbContext) — ILogger로 교체
- #4 (blocking .Open()) —
OpenAsync()로 교체