AI 에이전트에게 데이터베이스 접근 권한을 1034번 부여해 보았습니다. Text-to-SQL은 23번의 안전하지 않은 작업을 수행했지만
요약
AI 에이전트의 데이터베이스 접근 시 발생하는 보안 문제를 해결하기 위해 SQL 프롬프트 대신 ORM 계층에서 안전성을 강제하는 'ormai'를 소개합니다. Text-to-SQL 방식의 취약점을 보완하여 SQL 인젝션을 방지하고 정책 기반의 런타임 제어를 제공합니다.
핵심 포인트
- Text-to-SQL 방식은 SQL 인젝션 및 안전하지 않은 작업에 취약함
- ormai는 ORM 계층에서 타입이 지정된 도구(typed tools)를 제공하여 보안 강화
- 매개변수화된 ORM 쿼리 컴파일을 통해 SQL 인젝션 원천 차단
- 필드 수준 정책, 쿼리 예산, 테넌트 범위 필터링 등 강력한 런타임 제어 기능 제공
AI 에이전트에게 데이터베이스 접근 권한을 부여하는 순간, 여러분은 운영 환경(production) 자격 증명을 가진 주니어 엔지니어가 제기할 수 있는 모든 질문을 떠안게 됩니다. 다만 에이전트는 지치지 않으며, 허락을 구하지도 않는다는 점이 다릅니다.
만약 에이전트가 개인정보(PII)로 가득 찬 컬럼을 읽는다면 어떻게 될까요? 테이블 전체를 스캔하여 비용을 발생시키는 쿼리를 작성한다면 어떨까요? 사용자가 프롬프트 인젝션(prompt injection)을 통해 에이전트를 속여 행(row)을 삭제하게 만든다면 어떻게 될까요? 사후에 에이전트가 실제로 무엇을 했는지 어떻게 증명할 수 있을까요?
일반적인 답변은 Text-to-SQL입니다. 모델이 SQL을 작성하게 하고 이를 실행하는 것이죠. 하지만 이는 안전성을 강제하기에 정확히 잘못된 계층(layer)입니다. SQL 문자열을 얻게 되는 시점에는 이미 통제권을 잃었기 때문입니다. 우리는 Spider 데이터셋을 사용하여 1034개의 자연어 쿼리로 벤치마킹을 진행했습니다. 그 결과 Text-to-SQL은 23번의 안전하지 않은 작업(unsafe operations)을 실행했으며 감사 추적(audit trail)을 남기지 않았습니다. SQL 인젝션(SQL injection)도 가능했습니다.
ormai는 다른 접근 방식을 취합니다. 프롬프트 계층(prompt layer)이 아닌 ORM 계층에서 안전성을 강제합니다. 동일한 1034개의 쿼리에 대해 ormai는 안전하지 않은 작업을 0건 실행했으며, SQL 인젝션이 불가능했고, 모든 호출이 로그로 기록되었습니다.
핵심 아이디어: 에이전트에게는 SQL 프롬프트가 아닌 타입이 지정된 도구(typed tools)를 제공합니다
OrmAI는 기존의 ORM 모델을 정책이 강제되는 런타임(runtime)으로 감쌉니다. 에이전트는 원시(raw) SQL을 절대 보거나 작성하지 않습니다. 대신 다음과 같은 작은 세트의 타입이 지정된 도구(typed tools)를 받습니다:
- 읽기 안전(Read-safe):
db.query,db.get,db.aggregate,db.describe_schema - 쓰기 안전(Write-safe):
db.create,db.update,db.delete,db.bulk_update(각 도구는 정책에 의해 제어됨)
에이전트가 만드는 모든 요청은 매개변수화된(parameterized) ORM 쿼리로 컴파일됩니다. 데이터베이스는 에이전트가 작성한 SQL 문자열을 절대 받지 않습니다. 이 단 하나의 아키텍처적 선택이 SQL 인젝션 구멍을 막아줍니다. 주입할 문자열 자체가 존재하지 않기 때문입니다.
작동 원리: 에이전트와 ORM 사이의 런타임
OrmAI는 에이전트와 여러분의 ORM 사이에서 계층(layer)으로 존재합니다. 요청은 데이터베이스에 도달하기 전 세 가지 강제 단계(enforcement stages)를 거칩니다:
Your Agent
| calls a typed tool
OrmAI Runtime
...
**정책 집행기 (policy enforcer)**는 무엇이 허용될지를 결정합니다: 어떤 모델, 어떤 필드, 어떤 작업, 그리고 어느 정도의 양까지 말입니다. 필드 수준의 정책 (Field-level policies)은 비밀번호를 숨기고 이메일을 자동으로 마스킹 (mask) 합니다. 쿼리 예산 (Query budgets)은 행 (rows)의 수를 제한하고, 깊이 (depth)를 포함하며, 문장 타임아웃 (statement timeouts)을 설정하여 제어 불능의 쿼리가 무분별하게 실행되는 것을 방지합니다. 쓰기 (Writes) 작업은 명시적으로 활성화하지 않는 한 꺼져 있으며, 민감한 모델에 대해서는 이유를 요구하거나 사람의 승인을 받도록 설정할 수 있습니다.
**테넌트 범위 필터 (tenant scope filter)**는 모든 쿼리에 테넌트 술어 (tenant predicate)를 자동으로 주입합니다. .tenantScope('tenant_id')는 테넌트 A의 컨텍스트 내에서의 요청이 에이전트가 필터링을 기억했는지 여부와 상관없이 물리적으로 테넌트 B의 행을 반환할 수 없음을 의미합니다. 격리 (Isolation)는 사후에 덧붙여진 것이 아니라, 프롬프트에 맡겨진 것도 아닌, 내장된 기능입니다.
**감사 로거 (audit logger)**는 주체 (principal), 테넌트 (tenant), 추적 ID (trace ID), 입력 (input), 그리고 출력 (output)과 함께 모든 호출을 기록합니다. 누군가 "에이전트가 무엇을 했는가?"라고 물을 때, 당신은 답을 가지고 있게 됩니다.
당신은 이 모든 것을 PolicyBuilder로 기술하며, 이는 당신이 원래 작성하고 싶어 했을 보안 검토 (security review) 문서처럼 읽힙니다:
from ormai.utils import PolicyBuilder, DEFAULT_PROD
policy = (
...
일반적인 보안 태세 (postures)를 위해 프리셋 (Presets)이 제공됩니다: DEFAULT_DEV는 허용적이고, DEFAULT_INTERNAL은 중간 수준이며, DEFAULT_PROD는 엄격합니다. 하나를 선택하여 시작한 뒤 점차 강화해 나가면 됩니다.
구체적인 연결 (A concrete wiring)
Python 경로는 의도적으로 짧게 설계되었습니다. 기존의 SQLAlchemy 엔진 (engine)과 세션 (session)을 지정하고 정책을 전달하기만 하면, 에이전트가 사용할 수 있는 도구 세트 (toolset)를 얻을 수 있습니다:
from ormai.quickstart import mount_sqlalchemy
from ormai.utils import DEFAULT_DEV
...
이것은 Python 전용이 아닙니다. 동일한 빌더 패턴 (builder pattern)을 통해 표현되는 동일한 정책 모델을 가진 TypeScript/Node.js 구현체도 존재하며, 이는 Vercel AI SDK, LangChain.js, MCP 등을 포함한 다양한 도구에 플러그인 형태로 연결됩니다. ORM 측면에서의 지원 범위도 넓습니다. Python에서는 SQLAlchemy, SQLModel, Django ORM, Tortoise, Peewee를 지원하며, TypeScript에서는 Prisma, Drizzle, TypeORM을 지원합니다. 스키마 조사 (Schema introspection)가 자동으로 이루어지므로, 모델, 필드, 관계 및 기본 키 (primary keys)를 사용자가 다시 선언할 필요 없이 스스로 찾아냅니다.
적합하지 않은 경우
의무적인 정직성 섹션입니다.
-
의도적으로 에이전트가 할 수 있는 일을 제한합니다. OrmAI는 안전하고, 타입이 지정되었으며, 정책에 의해 경계가 설정된 접근을 목표로 합니다. 만약 귀하의 유스케이스가 진정으로 임의의 분석용 SQL, 윈도우 함수 (window functions), 재귀적 CTE (recursive CTEs), 수동으로 조정된 조인 (hand-tuned joins)을 필요로 한다면, 타입이 지정된 도구 인터페이스는 마치 감옥처럼 느껴질 것입니다. 이것이 귀하가 선택하는 트레이드오프 (trade-off)입니다: 안전성을 위해 표현력을 양보하는 것입니다.
-
정책의 품질은 귀하가 작성한 만큼만 보장됩니다. OrmAI는 귀하의 규칙을 강제할 뿐입니다. 어떤 필드가 민감한지 스스로 추측하지 않습니다. 비밀 컬럼을 거부하는 것을 잊은 정책을 배포한다면, 런타임 (runtime)은 이를 충실히 노출할 것입니다. 프리셋 (presets)이 도움이 되긴 하지만, 검토의 책임은 여전히 귀하에게 있습니다.
-
요청 경로에 또 다른 계층이 추가됩니다. 요청은 ORM에 도달하기 전 런타임을 통해 컴파일됩니다. 대부분의 에이전트 워크로드에서 이러한 오버헤드는 모델 호출에 비해 무시할 수 있는 수준이지만, 핫 패스 (hot path)에서 순수 데이터베이스 지연 시간 (latency)을 추구하고 있다면 이를 측정해 보십시오.
-
지원되는 ORM 중 하나를 사용한다고 가정합니다. 이 도구의 가치는 기존 ORM을 감싸는(wrapping) 데서 나옵니다. 만약 귀하의 데이터 접근 방식이 직접 작성한 SQL (hand-rolled SQL)이거나 지원되지 않는 ORM이라면, 아직 OrmAI가 감쌀 수 있는 대상이 없습니다.
-
위험을 줄여줄 뿐, 완전히 제거하지는 않습니다. Spider 벤치마크에서 안전하지 않은 작업이 0건이었다는 것은 강력한 신호이지, 모든 스키마와 모든 프롬프트에 대한 증명은 아닙니다. 이를 심층 방어 (defense in depth)의 수단으로 취급하고, 어쨌든 데이터베이스 수준의 권한은 엄격하게 유지하십시오.
핵심 요약 (Takeaways)
- 안전성 (Safety)은 프롬프트 계층 (prompt layer)이 아닌 ORM 계층 (ORM layer)에 속해야 합니다. 모델로부터 SQL 문자열을 받는 순간, 이미 인젝션 (injection) 방어 싸움에서 패배한 것입니다.
- 1034개 쿼리 벤치마크에서 SQL 인젝션 (SQL injection) 문제를 해결한 것은 더 영리한 프롬프트가 아니라, 타입이 지정된 도구 (Typed tools)와 매개변수화된 쿼리 (parameterized queries)였습니다.
- 필드 마스킹 (Field masking), 테넌트 스코핑 (tenant scoping), 그리고 쿼리 예산 (query budgets)은 "에이전트가 데이터베이스를 읽을 수 있다"를 "에이전트가 정책이 허용하는 정확한 내용만 읽을 수 있다"로 바꾸는 세 가지 제어 수단입니다.
- 모든 것을 감사 (Audit)하십시오. "에이전트가 무엇을 했는가"라는 질문에 대한 올바른 답변은 모든 호출에 대해 주체 (principal), 테넌트 (tenant), 그리고 트레이스 (trace)를 기록했을 때만 가능합니다.
코드, Spider 벤치마크 데모, 그리고 정책 문서는 여기에서 확인할 수 있습니다:
https://github.com/neul-labs/ormai
만약 에이전트를 프로덕션 데이터베이스 (production database)에 연결했다면, 현재 테넌트 스코핑 (scoping tenants)을 어떻게 하고 있는지 알고 싶습니다. 마음껏 테스트해 보시고, 이슈 (issues) 환영합니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기