Skip to content

보안 모델 (한국어)

🌐 SECURITY_MODEL.md의 번역입니다. 영어 원문이 표준이며, 페이지 번역은 경고 수준의 동기화 규칙을 따릅니다.

English: Security Model

cubrid-mcp-server는 CUBRID 데이터베이스와 MCP를 사용하는 LLM 클라이언트를 잇는 다리입니다. 모델이 보내는 모든 쿼리를 신뢰할 수 없는 입력으로 취급하세요: LLM은 파괴적 SQL을 환각할 수 있고, 악의적인 프롬프트는 서버가 노출할 의도가 없던 데이터를 빼내려 할 수 있습니다. 심층 방어가 설계 의도입니다.

계층 1 — 데이터베이스 권한 (필수)

모델이 사용하길 원하는 권한만 가진 전용 최소 권한 CUBRID 사용자로 서버를 실행하세요. 이 계정이 주된 보안 통제이며, 코드 수준의 안전 검사기는 가드레일일 뿐 이것을 대신할 수 없습니다. dba나 스키마 소유자 계정을 재사용하지 마세요.

-- 1. 사용자 생성
CREATE USER mcp_reader PASSWORD 'replace-me';

-- 2. 노출할 테이블에만 SELECT 권한 부여.
--    CUBRID는 테이블 단위 권한 부여입니다 (스키마 전체 db.* 부여는 없음).
GRANT SELECT ON customers TO mcp_reader;
GRANT SELECT ON orders TO mcp_reader;
-- ...모델이 읽을 수 있는 각 테이블마다 반복.

-- 3. CREATE, ALTER, DROP, INSERT, UPDATE, DELETE, GRANT, DBA 역할은 부여 금지.

모델에 필요한 테이블에만 SELECT를 부여하고, 계정에는 필요한 최소 권한만 부여하고 DDL 권한은 부여하지 마세요.

SELECT 권한만으로 모든 쓰기를 막을 수는 없습니다. CUBRID에서는 테이블에 SELECT만 할 수 있는 사용자도 읽기 자체로 데이터를 바꿀 수 있습니다. INCR(col)과 DECR(col)은 읽은 행을 갱신하고, 클릭 카운터 SELECT ... WITH INCREMENT FOR col(또는 WITH DECREMENT FOR col)도 같으며, serial.NEXT_VALUE, serial.NEXTVAL, SERIAL_NEXT_VALUE(serial, n)은 시리얼을 증가시킵니다. 사용자에게 아무 권한도 부여하지 않은 시리얼도 마찬가지입니다. 이 변경은 유지되며, 모든 읽기를 끝내는 롤백으로 되돌려지지 않습니다. 읽기 전용 검사(계층 2)는 이 형태를 거부하며, CUBRID_MCP_READONLY=0일 때도 execute_query는 이를 거부합니다.

추가 강화: 시크릿 매니저의 긴 무작위 비밀번호 사용, 정기적 로테이션, 스키마 전체가 아닌 테이블 단위 부여, MCP 서버 호스트만 접근 가능한 네트워크에 CUBRID 두기. 전체 정책은 SECURITY.md를 참고하세요.

계층 2 — 읽기 전용 SQL 화이트리스트

서버는 기본적으로 읽기 전용입니다. CUBRID_MCP_READONLY=1(기본값)이면 모든 문장이 sqlparse로 파싱되어 SELECT, SHOW, DESC, DESCRIBE, WITH(CTE)가 아니면 거부됩니다. CUBRID에는 EXPLAIN 문이 없으므로 실행 계획은 explain_query 도구를 사용하세요. 다중 문장 입력은 거부됩니다. 주석, 따옴표, 문장 경계는 sqlparse가 아니라 CUBRID 규칙으로 읽습니다. --, //, /* ... */가 주석이고(블록 주석은 중첩되지 않음), #은 주석이 아니며, 따옴표 안의 백슬래시는 일반 문자입니다. 백슬래시가 따옴표를 이스케이프하는지(서버 파라미터 no_backslash_escapes)에 따라 따옴표 경계가 달라지는 입력과 닫히지 않은 따옴표가 있는 입력은 거부됩니다. 문장의 첫 단어 자체도 허용된 키워드여야 합니다. sqlparse는 KILL, EVALUATE처럼 모르는 단어를 이름으로 읽으므로, 그렇지 않으면 KILL (SELECT 1)이 SELECT로 통과합니다. 쓰기 경로도 같은 방식으로 첫 단어를 INSERT, UPDATE, DELETE와 비교합니다.

