LLM 애플리케이션 코드 스캐닝: BrassCoders가 발견하는 것들
요약
BrassCoders는 LLM 애플리케이션의 보안 취약점을 탐지하기 위해 기존 Python 코드 스캐닝 기법을 활용합니다. 별도의 LLM 전용 규칙 없이도 API 키 노출, 입력 검증 누락, 인젝션 공격 등의 위험을 효과적으로 식별할 수 있음을 설명합니다.
핵심 포인트
- LLM 앱은 기존 Python 프로젝트와 유사한 보안 프로필을 가짐
- 하드코딩된 AI 제공자 키(OpenAI, Anthropic 등) 노출이 주요 위험
- 입력 검증 누락 및 SQL/셸 인젝션 위험이 존재함
- 기존 스캐너(Bandit, detect-secrets 등)로도 충분히 탐지 가능
LLM 애플리케이션 코드는 예측 가능한 보안 프로필을 가지고 있습니다. 스캐너 자체는 변하지 않습니다. BrassCoders는 일반적인 Python 프로젝트에 적용하는 것과 동일한 12가지 스캔을 AI 앱에도 적용합니다. 변하는 것은 발견되는 결과의 분포입니다. 초기 개발 단계에서는 하드코딩된 제공자 키(provider keys)가 지배적이며, 사용자 입력과 프롬프트 템플릿(prompt template) 사이에서 입력 검증(input validation)이 사라지고, 도구 호출(tool calls)이 존재하는 곳마다 SQL 및 셸 인젝션(shell injection)이 다시 나타납니다. 이 중 그 어느 것도 LLM 전용 탐지 규칙을 필요로 하지 않습니다. 모두 인식 가능한 구조를 따르는 코드에 적용되는 기존 스캐너 범위 내에 포함됩니다.
LLM 앱 보안 표면 (The LLM App Security Surface)
BrassCoders는 LLM 애플리케이션을 알려진 아키텍처를 가진 Python 코드베이스로 취급합니다. 즉, 사용자 입력이 들어오고, 프롬프트에 엮이며, 프롬프트가 AI 제공자(AI provider)로 전달되고, 응답이 데이터베이스, 셸, API 또는 렌더링된 페이지와 같은 어딘가로 라우팅되는 구조입니다. 이러한 아키텍처는 세 가지 접점에서 위험을 집중시킵니다: 제공자에 접속하는 데 사용되는 자격 증명(credential), 신뢰할 수 없는 입력으로부터 프롬프트를 구성하는 과정, 그리고 모델의 출력을 다운스트림 시스템(downstream systems)으로 다시 라우팅하는 과정입니다.
각 접점은 기존 스캐너의 탐지 범위와 매핑됩니다. Bandit은 셸 인젝션(shell injection)과 SQL 인젝션(SQL injection)을 잡아냅니다. detect-secrets 스캐너는 하드코딩된 자격 증명(hardcoded credentials)을 잡아냅니다. API 보안 스캐너는 문자열 연산 전의 입력 검증(input validation) 누락을 표시합니다. 새로운 규칙은 필요하지 않습니다. 아키텍처가 예측 가능하므로, 발견되는 결과 또한 예측 가능합니다.
API 키: AI 제공자 자격 증명 패턴 (API Keys: The AI Provider Credential Pattern)
BrassCoders는 Yelp의 detect-secrets를 상위 라이브러리로 사용하고 스캐너 레이어에 커스텀 패턴을 추가하여 OpenAI 키, Anthropic 키, AWS 액세스 키, GitHub 개인 액세스 토큰(personal access tokens), Stripe 라이브 키, JWT, PEM 형식의 개인 키(private keys), 그리고 높은 엔트로피를 가진 문자열 등 20개 이상의 비밀 형식(secret formats)을 탐지합니다. 특히 LLM 애플리케이션 코드에서는 AI 제공자 키가 초기 개발 단계에서 인라인(inline)으로 가장 일관되게 작성되는 자격 증명입니다.
그 패턴은 이해하기 쉽습니다. 개발자가 OpenAI 또는 Anthropic 연동을 프로토타이핑하면서, 무언가를 작동시키기 위해 소스 코드에 키를 직접 넣고, .env 리팩토링(refactor)이 이루어지기 전에 키를 커밋해 버리는 것입니다. 스캐너는 sk- 접두사를 발견하고 이를 HIGH 심각도(severity) 탐지 결과로 표시합니다. 그런 다음 YAML 출력의 AI 소비자(consumer) — Claude Code, Cursor, 혹은 귀하의 팀이 사용하는 무엇이든 — 가 해당 패턴이 실제 자격 증명인지 아니면 플레이스홀더(placeholder)인지 확인합니다. 왜냐하면 그러한 판단에는 스캐너가 의도적으로 시도하지 않는 문맥(context)이 필요하기 때문입니다.
Yelp's detect-secrets는 비밀 탐지 계층을 뒷받침하는 오픈 소스 엔트로피(entropy) 및 패턴 라이브러리입니다. 새로운 자격 증명 형식은 라이브러리 업그레이드를 통해 추가됩니다. 재현 가능한 벤치마크에서 이것이 어떻게 전개되는지 더 자세히 살펴보려면, hardcoded credentials post에서 공개된 코퍼스(corpus)의 실제 사례 두 가지를 다룹니다.
프롬프트 템플릿 이전의 입력 검증 (Input Validation)
BrassCoders는 사용자 제어 데이터가 정화(sanitization) 단계 없이 문자열 연산으로 직접 라우팅될 때 입력 검증(input validation) 누락을 표시합니다. 표준 웹 애플리케이션에서는 이것이 SQL 인젝션(SQL injection) 및 XSS 설정을 잡아냅니다. LLM 애플리케이션 코드에서는 동일한 구조적 패턴이 프롬프트 구성 시에 나타납니다.
사용자 입력으로부터 문자열 연결(string concatenation) 또는 f-string 보간(interpolation)을 통해 구축된 프롬프트 템플릿은 연결 방식으로 구축된 SQL 쿼리와 동일한 형태를 가집니다. 즉, 검증되지 않은 입력이 민감한 문맥에 놓이게 됩니다. BrassCoders는 구조적 부재를 찾아냅니다 — 즉, 연결 전의 타입 체크(type check), 길이 제한(length bound), 허용 목록(allow-list)이 없는 상태를 찾아냅니다. 조작된 입력이 모델에 도달했을 때 다운스트림(downstream)에서 어떤 일이 발생하는지는 정적(static)인 문제가 아니라 런타임(runtime) 문제입니다. 스캐너는 누락된 검증을 보고합니다. 귀하의 AI 어시스턴트의 트리아지(triage) 계층이 해당 경로가 악용 가능한지, 그리고 수정 사항이 어떤 모습이어야 하는지를 결정합니다.
이것이 실제 BrassCoders의 업무 분담 방식입니다. 스캐너는 악의적인 프롬프트(prompt)가 모델로 하여금 무엇을 하게 만들 수 있는지에 대해 추론하지 않습니다. 스캐너는 열려 있는 문을 찾아내고, AI 어시스턴트가 그 문이 중요한지를 평가합니다.
모델 반환 후의 출력 산출물 정화 (Output Sanitization)
BrassCoders는 LLM 애플리케이션이 모델의 응답을 처리하는 방식과 관련된 특정 유형의 발견 사항을 포착합니다. 즉, 출력값은 신뢰할 수 없는 데이터(untrusted data)이며, 코드는 이를 그렇게 취급해야 합니다. 모델의 응답이 정화(sanitization) 과정 없이 웹 페이지에 렌더링되거나, 데이터베이스에 기록되거나, 다른 시스템으로 라우팅되는 경우, 사용자 입력에 적용되는 것과 동일한 인젝션(injection) 클래스가 모델이 반환한 값에도 적용됩니다.
이는 Bandit B701 및 관련 발견 사항들로, 코드가 모델의 출력을 신뢰할 수 없는 문자열이 아닌 깨끗한 데이터로 취급하기 때문에 나타납니다. 스캐너는 모델이 올바르게 동작할지 알 수 없습니다. 스캐너는 그러한 가정을 지적(flag)합니다.
도구 호출(Tool Calls)과 SQL/Shell 인젝션 위험
BrassCoders는 사용자 제어 입력이 실행 표면(execution surfaces)에 도달하는 모든 곳에서 SQL 인젝션(SQL injection)과 셸 인젝션(shell injection)을 포착합니다. LLM 애플리케이션 코드에서 이 경로는 종종 도구 호출(tool-call) 계층을 통과합니다. 호출 체인 내에서 사용자 입력을 포함하면서 정화 과정을 거치지 않고 구축된, 데이터베이스를 쿼리하거나 셸 명령을 실행할 수 있는 에이전트는 동일한 구조적 결함을 가진 다른 애플리케이션과 동일한 취약점을 가집니다.
Bandit의 B608 발견 사항(가능한 SQL 인젝션)과 B602/B603 발견 사항(신뢰할 수 없는 입력을 포함한 셸 호출)은 패턴을 기반으로 작동합니다. 스캐너는 도구 호출 프레임워크의 라우팅을 추적하지 않습니다. 대신 쿼리나 셸 명령을 실행하는 코드를 찾아 입력되는 데이터가 정화되었는지 확인합니다. 정화되지 않았다면, 그것이 바로 발견 사항이 됩니다.
이것이 주요 관심 사항이라면 다음의 두 전용 포스트에서 더 자세히 다룹니다: SQL injection in AI-generated Python은 Bandit B608 패턴을 상세히 다루며, command injection with shell=True는 셸 호출 측면을 다룹니다.
스캐너의 범위를 벗어나는 것들
BrassCoders는 소스 계층(source layer)에서 명확한 경계를 설정합니다. LLM 애플리케이션에 특화된 두 가지 위험 요소는 정적 스캐너(static scanner)가 도달할 수 없는 영역에 있으며, 이 경계를 아는 것이 스캐너의 탐지 범위에 대한 신뢰를 구축하는 핵심입니다.
프롬프트 인젝션 (Prompt injection)은 에이전트가 생성하는 소스 코드의 패턴이 아니라, AI 에이전트 자체에 대한 런타임 공격 (runtime attack)입니다. 검색된 문서나 사용자 메시지에 포함된 악의적인 지침은 추론 시간 (inference time)에 에이전트의 동작을 재지정할 수 있습니다. 커밋된 코드에는 스캐너가 매칭할 수 있는 내용이 없습니다. 이에 대한 완화 조치 (mitigations)는 에이전트 권한 부여 (permissioning), 샌드박스 실행 (sandboxed execution), 격리된 컨텍스트 창 (isolated context windows), 그리고 송신 제어 (egress control) 단계에서 이루어집니다. 자세한 내용은 prompt injection post에서 확인할 수 있습니다.
추론 시의 데이터 유출 (Data leakage at inference) — 모델이 컨텍스트 (context)나 검색을 통해 전달받은 민감한 정보를 출력하는 경우 — 역시 정적 분석 (static analysis)으로는 확인할 수 없습니다. 민감한 문자열은 런타임에 시스템을 통해 흐르는 데이터이며, 저장소에 커밋된 리터럴 (literal)이 아니기 때문입니다. BrassCoders는 소스 코드에 작성된 개인정보 (PII) 및 자격 증명 (credentials)을 잡아내지만, 모델이 반환하는 내용을 제어하는 것은 다른 계층에서의 런타임 제어 (runtime control) 영역입니다. 해당 경계가 어디에 위치하는지는 LLM app data leakage post에서 다룹니다.
스캔 실행 방법
BrassCoders는 pip를 통해 설치하며, 오픈 소스 (OSS) 코어 사용 시 별도의 계정이 필요하지 않습니다.
pip install brasscoders
brasscoders scan /path/to/your/llm-app
스캔은 완전히 로컬 (locally)에서 실행됩니다. OSS 코어 모드에서는 단 1바이트의 데이터도 기기를 떠나지 않습니다. 텔레메트리 (telemetry)나 외부 네트워크 호출은 발생하지 않습니다. 탐지 결과는 .brass/ai_instructions.yaml에 저장되며, Claude Code나 Cursor에서 즉시 사용할 수 있는 형식으로 구성됩니다. 사용자의 AI 어시스턴트가 이 YAML 파일을 읽고 각 탐지 항목을 소스 검증 (source-verifies)하면 분류 (triage) 작업이 시작됩니다.
BrassCoders는 Python 3.10 이상을 필요로 하며 macOS, Linux, 그리고 Windows (WSL2)에서 실행됩니다. BrassCoders 유료 (Paid) 라이선스에는 3개의 머신 활성화 (machine activations)가 포함됩니다. OSS 코어는 활성화 요구 사항이 없으며 사용 제한 (usage caps)도 없습니다.
유료 모드에서 어떤 데이터가 머신을 떠나는지에 대한 전체 목록은, data-handling post에서 정확한 페이로드 (payload)를 다룹니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기