- 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 업데이트 - 정리: 진단 체크리스트 문서 삭제
8.2 KiB
T2SQL 보안 진단 보고서 — Gemma 4 반박
결론: Gemma 4의 CRITICAL 발견은 False Positive
Gemma 4 보고서는 QueryWithNl 경로에서 SQL 검증이 누락되었다고 주장함. 그러나 코드 기반 조사 결과, Python MCP 서버 측에서 동등한 검증이 존재함을 확인함.
STEP 1: 기능 아키텍처
Text-to-SQL 기능은 두 경로를 가짐:
- C# 직접 경로:
TextToSqlController.QueryWithNl()→TextToSqlService→SqlValidator→ PostgreSQL - 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)
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)
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로 분류함. 그러나:
- Python MCP 서버는
_validate_sql()을 통해 동등한 검증을 수행함 (server.pyL573-589) _execute_sql_internal()은 모든 SQL 실행 전에 검증을 강제함 (server.pyL907-914)- 검증 규칙은 C#
SqlValidator와 동일함: 키워드 차단, SELECT-only, 다중 문장 차단, 파일 경로 차단, 길이 제한 - 추가 보안 조치: auto-LIMIT (
_apply_sql_guards), statement_timeout 적용
실제 보안 상태
| 항목 | 상태 |
|---|---|
| SQL Injection | 차단됨 (키워드 기반 검증) |
| 데이터 삭제/수정 | 차단됨 (DROP/DELETE/UPDATE 차단) |
| 다중 문장 실행 | 차단됨 (세미콜론 차단) |
| 파일 접근 | 차단됨 (../~ 차단) |
| 과도한 데이터 반환 | 차단됨 (auto-LIMIT) |
| 무한 실행 | 차단됨 (statement_timeout) |
개선 제안 (선택적)
- Python 검증 로직을 C#과 동기화: 두 검증기가 동일한 키워드 목록과 규칙을 사용하도록 유지
- LLM 출력 검증 강화: LLM이 생성한 SQL에 대한 추가 검증 (예: 테이블명 화이트리스트)
- 감사 로깅: 실행된 SQL을 로깅하여 이상 패턴 감지
결론
Gemma 4의 CRITICAL 발견은 False Positive임. MCP 경로는 Python MCP 서버 측에서 동등한 SQL 검증을 수행하며, 알려진 SQL Injection 공격 벡터에 대해 적절히 보호됨. 추가적인 보안 조치는 선택적이며, 현재 상태에서는 심각한 보안 취약점이 없음.
진단일: 2026-05-17 | 프로토콜: diagnosis-checklist.md 8-Step | 검증 범위: C# + Python MCP Server