2026년 4월 10일 인터넷에 떠돌던 ….
– npm 상태계에서 올해 처음으로 CVSS 만점(10.0)을 받은 취약점 발견
– 프런트 엔드 및 Node.js 상태계는 역사상 가장 심각한 보안 위기 중 하나에 직면
– 애플리케이션이 원격 코드 실행(RCE)과 전체 클라우드 인프라 침해에 노출
– AWS 및 기타 클라우드 환경 침해와 RCE로 이어질 수 있는 치명적 취약점
개요
Axios는 브라우저(클라이언트)와 Node.js(서버) 환경 모두에서 동일한 API로 동작하는 Promise 기반 HTTP 클라이언트이다. 클라이언트에서는 XMLHttpRequest를, 서버에서는 네이티브 http 모듈을 사용하여 비동기 통신을 처리하는 동형(Isomorphic) 라이브러리 형태라고 보면 된다. 이 문서에서 설명할 취약점은 네이티브 http 모듈을 사용하는 서버에서 사용 시에 문제점을 언급하도록 하겠다.
GitHub 보안 어드바이저리(Security Advisory)에 따르면, 공격자가 다른 라이브러리를 통해 Object.prototype을 오염시킬 수 있는 상황이라면, Axios는 설정을 병합하는 과정에서 이러한 오염된 프로퍼티(Property)를 그대로 받아들이게 된다. 문제는 이 과정에서 개행(CRLF)가 포함된 헤더 값이 충분히 검증되지 않은 채 Request Header에 포함될 수 있다는 점이다.
이로 인해 겉으로는 안전해 보이는 고정된 HTTP 요청조차 실제로는 전혀 다른 의미를 갖게 된다. 공격자가 의도한 값이 헤더에 주입되면서, 해당 요청은 HTTP Request Smuggling으로 변질될 수 있고, 나아가 내부 서비스나 클라우드 메타데이터 서비스로 비정상적인 요청을 보내는 데 악용될 가능성도 존재한다.
특히 주목할 점은, 이 공격이 반드시 사용자의 직접적인 입력을 필요로 하지 않는다는 것이다. 어드바이저리(Advisory)에서는 이를 “zero direct user input” 조건에서도 성립 가능한 gadget attack chain으로 설명하고 있다. 즉, 애플리케이션 코드 자체는 안전해 보이더라도, 의존하고 있는 라이브러리 간의 상호작용을 통해 예상치 못한 공격 경로가 만들어질 수 있다는 의미다.
결국 이 취약점은 단일 버그라기보다, 여러 요소가 연결되면서 완성되는 “구조적인 문제”에 가깝다고 볼 수 있다.
기술 분석
전체 공격 과정에 따른 공격 성립 난이도
이 내용을 조금 더 기술적으로 들여다보면, 이 취약점의 본질은 Axios 자체의 단순한 입력 검증 문제라기보다는, JavaScript의 Prototype Pollution과 라이브러리 동작 방식이 결합되면서 발생하는 구조적인 이슈에 가깝다.
일반적으로 객체 병합 로직에서 __proto__와 같은 특수 키에 대한 필터링이 제대로 이루어지지 않을 경우, 전역 객체인 Object.prototype이 오염될 수 있다. 이렇게 한 번 오염이 발생하면, 이후 생성되는 모든 객체는 해당 프로퍼티를 상속 받게 된다. 문제는 이러한 “보이지 않는 상태 변화”가 애플리케이션 전반에 영향을 미친다는 점이다.
여기서 Axios가 개입하게 된다. Axios는 요청을 보내기 전 설정 객체를 병합하는 과정을 거치는데, 이때 프로토타입 체인에 존재하는 값까지 명시적인 구분 없이 함께 처리한다. 그 결과, 개발자가 직접 설정하지 않은 값이 마치 정상적인 요청 설정처럼 포함될 수 있다.
이 흐름은 결국 Node.js의 HTTP 요청 단계까지 이어진다. Node.js에서는 req.setHeader()를 통해 헤더가 실제 요청으로 변환되는데, 이 과정에서 프로토콜 규격에 맞지 않는 값(예: CRLF)은 차단되지만, 형식적으로 정상적인 헤더 값은 그대로 전송된다. 즉, 공격이 완전히 차단되는 것이 아니라 “일부 위험한 형태만 제한되는 구조”라고 볼 수 있다.
이 지점에서 중요한 관점이 하나 나온다. 바로 “Exploitability(공격 성립 난이도)와 Impact(성공시 피해)의 괴리”다.
“이 취약점은 특정 조건이 충족되어야 실제 공격으로 이어질 수 있기 때문에 재현 난이도는 다소 존재하지만, 일단 공격 체인이 완성될 경우 내부 네트워크 접근(SSRF)이나 클라우드 메타데이터 탈취와 같은 심각한 결과로 이어질 수 있다.” 고 쓰고 현실에서 발생할 수 있는 확률은 거의 없다고 보면 된다. CLRF를 넣지 못해 HTTP Request Smuggling 이 어려워진 상황에서 정상 헤더를 변경해서 할 수 있는 것들이 얼마나 있을까? 오버 하지 말자!! CVSS 10.0은 Impact는 크지만, 공격 성립 난이도는 거의(조건부) 없다고 볼 수 있다.
왜 공격 성립 난이도가 낮은지 살펴보도록 하자.
공격 성립 조건 = Prototype-Pollution(프로토타입 오염 공격) + Axios Merge 구조 + SSRF 가능 + 클라우드 metadata 접근.
총 4가지가 맞아 떨어져야 공격이 가능하며, 특히 Node.js의 HTTP 레이어 제약으로 인해, CRLF와 같은 악성 헤더는 Reject 되므로, Request Smuggling 공격은 거의 불가능한 제약 상황일 수 밖에 없게 되는 것이다.
그럼에도 불구하고, 기술을 기술로 대하는 기술자로서 취약점이 어떻게 발현되는지 살펴보도록 하겠다.
심층 기술 분석
1. 프로토타입 오염 공격(Prototype-Pollution)
공격 체인의 첫번째 단계인 프로토타입 오염은 JavaScript의 프로토타입 체인 메커니즘은 가장 독특한 특징 중 하나이면서, 동시에 가장 위험한 요소이기도 하다. 공격자가 Object.prototype을 수정할 수 있게 되면, 모든 객체가 이러한 오염된 속성을 상속 받게 된다.
npm 생태계에서는 이러한 프로토타입 오염 취약점이 매우 흔하게 발견된다. Snyk의 2026년 1분기 보안 보고서에 따르면, 지난 1년 동안 1,200개 이상의 프로토타입 오염 취약점이 발견되었으며, 여기에는 qs, ini, minimist, body-parser, lodash 처럼 주간 다운로드 수가 10억 회를 넘는 핵심 라이브러리들도 포함되어 있다. 즉 프로토타입 오염은 쉽게 발생 될 수 있다.
어떻게 오염되는지, qs 라이브러리의 객체 파싱에 의해 오염되는 과정을 한번 살펴보자
var qs = parseQs("test=1&a[b][c]=2");
console.log(qs); // { test: '1', a: { b: { c: '2' } } }
위 코드를 보면, 내부 내용을 기반으로 각 객체가 구성되고, []에 다라 계층 별로 할당하는 형태라는 걸 볼 수 있다. 위 코드에 아래와 같은 쿼리를 넣으면 어떻게 될까?
var qs = parseQs("proto[a]=3");
console.log(qs); // {}var obj = {};
console.log(obj.a); // 3
parseQs에 의해 값이 수정되어 obj.proto.a 프로토타입 오염이 발생하게 된다. 결과적으로 빈 객체를 선언하고 출력하면 obj.a 객체 프로토타입이 오염되었기 때문에 3이 출력되는걸 볼 수 있다.
이처럼 첫 번째 단계에서 쿼리 파서에 취약점이 존재한다고 가정하고, 다음과 같은 쿼리를 통해 프로토타입 오염을 시킬 수 있다.
Object.prototype['x-amz-target'] = "dummy\r\n\r\nPUT /latest/api/token HTTP/1.1\r\nHost: 169.254.169.254\r\nX-aws-ec2-metadata-token-ttl-seconds: 21600\r\n\r\nGET /ignore";
위 코드는 HTTP Request Smuggling 발생시키기 위해, 개행문자(CRLF)를 통해 HTTP 패킷을 분리하는 페이로드라고 보면 된다.
이제 프로토타입 오염을 시켰으니, 다음 단계로 이동해보자.
2. Axios config merge 및 headers 생성
await axios.get('https://analytics.internal/pings');
위 코드처럼 Request인 axios.get 이 요청될 때, 먼저 mergeConfig.js의 mergeConfig 함수가 호출된다. 이 함수는 두 개의 설정 객체(기본 설정 + 사용자 설정)를 병합하는 함수이다. 이 과정에서 이미 prototype은 오염된 상태이기 때문에, 오염된 값이 설정에 병합되어지게 된다.
Object.prototype['x-amz-target'] = "dummy\r\n\r\nPUT /latest/api/token HTTP/1.1\r\nHost: 169.254.169.254\r\nX-aws-ec2-metadata-token-ttl-seconds: 21600\r\n\r\nGET /ignore";
(예상) 이 오염된 값으로 인해, 실제 요청 패킷은 아래와 같이 구성되어 질 것이다.
GET /pings HTTP/1.1
Host: analytics.internal
x-amz-target: dummy
PUT /latest/api/token HTTP/1.1
Host: 169.254.169.254
X-aws-ec2-metadata-token-ttl-seconds: 21600
GET /ignore HTTP/1.1
...
이후 과정은 mergeConfig 함수를 거쳐, dispatchRequest.js 통해 headers 구조 평탄화 및 정리를 하고 adapter(lib/adapters/http.js)에서 setHeader 함수를 거치게 되는데….. 두둥…
이 과정에서 잘못된 값이 들어오면 뱉어버린다. 그래서 개행을 통한 HTTP Request Smuggling은 불가해지며, 다른 임의의 헤더만 추가할 수 밖에 없어진다. 즉 최종 공격인 AWS 자격 증명 도용은 IMDSv2 우회 매커니즘을 사용할 수 없어지므로, 사용 불가 해진다고 보면 된다.
그러면 임의의 추가 헤더를 붙이는 방법 하나 남는다. 임의로 추가 헤더를 붙여서 과연 어떤 공격을 할 수 있을까? 간혹 가다 좋은 시나리오가 있을 수도 있지만, 실제 공격에 활용되기에는 미미할 것으로 보인다.
req.setHeader에 개행을 넣었을때 아래와 같이 ERR_INVALID_CHAR 예외(Exception)가 발생하는 걸 확인 할 수 있다.
const http = require(‘http’);
const options = {
hostname: 'httpbin.org',
path: '/headers',
method: 'GET',
};
const req = http.request(options, (res) => {
res.on('data', chunk => console.log(chunk.toString()));
});
req.setHeader('x-amz-target', 'TEST\r\nTEST');
req.end();
node:internal/errors:542
throw error;
^
TypeError [ERR_INVALID_CHAR]: Invalid character in header content ["x-amz-target"]
at ClientRequest.setHeader (node:_http_outgoing:645:3)
at Object. (C:\Users\silverbug\Desktop\AXTEST\test2.js:13:5)
at Module._compile (node:internal/modules/cjs/loader:1812:14)
at Object..js (node:internal/modules/cjs/loader:1943:10)
at Module.load (node:internal/modules/cjs/loader:1533:32)
at Module._load (node:internal/modules/cjs/loader:1335:12)
at wrapModuleLoad (node:internal/modules/cjs/loader:255:19)
at Module.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:154:5)
at node:internal/main/run_main_module:33:47 {
code: 'ERR_INVALID_CHAR'
}
실제 공격 PoC 테스트 과정을 살펴보자
![[CVE-2026-40175] Axios Prototype Pollution 기반 공격 체인](https://hacksum.net/wp-content/uploads/2024/07/REDvsPEN-90x60.png)