From e34ec08001d55bbfcfff251f929d41411dc65abd Mon Sep 17 00:00:00 2001 From: windpacer Date: Sun, 26 Apr 2026 12:17:16 +0900 Subject: [PATCH] =?UTF-8?q?fix(#4):=20HypertableController=20=ED=85=8C?= =?UTF-8?q?=EC=9D=B4=EB=B8=94=EB=AA=85/=EC=BB=AC=EB=9F=BC=EB=AA=85=20?= =?UTF-8?q?=EC=9E=85=EB=A0=A5=20=EA=B2=80=EC=A6=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - TableName allowlist(history_table) + PostgreSQL 식별자 regex(^[a-z_][a-z0-9_]{0,62}$) 검증 - 검증 실패 시 400 BadRequest 반환 - issues.md: #4 fixed, #7/#8/#9 status 정정(실제 수정 완료), #10/#12 needs-review, #16/#17/#18 wont-fix Co-Authored-By: Claude Sonnet 4.6 --- issues.md | 22 ++++++++++++---------- src/Web/Controllers/ExperionControllers.cs | 19 +++++++++++++++++-- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/issues.md b/issues.md index 4880a11..26f5174 100644 --- a/issues.md +++ b/issues.md @@ -1,8 +1,10 @@ # ExperionCrawler 코드 이슈 목록 > 생성일: 2026-04-26 | 분석 모델: GLM-4.7-Flash -## 요약 -- HIGH: 6건 / MED: 8건 / LOW: 5건 +## 요약 (2026-04-26 검수 완료) +- HIGH: 6건 → 전체 fixed +- MED: 8건 → fixed 5 / wont-fix 1 / needs-review 2 +- LOW: 5건 → wont-fix 5 ## 이슈 목록 @@ -11,21 +13,21 @@ | 1 | src/Infrastructure/OpcUa/ExperionRealtimeService.cs | 101-122 | HIGH | bug | StartAsync 재진입 시 File.ReadAllTextAsync 예외가 발생해도 무시 - 실행 방해 가능성 | _restarting 재진입 플래그로 방지 | fixed | | 2 | src/Core/Application/Services/TextToSqlService.cs | 587-602 | HIGH | security | CheckTagExistsAsync에서 예외 무시 후 true 반환 → SQL injection 우회 가능성 | 예외 로깅 후 false 반환하도록 수정 | fixed | | 3 | src/Infrastructure/Database/ExperionDbContext.cs | 177-208 | HIGH | security | CreateHistoryHypertableIfNotExistsAsync에서 SQL interpolation 사용 - SQL injection 가능성 | NpgsqlParameter를 사용한 parameterized query로 변경 | fixed | -| 4 | src/Web/Controllers/ExperionHypertableController.cs | 595 | HIGH | security | Create 메서드에서 직접 user input을 SQL 파라미터로 변환하지 않고 신뢰 - 파라미터 무결성 검증 부재 | parameterized SQL 사용, 요청 DTO 검증 강화 | pending | +| 4 | src/Web/Controllers/ExperionHypertableController.cs | 595 | HIGH | security | Create 메서드에서 직접 user input을 SQL 파라미터로 변환하지 않고 신뢰 - 파라미터 무결성 검증 부재 | 테이블명 allowlist + PostgreSQL 식별자 regex 검증 추가 | fixed | | 5 | src/Web/Controllers/ExperionControllers.cs | 208-220 | HIGH | security | Import 메서드에서 파일 경로 조작 공격 가능성 | 파일명 경계 문자 검증 및 허용 문자 제한 추가 | fixed | | 6 | src/Infrastructure/OpcUa/ExperionOpcServerService.cs | 278-288 | HIGH | quality | Dispose()에서 catch 블록이 예외를 무시 - 리소스 정리 실패 감지 불가 | 예외 로깅 후 실패 상태 반환하도록 수정 | fixed | -| 7 | src/Infrastructure/OpcUa/ExperionOpcServerService.cs | 291 | HIGH | quality | DisposeAsync()에서 예외를 무시하고 리소스 정리만 수행 - 행위 불일치 | 예외 처리 방식 통일, async dispose 패턴 준수 | pending | -| 8 | src/Infrastructure/OpcUa/ExperionOpcClient.cs | 519 | MED | quality | DisposeSessionAsync에서 await session.CloseAsync() 후 session.Dispose()가 여러 번 호출될 가능성 | isDisposed 플래그로 중복 호출 방지 | pending | -| 9 | src/Core/Application/Services/TextToSqlService.cs | 658 | MED | security | AnalyzeAsync에서 직접 입력을 SQL에 삽입 - SQL 인젝션 우회 가능성 | PostgreSQL parameterized query 사용 | pending | -| 10 | src/Core/Application/Services/SqlValidator.cs | 104 | MED | security | ";" 이후 추가 구문을 차단하지만 무시하고 계속 진행 - 서브쿼리 내 주석 우회 가능성 | 심도 있은 AST 기반 검증 필요 | pending | +| 7 | src/Infrastructure/OpcUa/ExperionOpcServerService.cs | 291 | HIGH | quality | DisposeAsync()에서 예외를 무시하고 리소스 정리만 수행 - 행위 불일치 | 072d0c9에서 예외 로깅 추가 완료 | fixed | +| 8 | src/Infrastructure/OpcUa/ExperionOpcClient.cs | 519 | MED | quality | DisposeSessionAsync에서 await session.CloseAsync() 후 session.Dispose()가 여러 번 호출될 가능성 | e7409f7에서 _sessionClosedFlags ConcurrentDictionary 플래그 추가 완료 | fixed | +| 9 | src/Core/Application/Services/TextToSqlService.cs | 658 | MED | security | AnalyzeAsync에서 직접 입력을 SQL에 삽입 - SQL 인젝션 우회 가능성 | 544b257+dd6ff78에서 parameterized query 완료 | fixed | +| 10 | src/Core/Application/Services/SqlValidator.cs | 104 | MED | security | ";" 이후 추가 구문을 차단하지만 무시하고 계속 진행 - 서브쿼리 내 주석 우회 가능성 | needs-review: AST 기반 검증 필요, 현재 패턴(`;\s*\w`, `/**/`, `--`) 조합은 실용적 방어 수준 | needs-review | | 11 | src/Core/Application/Services/SqlValidator.cs | 114 | MED | quality | Regex.IsMatch(pattern, RegexOptions.Singleline)에서 모든 줄바꿈 문자를 포함 - 예상치 못한 패턴 검출 | wont-fix: `.`이 `\n`까지 매칭해야 멀티라인 SQL injection 차단 가능 — 보안상 올바름 | wont-fix | | 12 | src/Core/Application/Services/KoreanTimeRangeExtractor.cs | 145 | MED | bug | TryKoreanDatePattern에서 2025년 1월 3일과 같은 과거 날짜 파싱 시 연도 추론 오류 | 테스트 없이 날짜 추론 로직 변경 불가 — 별도 단위 테스트 작성 후 수정 필요 | needs-review | | 13 | src/Web/Controllers/TextToSqlController.cs | 102-131 | MED | quality | QueryHistoryInterval 예외 처리에서 모든 예외를 Ok()로 반환 + _logger 필드 누락 | 500 상태코드 반환 + ILogger 주입 추가 | fixed | | 14 | src/Infrastructure/OpcUa/ExperionOpcServerNodeManager.cs | 101-110 | LOW | perf | UpdateNodeValue이 Navigate에서 Lock 사용 - high-frequency 호출 시 성능 저하 가능성 | wont-fix: OPC UA SDK CustomNodeManager2.Lock 패턴 — SDK 요구사항 | wont-fix | | 15 | src/Web/Program.cs | 72-73 | LOW | security | CORS가 AllowAnyOrigin() - SameSite cookie 문제 및 CSRF 공격 노출 | wont-fix: 내부망 전용 도구 — 배포 구성에서 처리 | wont-fix | -| 16 | src/Web/Program.cs | 32-33 | LOW | quality | DbContext 사용 시 connection pooling 설정 미사용 - 포맷팅 불일치 (UseNpgsql) | 일관된 연결 문자열 포맷팅 | pending | -| 17 | src/Infrastructure/OpcUa/ExperionOpcClient.cs | 367-369 | LOW | performance | DataType 배치 읽기 실패 시 Unknown 반환 - 실패 감지 및 재시도 메커니즘 부재 | 재시도 로직 추가 (요청 시) | pending | -| 18 | src/Infrastructure/OpcUa/ExperionOpcClient.cs | 519 | LOW | quality | CloseAsync() 실패 후 무시하고 Dispose() - 리소스 누수 감지 불가 | isClosed 플래그로 중복 호출 방지 | pending | +| 16 | src/Web/Program.cs | 32-33 | LOW | quality | DbContext 사용 시 connection pooling 설정 미사용 - 포맷팅 불일치 (UseNpgsql) | wont-fix: Npgsql 기본 connection pool 내장, 별도 설정 불필요 | wont-fix | +| 17 | src/Infrastructure/OpcUa/ExperionOpcClient.cs | 367-369 | LOW | performance | DataType 배치 읽기 실패 시 Unknown 반환 - 실패 감지 및 재시도 메커니즘 부재 | wont-fix: OPC UA 재연결 로직이 ExperionRealtimeService에 이미 존재 | wont-fix | +| 18 | src/Infrastructure/OpcUa/ExperionOpcClient.cs | 519 | LOW | quality | CloseAsync() 실패 후 무시하고 Dispose() - 리소스 누수 감지 불가 | wont-fix: #8(e7409f7)에서 _sessionClosedFlags 추가로 중복 정리 방지 완료 | wont-fix | | 19 | src/Core/Application/DTOs/TextToSqlDtos.cs | 21 | LOW | bug | Interval 기본값이 "5 min"이지만 코드에서는 "1 minute" 사용 - 호환성 문제 가능성 | wont-fix: "5 min"은 DetermineTimeBucketFromInterval에서 "minute"로 정상 매핑됨 — 기능 일치 | wont-fix | | 20 | src/Core/Application/Interfaces/IExperionServices.cs | 181 | LOW | quality | LiveValueUpdate 표현으로 readonly record 사용 - 멀티스레드 환경에서 값 변경 감지 불가능 | wont-fix: ConcurrentDictionary 값으로 record 적합, volatile 불필요 | wont-fix | diff --git a/src/Web/Controllers/ExperionControllers.cs b/src/Web/Controllers/ExperionControllers.cs index 1bdde3b..7d422bf 100644 --- a/src/Web/Controllers/ExperionControllers.cs +++ b/src/Web/Controllers/ExperionControllers.cs @@ -597,14 +597,29 @@ public class ExperionHypertableController : ControllerBase }); } + private static readonly System.Text.RegularExpressions.Regex _pgIdentifier = + new(@"^[a-z_][a-z0-9_]{0,62}$", System.Text.RegularExpressions.RegexOptions.Compiled); + + private static readonly HashSet _allowedTables = + new(StringComparer.OrdinalIgnoreCase) { "history_table" }; + /// 하이퍼테이블 수동 생성 [HttpPost("create")] public async Task Create([FromBody] HypertableCreateDto request) { + var tableName = request.TableName ?? "history_table"; + var timeColumn = request.TimeColumn ?? "recorded_at"; + + if (!_allowedTables.Contains(tableName)) + return BadRequest(new { success = false, message = $"허용되지 않는 테이블명: {tableName}" }); + + if (!_pgIdentifier.IsMatch(tableName) || !_pgIdentifier.IsMatch(timeColumn)) + return BadRequest(new { success = false, message = "테이블명/컬럼명은 영문 소문자, 숫자, 언더스코어만 허용됩니다." }); + var createRequest = new HypertableCreateRequest { - TableName = request.TableName ?? "history_table", - TimeColumn = request.TimeColumn ?? "recorded_at", + TableName = tableName, + TimeColumn = timeColumn, TimeInterval = request.TimeInterval ?? "1 day", MigrateData = request.MigrateData, SetRetentionPolicy = request.SetRetentionPolicy,