Cursor가 Deep Merge 함수에서 Prototype Pollution을 생성하는 이유
요약
Cursor와 같은 AI 코딩 도구가 보안 검증이 누락된 재귀적 객체 병합 함수를 생성하여 Prototype Pollution 취약점을 유발하는 사례를 분석합니다. AI 모델이 학습한 데이터의 패턴이 기능적 정확성에만 치중되어 보안 가드(guard)를 누락하는 문제를 지적합니다.
핵심 포인트
- AI가 생성한 deep merge 함수는 키 필터링 누락으로 Prototype Pollution 위험이 있음
- 학습 데이터가 보안 검증보다 기능적 동작 위주로 구성되어 발생하는 문제
- 단위 테스트에서는 발견하기 어려운 사용자 입력 기반의 취약점 특성
- 해결책으로 __proto__, constructor, prototype 키에 대한 가드 추가 필요
요약 (TL;DR)
- AI 에디터는
__proto__및constructor.prototype을 포함하여 소스 객체의 모든 키를 신뢰하는 재귀적 객체 병합(recursive object merges) 코드를 작성합니다. - 정교하게 조작된 단 하나의 페이로드(payload)가
Object.prototype을 조용히 덮어씌워, 서버가 재시작될 때까지 Node 프로세스 내의 모든 객체를 감염시킵니다. - 해결책은 루프 상단에 세 가지 키에 대한 가드(guard)를 추가하는 것입니다: 재귀를 수행하기 전에
__proto__,constructor,prototype을 건너뛰어야 합니다.
나는 Cursor에게 deep merge 함수를 작성해 달라고 요청했습니다. 기본 설정 위에 환경별 설정을 레이어링(layering)해야 했기 때문입니다. 이는 내가 수십 번은 만들어 본 패턴입니다. Cursor가 생성한 함수는 15줄이었고, 중첩된 객체(nested objects)를 올바르게 처리했으며, 내가 작성한 모든 테스트를 통과했습니다.
사흘 뒤, 나는 사용자 JSON을 해당 함수로 전달하는 모든 엔드포인트에 {"__proto__": {"isAdmin": true}}를 POST 할 수 있다는 사실을 발견했습니다. 그러면 서버는 이후의 모든 객체가 isAdmin이 true로 설정된 것으로 취급하게 됩니다. 단 한 번의 요청으로 Node 프로세스 전체가 침해되었습니다. 이 감염은 서버가 재시작될 때까지 지속되었습니다.
Cursor가 작성한 함수는 prototype pollution (CWE-1321)의 가장 전형적인 형태였습니다.
취약한 코드
키 필터링 없이 객체 키를 반복하는 재귀적 병합 함수는 Object.prototype으로 향하는 직접적인 쓰기 채널이 됩니다. 소스 객체에 있는 __proto__라는 이름의 키는 그 시점 이후 프로세스에서 생성되는 모든 객체의 프로토타입 체인(prototype chain)을 덮어씌웁니다.
// CWE-1321 - AI가 생성한 deep merge, 키 검증 없음
function deepMerge(target, source) {
for (const key of Object.keys(source)) {
...
이 함수는 합리적으로 보입니다. JavaScript의 deep merge에 관한 수백 개의 튜토리얼에 등장하는 정확한 패턴입니다. AI 에디터들은 해당 튜토리얼들로부터 이를 학습했으며, 그 튜토리얼들에는 키 가드(key guard)가 포함되어 있지 않은 경우가 거의 없습니다.
왜 이런 일이 계속 발생하는가
AI 에디터들이 Prototype Pollution (프로토타입 오염)이 가능한 merge (병합) 함수를 생성하는 이유는, 재귀적 merge (재귀적 병합) 패턴이 온라인에서 가장 많이 복사되는 JavaScript (자바스크립트) 코드 조각 중 하나이며, 해당 코드 조각의 압도적 다수가 안전 검사 (safety check)를 누락하고 있기 때문입니다. 모델은 이 문제가 안전하게 해결된 사례보다, 기능적으로 올바르고 결과적으로 정확한 병합 값을 반환하도록 해결된 사례를 훨씬 더 많이 학습했습니다.
이 취약점이 살아남는 데에는 더 미묘한 이유도 있습니다. 위험한 동작은 사용자 제어 입력 (user-controlled input)이 함수로 흘러 들어갈 때만 나타납니다. 하드코딩된 fixture (픽스처) 객체를 대상으로 작성된 단위 테스트 (unit test)에서는 결코 이 문제가 발생하지 않습니다. 함수는 테스트를 통과하고, 병합되며, 배포됩니다. 다음 세대 모델을 위한 학습 데이터는 이를 올바른 구현으로 기록합니다.
이 패턴에 대한 실제 CVE (Common Vulnerabilities and Exposures) 사례들이 존재합니다. lodash는 prototype-pollutable merge (프로토타입 오염 가능 병합) 문제로 CVE-2019-10744 (CVSS 9.8)를 받았습니다. jQuery도 유사한 문제(CVE-2019-11358)가 있었습니다. 여러 Express middleware (미들웨어) 패키지들도 이 문제로 인해 패치되었습니다. 이것은 이론적인 예외 사례가 아닙니다.
해결 방법 (The Fix)
재귀가 내려가기 전에 prototype chain (프로토타입 체인)에 도달할 수 있는 모든 키를 차단하십시오. 세 가지 키가 전체 공격 표면 (attack surface)을 커버합니다: __proto__, constructor, 그리고 prototype입니다. 루프 상단에 continue 문 하나를 추가하는 것이 해결책의 전부입니다.
// 안전한 deep merge (깊은 병합) - prototype-chain 키 차단
function deepMerge(target, source) {
for (const key of Object.keys(source)) {
...
신뢰할 수 없는 입력을 처리하는 dict (딕셔너리) 병합에 대한 Python (파이썬) 대응 방식은 다음과 같습니다:
# Python 안전한 재귀적 병합
BLOCKED_KEYS = {'__proto__', '__class__', '__subclasshook__'}
...
보완적인 접근 방식 중 하나는 merge (병합) 대상(target)을 Object.create(null)로 초기화하는 것입니다. 이는 __proto__ 접근을 통해 오염될 수 없는, 프로토타입이 없는 객체입니다. 이를 대체재가 아닌 방어 심층 (defense-in-depth) 조치로 사용하십시오. constructor.prototype 공격은 Object.create(null)를 우회할 수 있으므로, 키 검사는 여전히 필요합니다.
자주 묻는 질문 (FAQ)
자주 묻는 질문 (FAQ)
질문: JavaScript에서 프로토타입 오염(prototype pollution)이 실제로 악용 가능한 보안 버그로 이어지나요?
답변: 예. lodash CVE-2019-10744 (CVSS 9.8)가 가장 명확한 예시입니다. 성공적인 공격은 프로세스 내의 모든 객체에서 isAdmin, role, 또는 authenticated와 같은 속성을 덮어씁니다. 신뢰할 수 없는 객체에서 속성을 읽어와 그에 따라 동작하는 모든 것은 권한 상승(privilege escalation)이나 인증 우회(auth bypass)로 이어지는 경로가 됩니다.
질문: TypeScript나 linter를 사용해서 타입 레벨에서 이를 잡아낼 수 있나요?
답변: 아니요. TypeScript는 Object.keys가 string[]를 반환하는 것으로 타입을 지정하며, 이는 정확합니다. 하지만 타입 레벨에서는 `
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기