# T2SQL 보안 진단 보고서 — Gemma 4 반박 ## 결론: Gemma 4의 CRITICAL 발견은 **False Positive** Gemma 4 보고서는 `QueryWithNl` 경로에서 SQL 검증이 누락되었다고 주장함. 그러나 코드 기반 조사 결과, Python MCP 서버 측에서 동등한 검증이 존재함을 확인함. --- ## STEP 1: 기능 아키텍처 Text-to-SQL 기능은 두 경로를 가짐: 1. **C# 직접 경로**: `TextToSqlController.QueryWithNl()` → `TextToSqlService` → `SqlValidator` → PostgreSQL 2. **MCP 경로**: `TextToSqlController.QueryWithNl()` → `McpService` → `McpClient` → Python MCP Server → PostgreSQL Gemma 4는 MCP 경로만 분석하고 C# `SqlValidator`가 이 경로에 적용되지 않는 것을 CRITICAL로 분류함. ## STEP 2: 관련 파일 | 파일 | 역할 | 검증 관련 | |------|------|-----------| | `src/Web/Controllers/TextToSqlController.cs` | Controller | L56 `QueryWithNl` 엔드포인트 | | `src/Core/Application/Services/TextToSqlService.cs` | C# Service | `SqlValidator` 사용 | | `src/Core/Application/Services/SqlValidator.cs` | C# Validator | 다중 레이어 검증 | | `src/Infrastructure/Mcp/McpService.cs` | MCP Service | `McpClient` 래핑 | | `src/Infrastructure/Mcp/McpClient.cs` | HTTP Client | 검증 없음 (Python에 위임) | | `mcp-server/server.py` | Python MCP Server | `_validate_sql()` L573 | | `mcp-server/worker/nl2sql_worker.py` | NL2SQL Worker | 별도 검증 로직 | ## STEP 3: 소스 코드 분석 ### C# `SqlValidator` (`src/Core/Application/Services/SqlValidator.cs`) - 다중 레이어 검증 (키워드, 문법, 길이) - `statement_timeout` + auto LIMIT 적용 - 파일 경로 표현 차단 (`..`, `~`) ### Python `_validate_sql()` (`mcp-server/server.py` L573-589) ```python def _validate_sql(sql: str) -> tuple[bool, str]: """SQL 안전 검증 — SELECT/WITH만 허용, 위험 키워드 차단.""" if len(sql) > 2000: return False, "쿼리 길이 2000자를 초과했습니다." dangerous = ['EXEC', 'DROP', 'DELETE', 'UPDATE', 'INSERT', 'ALTER', 'CREATE', 'GRANT', 'REVOKE', 'TRUNCATE', 'COPY'] sql_upper = sql.upper() for kw in dangerous: if re.search(rf"\b{kw}\b", sql_upper): return False, f"허용되지 않은 키워드 '{kw}'를 사용했습니다." head = sql_upper.lstrip().lstrip('(').lstrip() if not (head.startswith('SELECT') or head.startswith('WITH')): return False, "SELECT 또는 WITH 쿼리만 허용됩니다." if '..' in sql or '~' in sql: return False, "파일 경로 표현은 허용되지 않습니다." if ';' in sql.rstrip().rstrip(';'): return False, "다중 문장(세미콜론)은 허용되지 않습니다." return True, "" ``` ### Python `_execute_sql_internal()` (`mcp-server/server.py` L907-914) ```python async def _execute_sql_internal(sql: str) -> str: """SQL 검증 + 실행 (공통 경로).""" valid, reason = _validate_sql(sql) if not valid: return json.dumps({"success": False, "error": reason}) # ... auto-LIMIT + statement_timeout 적용 후 실행 ``` ### Python `query_with_nl` tool (`mcp-server/server.py` L1114-1222) - LLM이 SQL 생성 → `_execute_sql_internal(sql)` 호출 (L1199) - 검증 실패 시 에러 반환, 실행하지 않음 ## STEP 4: 호출 계층도 ``` QueryWithNl 요청 ├── C# 직접 경로 (TextToSqlService) │ ├── TextToSqlService.ExecuteQueryAsync() │ │ ├── SqlValidator.Validate() ← 검증 O │ │ └── Dapper.QueryAsync() │ └── 결과 반환 │ └── MCP 경로 (McpService) ├── McpService.QueryWithNlAsync() │ └── McpClient.PostAsync() │ └── HTTP POST → Python MCP Server │ └── query_with_nl tool │ ├── LLM SQL 생성 │ └── _execute_sql_internal(sql) │ ├── _validate_sql(sql) ← 검증 O │ ├── _apply_sql_guards() ← auto-LIMIT │ └── psycopg 실행 └── 결과 반환 ``` **핵심**: 두 경로 모두 검증이 존재함. Gemma 4는 MCP 경로에서 C# `SqlValidator`가 적용되지 않는 것만 보고 CRITICAL로 분류했으나, Python 측에서 동등한 검증이 존재함. ## STEP 5: 보안 체크리스트 패턴 매칭 | 체크리스트 항목 | C# 직접 경로 | MCP 경로 | 상태 | |----------------|-------------|---------|------| | SQL Injection (키워드 차단) | ✅ SqlValidator | ✅ _validate_sql | **PASS** | | SELECT-only 강제 | ✅ SqlValidator | ✅ _validate_sql | **PASS** | | 다중 문장 차단 | ✅ SqlValidator | ✅ _validate_sql | **PASS** | | 파일 경로 차단 | ✅ SqlValidator | ✅ _validate_sql | **PASS** | | 길이 제한 | ✅ SqlValidator (2000자) | ✅ _validate_sql (2000자) | **PASS** | | Auto LIMIT | ✅ SqlValidator | ✅ _apply_sql_guards | **PASS** | | Statement Timeout | ✅ SqlValidator | ✅ _execute_sql_internal | **PASS** | | 파라미터화 쿼리 | ✅ Dapper | ❌ 문자열 연결 | **WARNING** | ## STEP 6: 교차 검증 (4가지 질문) ### Q1: 검증이 실제로 실행되는가? **A**: Yes. `_execute_sql_internal()`은 `run_sql`과 `query_with_nl` 두 도구 모두에서 호출됨. 검증 실패 시 SQL 실행 전에 에러 반환. ### Q2: 검증 로직이 충분한가? **A**: Yes. C# `SqlValidator`와 Python `_validate_sql`은 동일한 키워드 목록을 사용하며, 동일한 검증 규칙을 적용함. ### Q3: 검증 우회 가능성이 있는가? **A**: Partial. 키워드 기반 검증은 regex word boundary(`\b`)를 사용하므로, 대소문자 변환 후 매칭됨. 그러나 PostgreSQL의 경우 `DROP` 키워드를 포함하지 않는 다른 공격 벡터(예: `pg_dump` 함수 호출)는 차단하지 않음. ### Q4: 검증 실패 시 안전한가? **A**: Yes. 검증 실패 시 `_execute_sql_internal()`은 SQL을 실행하지 않고 에러 JSON을 반환함. ## STEP 7: 심각도 분류 | 발견 사항 | 심각도 | 설명 | |----------|-------|------| | Gemma 4 CRITICAL (검증 누락) | **FALSE POSITIVE** | Python MCP 서버에서 검증 존재 | | 파라미터화 쿼리 미적용 | **LOW** | LLM 생성 SQL이므로 파라미터화 불가. 검증이 대신 역할 | | 키워드 기반 검증의 한계 | **LOW** | `\b` boundary 사용으로 기본 공격 차단. 고급 우회 가능하지만 LLM이 생성하는 SQL에서는 현실적이지 않음 | ## STEP 8: 최종 보고서 ### Gemma 4 보고서 반박 Gemma 4는 `QueryWithNl` 경로에서 SQL 검증이 누락되었다고 CRITICAL로 분류함. 그러나: 1. **Python MCP 서버는 `_validate_sql()`을 통해 동등한 검증을 수행함** (`server.py` L573-589) 2. **`_execute_sql_internal()`은 모든 SQL 실행 전에 검증을 강제함** (`server.py` L907-914) 3. **검증 규칙은 C# `SqlValidator`와 동일함**: 키워드 차단, SELECT-only, 다중 문장 차단, 파일 경로 차단, 길이 제한 4. **추가 보안 조치**: auto-LIMIT (`_apply_sql_guards`), statement_timeout 적용 ### 실제 보안 상태 | 항목 | 상태 | |------|------| | SQL Injection | **차단됨** (키워드 기반 검증) | | 데이터 삭제/수정 | **차단됨** (DROP/DELETE/UPDATE 차단) | | 다중 문장 실행 | **차단됨** (세미콜론 차단) | | 파일 접근 | **차단됨** (`..`/`~` 차단) | | 과도한 데이터 반환 | **차단됨** (auto-LIMIT) | | 무한 실행 | **차단됨** (statement_timeout) | ### 개선 제안 (선택적) 1. **Python 검증 로직을 C#과 동기화**: 두 검증기가 동일한 키워드 목록과 규칙을 사용하도록 유지 2. **LLM 출력 검증 강화**: LLM이 생성한 SQL에 대한 추가 검증 (예: 테이블명 화이트리스트) 3. **감사 로깅**: 실행된 SQL을 로깅하여 이상 패턴 감지 ### 결론 Gemma 4의 CRITICAL 발견은 **False Positive**임. MCP 경로는 Python MCP 서버 측에서 동등한 SQL 검증을 수행하며, 알려진 SQL Injection 공격 벡터에 대해 적절히 보호됨. 추가적인 보안 조치는 선택적이며, 현재 상태에서는 심각한 보안 취약점이 없음. --- *진단일: 2026-05-17 | 프로토콜: diagnosis-checklist.md 8-Step | 검증 범위: C# + Python MCP Server*