AI가 생성한 Python 코드에서의 SQL Injection: 실제 배포 사례
요약
AI가 생성한 Python 코드에서 SQL Injection 취약점이 발견된 실제 사례를 분석합니다. BrassCoders의 벤치마크 결과, 모델이 정상적인 경로(Happy path)를 구현하는 과정에서 보안 검토 없이 위험한 문자열 포매팅 방식을 사용함을 확인했습니다.
핵심 포인트
- AI 생성 코드에서 SQL Injection이 발생하는 구조적 패턴 확인
- 단순한 구현을 위해 Python의 % 연산자를 사용하는 위험성
- 보안 요청이 없는 프롬프트의 경우 모델이 취약한 코드를 생성할 가능성 높음
- Pylint 등 기존 도구와 모델 자체 검토 단계에서도 탐지되지 않을 수 있음
BrassCoders는 자체적으로 발표한 벤치마크 코퍼스에 포함된 15개의 AI 생성 Python 파일 중 2개에서 SQL injection을 발견했습니다. user_lookup.py는 Python의 % 연산자를 사용하여 SQLite 쿼리를 생성했고, bulk_insert.py는 동일한 방식으로 Postgres INSERT 문을 생성했습니다. 두 파일 모두 버그를 의도적으로 심지 않은 현실적인 프롬프트(Prompt)로부터 생성되었습니다. 두 파일 모두 프롬프트 검토를 통과했습니다. Pylint나 모델 자체의 생성 단계에서도 어떠한 경고도 발생하지 않았습니다.
이는 AI가 생성한 코드에서 SQL injection이 나타나는 구조적 패턴이며, 두 파일 모두 동일한 양상을 보입니다.
AI 어시스턴트가 SQL Injection을 생성하는 방식
BrassCoders가 발표한 코퍼스에는 두 개의 SQL injection 사례(Bandit B608)가 포함되어 있습니다. 이는 user_lookup.py와 bulk_insert.py 모두 호출자가 제공한 값을 쿼리 문자열에 직접 삽입하기 위해 Python 문자열 포매팅 (String Formatting)을 사용했기 때문입니다. 모델은 프롬프트가 정상적인 경로(Happy path)를 설명했고, 해당 경로가 안전한 입력값에서는 문제없이 작동했기 때문에 더 단순한 구문을 선택한 것입니다.
user_lookup.py는 "sqlite 데이터베이스에서 id로 사용자 레코드를 JSON으로 반환하는 작은 Flask 엔드포인트를 작성하라"는 프롬프트로부터 생성되었습니다. 모델의 구현은 다음과 같습니다:
@app.route("/user/")
def get_user(user_id):
conn = get_db()
...
% user_id 호출은 Flask 라우트 파라미터를 SQL 문자열에 직접 삽입합니다. /user/1 OR 1=1로 보내는 HTTP 요청은 SELECT id, name, email FROM users WHERE id = 1 OR 1=1을 실행하며, 이는 데이터베이스의 모든 사용자 레코드를 가져옵니다. 이 쿼리는 user_id가 유효한 정수인 모든 테스트 케이스에서는 "작동"합니다. 취약점은 그 외의 모든 입력값에서 존재합니다.
두 번째 파일: Bulk Insert
BrassCoders는 bulk_insert.py에 대해서도 동일한 B608 규칙을 적용하여 경고를 표시했습니다. 이 파일은 "(name, email) 튜플 리스트를 Postgres users 테이블에 대량 삽입(bulk-insert)하는 함수를 작성하라"는 프롬프트로부터 생성되었으며, 삽입 루프의 모든 행에 걸쳐 % 문자열 포매팅을 사용합니다.
구현 내용은 다음과 같습니다:
def bulk_insert_users(conn, users):
cur = conn.cursor()
for name, email in users:
...
이는 %를 통해 name과 email을 보간(interpolation)하여 모든 행에 대해 별도의 SQL 문자열을 생성합니다. '); DROP TABLE users; --와 같은 이름은 INSERT 문을 종료시키고, DROP 문을 실행하며, 나머지 부분을 주석 처리합니다. 이 함수는 ("Ada", "ada@example.com")에 대해서는 올바르게 작동합니다. 하지만 공격자가 제어하는 입력값에 대해 임의의 SQL을 실행하게 됩니다.
모델이 생성 중에 경고하지 않는 이유
모델은 보안 검토를 요청하는 프롬프트가 아니었기 때문에 보안 경고를 내보내지 않고 두 파일 모두를 작성했습니다. AI 코딩 어시스턴트는 프롬프트를 완성합니다. "Flask 엔드포인트를 작성하라"는 요청은 작동하는 엔드포인트를 작성함으로써 완성됩니다. 해당 엔드포인트가 프로덕션(production) 입력에 대해 안전한지는 별개의 문제이며, 사용자가 묻지 않는 한 모델은 이에 대해 답하지 않습니다.
이는 BrassCoders의 벤치마크에서 발견된 생성 모드(generation-mode)의 결과입니다. 모델은 코드를 작성하는 동안 스스로 경고하지 않은 성능 및 보안 버그를 도입했으며, 명시적으로 검토를 요청했을 때 동일한 버그를 찾아냈습니다. 누군가 요청하든 안 하든 모든 커밋(commit) 시 자동으로 실행되는 게이트(gate)는, 사용자가 호출하는 것을 기억해야 하는 모델과는 다른 도구입니다.
Bandit의 B608 규칙은 1초도 안 되어 두 파일 모두에서 경고를 발생시킵니다. API 호출도 필요하지 않습니다. 매 실행마다 동일한 결과를 찾아냅니다.
해결 방법
두 가지 발견 사항 모두 동일한 해결책을 가집니다: 매개변수화된 쿼리 (parameterized queries).
user_lookup.py의 경우:
cur.execute("SELECT id, name, email FROM users WHERE id = ?", (user_id,))
bulk_insert.py의 경우:
cur.execute(
"INSERT INTO users (name, email) VALUES (%s, %s)",
(name, email)
...
? 또는 %s (데이터베이스 드라이버에 따라 다름)는 플레이스홀더(placeholder)이지 포매팅 지시어가 아닙니다. 데이터베이스 드라이버는 값을 쿼리 문자열과 분리하여 전달하므로, user_id나 name에 포함된 어떠한 SQL 구문도 쿼리 구조를 변경할 수 없습니다.
BrassCoders는 탐지 결과와 함께 수정 권고 사항(remediation note)을 출력합니다. Claude Code는 "? 또는 %s 플레이스홀더(placeholder)를 사용하는 매개변수화된 쿼리(parameterized query)로 교체하고, cursor.execute()에 별도의 값 튜플(values tuple)을 전달하십시오"라는 문구를 읽고, 문제가 된 라인으로부터 매개변수화된 버전을 생성합니다. 수정에는 몇 초밖에 걸리지 않지만, 코드 리뷰에서 취약점을 찾아내는 데는 모델이 프롬프트(prompting) 없이는 제공하지 않는 주의력이 필요합니다.
결과 재현하기
git clone https://github.com/CopperSunDev/brasscoders
pip install brasscoders
brasscoders --offline scan brasscoders/docs/benchmarks/ai-code-findings-corpus/files
user_lookup.py 및 bulk_insert.py에 대한 B608 탐지 결과는 .brass/ai_instructions.yaml 파일에 심각도(severity) critical, 파일 경로, 줄 번호, 그리고 매개변수화된 쿼리(parameterized-query) 수정 방법과 함께 나타납니다. 스캔은 1초 미만이 소요되며, 실행할 때마다 동일한 출력을 생성합니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기