읽기 전용 모드는 SELECT 안에서 데이터를 바꾸는 CUBRID 형태도 거부합니다. 이 형태는 SELECT 권한만으로 막을 수 없습니다(계층 1 참고). 대상은 함수 INCR, DECR, 시리얼 증가 NEXT_VALUE, NEXTVAL, SERIAL_NEXT_VALUE, 클릭 카운터 WITH INCREMENT FOR / WITH DECREMENT FOR입니다. 이름은 호출될 때((가 뒤따를 때)나 속성으로 쓰일 때(owner.serial.NEXT_VALUE처럼 . 뒤에 올 때) 대소문자와 무관하게, 따옴표 없이든 따옴표 안이든("INCR", [incr], `incr`), 사이에 공백이나 주석이 있어도, 서브쿼리와 CTE를 포함해 문장 어디에서든 찾습니다. 문자열 리터럴과 incr_total, incr#, #incr 같은 더 긴 이름(#은 첫 글자로도 쓰이는 식별자 문자)은 해당하지 않습니다. CURRENT_VALUE, CURRVAL, SERIAL_CURRENT_VALUE는 시리얼을 읽기만 하므로 계속 허용됩니다. 이런 이름의 컬럼은 한정해서(t.next_value) 쓰면 거부되므로 한정 없이 선택하세요.

SQL 키워드 검사는 가드레일이자 심층 방어이며 보안 경계가 아닙니다. 명백한 실수를 막는 클라이언트 측 최선 노력(best-effort) 필터일 뿐이며, 주된 통제는 전용 최소 권한 데이터베이스 계정(계층 1)입니다. 모델에 필요한 테이블에만 SELECT를 부여하세요.

CUBRID_MCP_READONLY=0은 이 화이트리스트를 완화합니다 — 데이터베이스 사용자가 이미 읽기 전용이고 파서가 오판하는 결과 집합 문장이 필요할 때만 사용하세요. 이 설정이 execute_query를 쓰기 경로로 만들지는 않습니다: execute_query는 쓰기·DDL·트랜잭션 제어 키워드(INSERT, UPDATE, DELETE, REPLACE, MERGE, CREATE, ALTER, DROP, TRUNCATE, RENAME, GRANT, REVOKE, COMMIT, ROLLBACK, SAVEPOINT, SET, PREPARE, EXECUTE, DEALLOCATE, DO), 메서드 호출(CALL, EVALUATE)과 KILL로 시작하는 문장(선두 키워드와 문장의 첫 단어로 비교)과 다중 문장·빈 입력을 데이터베이스에 도달하기 전에 거부하고, 커밋하지 않으며, 결과 집합을 반환하지 않는 문장(연결은 유지)과 성공한 모든 읽기를 롤백합니다. explain_query는 이 플래그와 무관하게 항상 읽기 전용 검사를 적용합니다(아래 참고). 이 키워드 검사는 보안 경계가 아니라 가드레일이며, 보안 경계는 여전히 읽기 전용 데이터베이스 계정입니다. 쓰기에는 execute_write(옵트인 쓰기 모드)를 사용하세요. CUBRID_MCP_READONLY=0으로 execute_query를 통해 쓰기를 보내던 클라이언트는 이것으로 전환해야 합니다. CALL login(...) ON CLASS db_user는 캐시된 공유 세션의 사용자를 이후의 모든 도구 호출(execute_write 포함)에 대해 바꾸므로 CALL을 거부합니다. execute_write(INSERT/UPDATE/DELETE만)와 explain_query(SELECT/WITH만)도 이를 거부하므로 어떤 도구로도 세션 사용자를 바꿀 수 없습니다. KILL은 다른 세션의 트랜잭션을 종료합니다. LOCK은 CUBRID 문장이 아니고 GET은 서버 설정을 읽기만 하므로 이 검사는 둘을 거부하지 않습니다.

CUBRID_MCP_READONLY=0이어도 execute_query는 읽기 전용 모드와 같은 방식으로 INCR, DECR, 시리얼 증가, 클릭 카운터를 거부합니다. 이들은 결과 집합을 반환하는 문장에서도 데이터를 바꾸며, 쓰기는 execute_write의 몫입니다.

계층 2b — 옵트인 쓰기 모드 (CUBRID_MCP_WRITE)

쓰기 접근은 기본적으로 꺼져 있습니다. CUBRID_MCP_WRITE=1을 설정하면 별도의 execute_write 도구가 등록되며 다음으로 제한됩니다:

  • DML 전용. 전용 화이트리스트(ensure_write_allowed)가 단일 INSERT/UPDATE/DELETE만 허용하고, 독립 실행형 읽기·DDL·트랜잭션 제어·다중 문장 입력을 거부합니다. (단일 DML 안의 서브쿼리, 예컨대 INSERT ... SELECT는 합법적으로 허용됩니다.)
  • 원자적 트랜잭션. 문장은 명시적 트랜잭션에서 실행됩니다 — 성공 시 커밋, 어떤 실패든 롤백.
  • DDL은 의도적으로 미지원. execute_write는 DML 도구(INSERT/UPDATE/DELETE)이고 execute_query는 읽기 전용이므로 CREATE/ALTER/DROP/TRUNCATE는 양쪽 모두에서 거부됩니다. (서버는 autocommit을 끈 pycubrid를 사용하며, 테스트한 CUBRID 버전(10.2, 11.2, 11.4)에서 DDL은 트랜잭션에 속해 커밋하지 않으면 롤백됩니다. 따라서 이 제외는 DDL 자동 커밋이 아니라 도구 계약에 근거합니다.)
  • 비활성 시 미등록. 쓰기 모드가 꺼져 있으면 execute_write는 MCP 기능 탐색에 나타나지 않습니다 — 도구 자체가 제공되지 않습니다. SQL 검사는 가드레일이므로 실제로 쓰기를 제한하는 것은 여전히 데이터베이스 계정입니다.
  • 연결별 게이팅. 멀티커넥션에서 어떤 연결이 쓰기를 켜면 도구가 등록되지만, 매 쓰기는 대상 연결의 설정으로 강제됩니다 — 쓰기가 꺼진 연결은 다른 연결이 켜져 있어도 거부합니다.
  • execute_query는 쓰기 플래그의 영향을 받지 않으며, 쓰기 모드가 이를 쓰기 경로로 만들지 않습니다(계층 2의 SELECT 부작용은 CUBRID_MCP_READONLY=0일 때도 거부합니다).

explain_query는 문장을 실행합니다

explain_query는 계획만 세우지 않습니다: SELECT/WITH 문을 SET TRACE ON 아래에서 실행하고 SHOW TRACE를 읽은 뒤, 트레이스를 끄고 트랜잭션을 롤백합니다. 따라서 문장은 실제 데이터베이스 자원을 사용하고(쿼리 자체만큼 오래 걸릴 수 있음), execute_query와 동일한 키워드 검사 및 데이터베이스 권한이 적용됩니다. 롤백은 서버의 최선 노력 정리 단계이며 최소 권한 계정을 대신하지 않습니다. 정리 단계가 하나라도 실패하면 서버는 트레이스가 켜져 있거나 트랜잭션이 열려 있을 수 있는 세션을 재사용하지 않고 연결을 폐기하며, 다음 호출에서 다시 연결합니다.

계층 3 — 출력 제한

execute_query는 행을 CUBRID_MCP_MAX_ROWS(기본 1000)로, 렌더링된 출력을 CUBRID_MCP_MAX_CHARS(기본 4000)로 제한합니다. 이는 모델의 컨텍스트 창을 보호하고 한 번의 탐색 쿼리가 빼낼 수 있는 데이터량을 제한합니다. 바이너리 값은 작으면 base64로, 크면 요약됩니다(<binary N bytes>).

계층 4 — 감사 로그 (옵트인)

CUBRID_MCP_AUDIT_LOG=1을 설정하면 실행된 문장(execute_query, explain_query, execute_write)마다 stderr에 구조화된 JSON 기록 한 줄이 남습니다. 기본은 꺼짐이며 연결별로 적용됩니다(CUBRID_<NAME>_MCP_AUDIT_LOG).

필드 설명
tool 문장을 실행한 MCP 도구
status ok 또는 error
category 선행 SQL 키워드만 (예: SELECT, WITH), 주석과 문자열 리터럴을 제거한 뒤 읽음 — 전체 문장은 절대 기록 안 함
identifiers FROM/JOIN 뒤에서 식별자 정규식으로 추출한 테이블 이름. 식별자가 아닌 것(값, 리터럴, 표현식)은 버려짐
sql_length 제출된 SQL의 글자 수
row_count, truncated 결과 크기 및 잘림 여부 (성공 시만)
duration_ms 호출 소요 시간 (정수 밀리초)
error_type 실패 시 예외 클래스 이름만

원본 SQL 텍스트, 바인드 파라미터, 리터럴 값은 절대 기록되지 않습니다 — 쿼리에 박힌 비밀이 감사 스트림으로 새지 않습니다.

로깅 규율

서버는 MCP stdio를 사용합니다: stdout은 JSON-RPC 프로토콜 스트림으로 예약되어 있어 모든 로그는 stderr로 출력됩니다(기본 레벨 INFO). LLM 클라이언트에 반환되는 오류는 민감정보 제거(redaction) 처리됩니다 — 예외 카테고리(예: query failed: OperationalError)만 반환되고 전체 세부 정보는 운영자를 위해 stderr에 기록됩니다. 연결 실패는 고정 메시지 failed to connect to CUBRID만 반환하고 health_check는 예외 클래스 이름만 반환하며, 호스트, 포트, 데이터베이스, 드라이버 원인은 stderr에만 기록됩니다. 스키마 세부 사항, 호스트명, SQL 조각, 설정 값이 클라이언트에 노출되지 않습니다.