- 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 업데이트 - 정리: 진단 체크리스트 문서 삭제
166 lines
8.2 KiB
Markdown
166 lines
8.2 KiB
Markdown
# 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*
|