# 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*
