AI 생성 코드에서의 SQL Injection: 왜 Cursor는 이를 계속 작성하는가
요약
AI 에디터인 Cursor가 생성하는 코드에서 SQL Injection 취약점이 발생하는 원인과 해결 방법을 다룹니다. AI 모델이 학습 데이터의 패턴을 따라 문자열 보간 방식을 선호하기 때문에 발생하는 보안 문제를 지적합니다.
핵심 포인트
- AI 에디터는 학습 데이터의 영향으로 SQL 작성 시 문자열 보간을 선호함
- 문자열 보간 방식은 코드 리뷰 시에도 취약점이 눈에 잘 띄지 않음
- 매개변수화된 쿼리(Parameterized Queries)를 사용하여 보안 문제를 해결해야 함
- AI는 보안 추론이 아닌 패턴 매칭을 수행하므로 개발자의 검토가 필수적임
요약 (TL;DR)
- AI 에디터는 SQL 작성 시 문자열 보간 (String Interpolation)을 선호합니다. 이것이 바로 인젝션 (Injection)이 발생하는 방식입니다.
- 쿼리가 올바르게 "읽히기" 때문에 코드 리뷰 과정에서 이 버그는 눈에 보이지 않습니다.
- 한 가지 변화로 해결할 수 있습니다: 매개변수화된 쿼리 (Parameterized Queries)를 사용하세요. 템플릿 리터럴 (Template Literals)로 SQL을 구축하지 마세요.
지난주에 Cursor에게 로그인 엔드포인트 (Endpoint)를 작성해 달라고 요청했습니다. Cursor는 첫 시도에 바로 작동하는 깔끔하고 읽기 쉬운 코드를 제공했습니다. 하지만 동시에 교과서적인 SQL 인젝션 (SQL Injection) 취약점도 함께 넘겨주었으며, 아주 태연하게 말이죠.
AI가 생성한 데이터베이스 코드에는 이런 특징이 있습니다. 일반적인 입력값으로 테스트할 때는 거의 항상 잘 작동합니다. 실제 이메일을 입력하면 실제 사용자가 반환됩니다. 그대로 배포하게 되죠. 문제는 누군가가 처음부터 이름이나 이메일이 아닌 입력을 입력할 때만 나타납니다.
취약한 코드
이것은 제가 거의 모든 AI 생성 쿼리에서 보는 패턴입니다 (CWE-89):
const email = req.body.email;
const query = `SELECT * FROM users WHERE email = '${email}'`;
const user = await db.query(query);
괜찮아 보입니다. 읽기에도 좋습니다. 이제 누군가 이메일로 다음과 같은 값을 보낼 때 어떤 일이 일어나는지 살펴보세요:
' OR '1'='1
쿼리는 SELECT * FROM users WHERE email = '' OR '1'='1'이 됩니다. 이는 테이블의 모든 행을 반환합니다. 페이로드 (Payload)를 바꾸면 공격자는 테이블을 삭제하거나, 다른 스키마 (Schema)를 읽거나, 전체 사용자 목록을 덤프 (Dump)할 수 있습니다. 특별한 도구도 필요 없습니다. 그저 텍스트 필드와 5초면 충분합니다.
왜 이런 일이 계속 발생하는가
AI 에디터는 수백만 개의 튜토리얼과 StackOverflow 답변으로부터 코딩을 배웠으며, 그 예시들의 상당 부분은 문자열을 단순히 합쳐서 SQL을 구축합니다. 이는 블로그 포스트에서 쿼리를 보여주는 가장 짧은 방법입니다. 모델은 가장 안전한 패턴이 아니라, 가장 짧고 흔한 패턴을 재현합니다. JavaScript의 템플릿 리터럴 (Template Literals)은 보간 (Interpolation) 방식이 너무 깔끔해 보여서 위험성이 완전히 사라져 보이게 만들기 때문에 상황을 더 악화시킵니다.
모델은 악의적인 사용자에 대해 추론하는 것이 아닙니다. 모델은 쿼리가 "보통 어떻게 생겼는지"에 대해 패턴 매칭 (Pattern-matching)을 수행할 뿐입니다. 학습 데이터 중 그 누구도 보간된 버전을 위험한 것으로 라벨링 (Labeling)하지 않았습니다.
해결 방법
사용자 입력값을 SQL 문자열에 절대 직접 넣지 마세요. 입력값을 파라미터 (Parameter)로 별도 전달하여 드라이버 (Driver)가 이스케이프 (Escaping) 처리를 수행하도록 하세요:
const email = req.body.email;
const query = 'SELECT * FROM users WHERE email = $1';
const user = await db.query(query, [email]);
이제 데이터베이스는 이메일을 실행 가능한 SQL이 아닌 엄격한 값 (Value)으로 취급합니다. 악성 페이로드 (Payload)는 해당 문자열과 정확히 일치하는 이메일을 가진 사용자를 찾는 리터럴 (Literal) 검색이 됩니다. 결과는 아무것도 매칭되지 않으며, 공격은 무력화됩니다.
Python의 psycopg2를 사용할 때도 동일한 규칙이 적용됩니다:
cur.execute("SELECT * FROM users WHERE email = %s", (email,))
Prisma, Drizzle 또는 SQLAlchemy와 같은 ORM을 사용한다면, 로우 쿼리 (Raw query) 헬퍼를 직접 호출하기 전까지는 파라미터화 (Parameterization)가 자동으로 처리됩니다. 문제가 발생하는 지점은 바로 로우 쿼리를 호출할 때 보간 (Interpolation)이 다시 몰래 끼어든다는 것입니다. 따라서 코드베이스에서 로우 쿼리 호출 부분을 중점적으로 grep 하세요.
저는 이를 위해 SafeWeave를 사용하고 있습니다. 이 도구는 MCP 서버로서 Cursor 및 Claude Code에 연결되어, 제가 다음 파일로 넘어가기 전 보간된 SQL이 생성되는 즉시 이를 플래그 (Flag) 처리합니다. 그렇다 하더라도, semgrep을 이용한 기본적인 프리 커밋 훅 (Pre-commit hook)만으로도 이 포스트에서 다룬 대부분의 사례를 잡아낼 수 있습니다. 중요한 것은 어떤 도구를 사용하든 최대한 조기에 이를 포착하는 것입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기