Files
ExperionCrawler/프로젝트진단-2026-05-14.md
windpacer 302183c97e feat: P&ID 연결 분석, LLM 에이전트 모드, KB 확장, MCP 서버 리팩토링
- 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 업데이트
- 정리: 진단 체크리스트 문서 삭제
2026-05-21 23:36:57 +09:00

7.5 KiB

ExperionCrawler 코드 진단 보고서

분석일: 2026-05-14 | 기준: .roo/rules-code/diagnosis-checklist.md 8단계 CLAUDE.md 작업 이력(2026-04-14 ~ 2026-05-14)을 반영하여 이미 수정된 항목은 제외


🔴 HIGH (2건)

1. SQL 인젝션 — CreateHypertableAsync의 RetentionPeriod/CompressionPeriod 미검증 (HIGH)

문제: CreateHypertableAsync에서 request.RetentionPeriodrequest.CompressionPeriodIsValidSqlIdentifier 검증 없이 SQL 문자열에 직접 보간됨. TableName/TimeColumn은 검증되지만, INTERVAL 인자 두 개는 검증 사각지대. 근거: src/Infrastructure/Database/ExperionDbContext.cs:1736INTERVAL '{request.RetentionPeriod}', :1746INTERVAL '{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:651detail = 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로 예외가 삼켜짐. 수정: IAsyncDisposableDisposeAsync()에서 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분간 연결 유지. 하지만 개별 CallToolAsyncCancellationToken을 받으므로 컨트롤러 레이어에서 취소 가능. 수정: 기본값을 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. #1 (SQL 인젝션)RetentionPeriod/CompressionPeriod 검증 추가
  2. #2 (스택 트레이스 노출) — 고정 에러 메시지로 교체
  3. #3 (Console.WriteLine DbContext) — ILogger로 교체
  4. #4 (blocking .Open())OpenAsync()로 교체