CVE-2026-40175: Axios Prototype Pollution. IMDSv2 Bypass + RCE (Phân tích chi tiết)
CVE-2026-40175 (CVSS 10.0 Critical): Axios <1.15.0 bị prototype pollution gadget chain + CRLF header injection, cho phép HTTP request smuggling, bypass AWS IMDSv2, RCE và đánh cắp IAM credentials chỉ từ một dòng axios.get() hardcoded. Phân tích kỹ thuật, PoC, và cách patch.
CVE-2026-40175 · GHSA-fvcv-3m26-pcqx · CVSS 10.0 Critical
Affected package:axios(npm) - toàn bộ0.xvà1.x < 1.15.0
Patched in:[email protected]
Vulnerability class: CWE-113 (CRLF Injection) + CWE-444 (HTTP Request Smuggling) + CWE-918 (SSRF)
Attack chain: prototype pollution ở dependency khác → CRLF injection vào header value → HTTP request smuggling → bypass AWS IMDSv2 → đánh cắp IAM credentials của EC2 instance role.
TL;DR: một dòngaxios.get('https://...')hardcoded, không có user input, đủ để compromise toàn bộ AWS account nếu trong dependency tree có bất kỳ lib nào còn lỗ hổng prototype pollution. Bump axios lên1.15.0ngay.
Vụ Axios bị compromise bởi hacker hôm trước còn chưa khô thì hôm nay Axios lại ướt. Tối qua mình đang định tắt máy đi ngủ thì thấy GHSA-fvcv-3m26-pcqx pop lên trên Dependabot của một service nội bộ. Critical, CVSS 10.0, axios. Phản xạ đầu tiên của mình là thở dài - lại nữa à? - và mở advisory ra, đọc title:
"Axios has Unrestricted Cloud Metadata Exfiltration via Header Injection Chain"
Đọc lại lần hai. Cloud Metadata Exfiltration. Mình ngồi thẳng dậy.
Cho bạn nào chưa quen với cụm này: ngắn gọn, "Cloud Metadata Exfiltration" nghĩa là attacker moi được nội dung của cloud metadata service - một endpoint đặc biệt mà mọi VM trên AWS, GCP, hay Azure đều có thể gọi tới để hỏi "tôi là ai, tôi đang chạy với quyền gì". Trên AWS, endpoint đó là http://169.254.169.254, một địa chỉ link-local chỉ instance mới reach được. Khi bạn curl vào đó, nó trả về đủ thứ: instance ID, region, user-data script, và quan trọng nhất - temporary IAM credentials của role mà instance đang assume. Những credentials đó cho phép bạn nói chuyện với S3, RDS, Secrets Manager với toàn bộ quyền của instance. "Exfiltration" ở đây nghĩa là tuồn được những credentials đó ra ngoài. Một khi attacker có chúng, họ không còn cần exploit gì nữa - họ là bạn, ở mức AWS API.
Sau khi đọc hết PoC trong advisory, mình không bump version đi ngủ nữa. Mình ngồi dò lại tất cả service đang chạy trên EC2 có dùng axios - và viết bài này, một phần vì mình cần hiểu kỹ cái gì vừa xảy ra, một phần vì nếu bạn cũng đang chạy Node.js trên AWS, mình nghĩ bạn nên ngồi xuống đọc cùng.
Thứ làm mình đứng hình
Cái khiến CVE này không phải "bug axios bình thường" nằm ở một câu trong advisory: "Zero Direct User Input".
Thông thường khi nói tới SSRF hay CRLF injection, bạn hình dung một endpoint nhận URL hoặc header từ user, rồi dev quên sanitize. Bạn audit code, bạn grep req.body, bạn tìm chỗ data chảy từ ngoài vào hàm HTTP. Đó là pattern bạn đã thấy hàng trăm lần.
CVE-2026-40175 không cần bất kỳ thứ gì như thế. Đoạn code "bị khai thác" có thể là một dòng hardcoded trông sạch sẽ đến mức không lập trình viên tỉnh táo nào nghi ngờ:
await axios.get('https://analytics.internal/pings');

Không user input. Không header dynamic. Không URL từ ngoài. Vậy mà nó có thể là cái đòn bẩy để attacker steal IAM credentials của cả AWS account. Để hiểu vì sao, phải nối ba thứ tưởng như không liên quan: prototype pollution ở một lib khác, cách axios merge config, và một particular của AWS IMDSv2.
Mảnh thứ nhất: prototype pollution như một "ambient state"

Prototype pollution là loại bug mà mình từng coi là "khó exploit ngoài đời". Bạn cần một sink phù hợp để turn nó thành RCE, và đa số bài talk về nó nghe hơi academic.
Cho bạn nào chưa quen với class lỗi này: prototype pollution là một loại bug đặc thù của JavaScript, đến từ một quirk của ngôn ngữ. Trong JS, mọi object literal {} mà bạn tạo ra đều âm thầm "thừa kế" từ một object gốc tên là Object.prototype. Khi bạn viết obj.toString() mà chính obj không có method toString, JS sẽ đi ngược lên Object.prototype để tìm - đó là cách prototype chain hoạt động. Vấn đề là Object.prototype không phải read-only. Nếu attacker tìm được bất kỳ chỗ nào trong code mà bạn merge dữ liệu từ ngoài vào một object mà không lọc các key đặc biệt như __proto__ hoặc constructor.prototype (Object.assign, lodash.merge, một query parser cũ, một config loader…), họ có thể "ghi" thẳng vào Object.prototype. Và một khi Object.prototype.foo = 'bar' đã chạy, mọi object trong toàn bộ Node.js process từ đó trở đi đều "có" property foo khi bạn enumerate hoặc truy cập - kể cả những object được tạo ra ở module hoàn toàn không liên quan, viết bởi người không hề biết bạn tồn tại. Đó là lý do nó được gọi là "pollution": nó không hỏng một object, nó làm bẩn cái nguồn nước chung mà mọi object đều uống.
Nhưng prototype pollution có một đặc tính mà mình mới thật sự cảm được sau khi đọc CVE này: nó không phải là một vulnerability ở một function cụ thể - nó là một thay đổi global state ảnh hưởng đến mọi object trong process. Một khi Object.prototype['x-amz-target'] được set, mọi object literal {} trong toàn bộ Node.js process từ đó về sau đều sẽ "có" property x-amz-target khi bạn enumerate hoặc merge.
Và đây là chỗ axios bước vào.
Nếu bạn từng mở source code của axios ra đọc, bạn biết là mỗi request đi qua một quá trình mergeConfig để gộp default config, instance config, và per-request config thành một object cuối cùng. Trong quá trình merge đó, nó duyệt qua các property của headers. Nếu prototype đã bị nhiễm, axios sẽ nhặt cái property bẩn đó vào headers của request - kể cả khi developer không bao giờ đụng vào nó. Đây không phải bug của axios theo nghĩa "code sai logic". Đây là cái giá phải trả khi bạn tin tưởng vào Object.prototype trong một ngôn ngữ mà prototype có thể bị chỉnh sửa từ xa.
Nhiều thư viện đã từng có history pollution: qs cũ, minimist, ini, body-parser, lodash.merge. Nếu app của bạn có bất kỳ dependency nào trong số đó với một version có CVE pollution chưa patch, bạn đã có "mảnh thứ nhất" sẵn trong process - và bạn thậm chí không biết.
Mảnh thứ hai: CRLF, request smuggling, và một socket không hỏi nhiều

Mảnh thứ hai là phần làm mình thấy axios "đáng trách" hơn pollution lib gốc. Khi axios viết headers ra socket trong lib/adapters/http.js, nó truyền value xuống Node's http.request mà không kiểm tra xem value có chứa \r\n hay không.
CRLF injection trong HTTP header là một class lỗi cũ tới mức bạn nghĩ năm 2026 không thư viện lớn nào còn dính. Trick nằm ở chỗ: nếu bạn nhét \r\n\r\n vào giữa giá trị một header, bạn không chỉ "tạo header mới" - bạn kết thúc luôn cái HTTP request hiện tại và mở một request thứ hai ngay trên cùng connection. Đó là request smuggling ở dạng chân phương nhất.
Payload trong PoC của advisory tận dụng đúng điều đó:
Object.prototype['x-amz-target'] =
"dummy\r\n\r\nPUT /latest/api/token HTTP/1.1\r\n" +
"Host: 169.254.169.254\r\n" +
"X-aws-ec2-metadata-token-ttl-seconds: 21600\r\n\r\n" +
"GET /ignore";
Trước khi bóc payload, một chú thích nhanh: x-amz-target là một HTTP header chuẩn của AWS, thường được các SDK gửi kèm khi gọi API của các service kiểu DynamoDB, Lambda, KMS - nó cho service biết bạn đang muốn invoke operation nào (ví dụ DynamoDB_20120810.GetItem). Lý do attacker chọn cái tên này không phải vì nó đặc biệt mạnh, mà vì nó trông hợp lý: nếu một dev tình cờ log headers ra để debug, thấy x-amz-target thì sẽ nghĩ "à, lib AWS nào đó của mình set thôi" và bỏ qua. Nó là cái áo nguỵ trang. Bạn có thể thay bằng bất kỳ header nào khác, chain vẫn chạy.
Trông rối, nhưng nếu mình bóc từng dòng ra, bạn sẽ thấy nó là một HTTP request hoàn chỉnh đang giả dạng làm value của một header. Dòng đầu tiên "dummy" chỉ là một chuỗi vô hại - đây sẽ là "value thật" của header x-amz-target mà axios nghĩ nó đang gửi đi. Ngay sau dummy là cặp ký tự đặc biệt \r\n\r\n - trong giao thức HTTP, hai dòng trống liên tiếp có ý nghĩa duy nhất: "đây là chỗ kết thúc phần header và thân request hiện tại". Bất kỳ HTTP parser nào đọc đến đây đều sẽ nói "OK, request này xong rồi", và nếu còn bytes phía sau thì coi đó là request mới trên cùng connection.
Phần "request mới" đó được attacker viết ra rất cẩn thận, đúng cú pháp HTTP. PUT /latest/api/token HTTP/1.1 là dòng request line - chính xác là cái mà IMDSv2 yêu cầu để cấp token (một SSRF cổ điển không bao giờ ép app gửi PUT được, đó là điểm mấu chốt). Host: 169.254.169.254 chỉ định máy chủ đích là IMDS; bất kỳ reverse proxy nào ở giữa parse pipelined HTTP sẽ nhìn vào header này để route. X-aws-ec2-metadata-token-ttl-seconds: 21600 là header bắt buộc của IMDSv2 - nó nói "cấp cho tôi token sống trong 6 giờ", và cũng là thứ mà SSRF cổ điển không bao giờ inject được vì axios không cho gọi API đặt header tuỳ ý từ URL. Sau đó lại một cặp \r\n\r\n để đóng request số 2.
Cuối cùng, "GET /ignore" ở dòng chót là một "trailer rác" - nó tồn tại đơn giản để hấp thụ bất kỳ bytes nào mà axios có thể append vào sau header value (ví dụ thêm \r\n của riêng nó), tránh tình trạng cú pháp request số 2 bị hỏng vì có ký tự thừa.
Bạn có thể đọc payload này theo cách khác cho dễ hình dung - đây là cùng một string, format ra thành bytes thật trên dây:
← (header trước đó của request 1)
x-amz-target: dummy ← header value "thật" mà dev nghĩ mình set
← \r\n\r\n: kết thúc request 1
PUT /latest/api/token HTTP/1.1 ← request 2 bắt đầu - method PUT
Host: 169.254.169.254 ← target = IMDS
X-aws-ec2-metadata-token-ttl-seconds: 21600 ← header IMDSv2 yêu cầu
← \r\n\r\n: kết thúc request 2
GET /ignore ← request 3 (rác, để hứng bytes thừa)
Đây là toàn bộ trick. Một string duy nhất, set một lần qua prototype pollution, biến mọi axios.get() trong process thành một cỗ máy gửi PUT đến IMDS. Khi axios merge cái property này vào headers rồi gửi GET /pings HTTP/1.1 đến analytics.internal, traffic thực tế đi ra socket sẽ trông như có hai request riêng biệt: request đầu tiên đi đến analytics.internal như developer mong đợi, request thứ hai là một PUT /latest/api/token hoàn toàn do attacker viết.
Đến đây, một câu hỏi rất hợp lý: cái request thứ hai đó đi đâu? Nó vẫn nằm trên cùng TCP connection đến analytics.internal cơ mà. Server analytics.internal sẽ nhận được nó, không phải IMDS.
Câu trả lời nằm ở mảnh thứ ba - và đây là lúc câu chuyện chuyển từ "smuggling thông thường" sang "tại sao IMDSv2 cũng cứu không nổi".
Mảnh thứ ba: tại sao IMDSv2 lẽ ra là an toàn - và tại sao lần này thì không

Mình muốn dừng ở đây một chút để công bằng với AWS, vì IMDSv2 là một trong những security control thiết kế tốt hiếm hoi mà mình thật sự khâm phục.
IMDSv1 cho phép bạn đơn giản GET /latest/meta-data/iam/security-credentials/ và lấy IAM credentials. SSRF cơ bản - attacker dụ server side gửi một request GET đến 169.254.169.254 là xong. Hàng loạt vụ Capital One 2019 (~106 triệu records) là từ một SSRF biến thành lệnh GET đến IMDSv1.
IMDSv2 đóng đường đó bằng một yêu cầu nhỏ nhưng then chốt: trước khi lấy được credential, client phải PUT /latest/api/token kèm header X-aws-ec2-metadata-token-ttl-seconds. Server IMDS sẽ trả lại một session token, bạn phải gửi token đó kèm mỗi request về sau. Thiết kế này dựa trên một observation tinh tế: tuyệt đại đa số SSRF chỉ làm được GET request với headers do server chọn. Một SSRF cổ điển không thể nào ép server gửi đúng một PUT với đúng header X-aws-ec2-metadata-token-ttl-seconds. Nói cách khác, IMDSv2 không vá SSRF - nó đặt một capability requirement mà SSRF thường không đạt được.
Đó là lý do từ 2019 đến giờ, dev có IMDSv2 bật lên đều ngủ ngon hơn một chút.
CVE-2026-40175 phá đúng giả định nền tảng đó. Khi attacker có thể smuggle nguyên một HTTP request bất kỳ qua axios, họ không bị giới hạn ở GET nữa. Họ viết được PUT. Họ chọn được header. Họ làm chính xác cái mà IMDSv2 giả định là "không thể từ một SSRF".
Nhưng vẫn còn câu hỏi của mình lúc nãy: request smuggle đó vẫn đang ở trên socket nối tới analytics.internal, chứ không phải IMDS. Làm sao nó tới được 169.254.169.254?
Câu trả lời thẳng thắn là: trong PoC của advisory, nó không cần tới được IMDS theo kiểu network. Cái smuggled request là một payload đầy đủ, hợp lệ, có Host header - và nếu attacker control được cả URL gốc của axios call (ở các biến thể khác của exploit chain), hoặc nếu app có một internal proxy/loadbalancer parsing pipelined HTTP requests theo cách khác với upstream, request thứ hai có thể được route lại. Trong các môi trường Kubernetes có service mesh hoặc envoy sidecar, "inconsistent interpretation of HTTP requests" - đúng nghĩa CWE-444 mà advisory liệt kê - là cách smuggled request thoát ra khỏi connection ban đầu để rơi vào một upstream khác. Đây là điểm mà chain phụ thuộc vào hạ tầng, nhưng giả định "có ít nhất một proxy ở giữa parse HTTP khác với origin server" gần như luôn đúng trong production.
Khi smuggled PUT đến được IMDS, nó hoàn toàn hợp lệ. IMDS trả token. Attacker dùng token đó để query /latest/meta-data/iam/security-credentials/, và bạn vừa mất role của EC2 instance - thường là role có quyền đụng tới S3, RDS, và trong nhiều trường hợp là Secrets Manager.
Một dòng axios.get('https://analytics.internal/pings'). Đó là tất cả những gì cần để chuỗi này khởi động.
Để mình vẽ lại từ đầu, cho rõ
Mình đã giải thích từng mảnh, nhưng nếu đến đây bạn vẫn chưa "thấy" được attack vector chạy như thế nào, lỗi là ở mình. Cách dễ hiểu nhất là vẽ ra so với SSRF cổ điển - thứ mà bạn đã quen.
SSRF cổ điển trên IMDSv1

Đây là pattern bạn đã thấy hàng trăm lần: attacker kiểm soát URL mà app gọi, app gọi IMDS, credentials chảy ra ngoài. AWS đã đóng đường này bằng IMDSv2.
IMDSv2 đóng đường đó như thế nào
IMDSv2 thêm một bước trước khi cho bạn đọc credentials: bạn phải PUT /latest/api/token kèm header X-aws-ec2-metadata-token-ttl-seconds, lấy token, rồi mới được GET. Một SSRF cổ điển chỉ làm được điều này:

App gọi axios.get(attackerUrl) - nó là GET, không phải PUT, và headers do app chọn chứ không phải attacker. Hai capability mà attacker thiếu: method và header value. Đó là lý do IMDSv2 hiệu quả với 99% SSRF.
CVE-2026-40175 trả lại cho attacker cả hai capability đó
Đây là phần mới. Theo dõi từng bước:

Bytes thật sự đi ra dây trong Step 3 trông như sau - chú ý cặp \r\n\r\n ngay sau x-amz-target: dummy đã kết thúc request 1, và request 2 bắt đầu ngay từ dòng PUT:
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
...
So sánh side-by-side
| Khía cạnh | SSRF cổ điển | CVE-2026-40175 |
|---|---|---|
| Cần user input ở callsite? | Có (URL/header từ user) | Không - code trông sạch sẽ |
| Method kiểm soát được? | GET only | Bất kỳ - PUT, DELETE, gì cũng được |
| Header value kiểm soát được? | Không (app chọn) | Có - qua prototype pollution |
| IMDSv2 chặn được? | Có | Không - đủ capability để hoàn thành PUT/token handshake |
| Cần lib nào khác bị pollution? | Không | Có (đây là precondition) |
| Class lỗi | CWE-918 (SSRF) | CWE-113 + CWE-444 + CWE-918 (chain) |
Điểm cốt lõi cần nhớ
SSRF cổ điển hỏi câu "app có chịu gọi IMDS hộ tôi không?" - và IMDSv2 trả lời: "có gọi cũng vô ích, vì anh không gửi được PUT với đúng header."
CVE này hỏi câu khác: "tôi có thể nhét nguyên một HTTP request thứ hai vào trong một header value của request thứ nhất không?" - và câu trả lời, suốt một thập kỷ axios tồn tại, là "có".
Sự khác biệt nằm ở chỗ: SSRF cổ điển bị giới hạn ở những gì axios sẵn sàng làm cho bạn. Request smuggling thì không - một khi bạn ghi được bytes tuỳ ý ra socket, bạn không còn là client của axios nữa. Bạn là người viết HTTP trực tiếp, và axios chỉ là một cái bưu điện đui mù chuyển hộ.
Cái fix dài đúng năm dòng

Patch trong v1.15.0 ngắn đến mức nếu bạn không biết câu chuyện đằng sau, bạn sẽ tưởng ai đó đang fix typo:
utils.forEach(requestHeaders, function setRequestHeader(val, key) {
if (/[\r\n]/.test(val)) {
throw new Error('Security: Header value contains invalid characters');
}
// ... proceed to set header
});
Năm dòng. Đó là khoảng cách giữa một thư viện 50 triệu downloads/tuần "có thể là gadget cho cloud account compromise" và "không thể nữa". Mình nhìn cái diff này khá lâu. Một phần vì nhẹ nhõm. Một phần vì mình đang nghĩ về việc có bao nhiêu thư viện khác trong stack của mình cũng đang viết header values trực tiếp ra socket mà chưa bao giờ ai ngồi nghĩ về CRLF.
Counter-argument mình đã tự nói với mình
Khi đọc xong PoC, phản xạ thứ hai của mình là: "Nhưng để pollution xảy ra, attacker đã phải có entry point ở một lib khác rồi. Nếu họ đã có thể pollute prototype, họ đã có exploit ở đâu đó. Cái này có thực sự là 'lỗi của axios' không?"
Mình đã thật sự ngồi với câu hỏi này một lúc - vì nó hợp lý. Theo logic threat modeling cổ điển, một gadget không phải vulnerability; gadget chỉ trở thành vấn đề khi đã có một primitive khác. Axios không tạo ra pollution. Axios chỉ tận dụng prototype như mọi merge utility khác.
Nhưng càng nghĩ, mình càng thấy lập luận đó là một dạng wishful thinking đặc trưng của ngành. Mình nghĩ đáp án đúng nằm ở chỗ: nếu thư viện của bạn đang là cây cầu duy nhất giữa "một lỗi tương đối nhỏ và phổ biến (prototype pollution)" với "RCE / cloud compromise", bạn không thể trốn sau câu "nhưng họ đã phải có pollution trước đó". Vai trò của bạn trong chain là vai trò của bạn trong chain. Browser engineers không nói "well, vuln này cần một XSS trước đó nên không phải lỗi của tôi" - họ defense-in-depth.
Năm dòng kiểm tra \r\n trên header value là một giá rẻ đến mức không có lý do gì để không trả. Việc nó chưa được trả trong suốt một thập kỷ axios tồn tại không phải "không phải lỗi axios" - đó là một tâm lý mà cả ngành Node.js đã quá quen: tin tưởng input từ object literal vì "nó là code của mình", quên rằng Object.prototype không phải code của ai cả mà là một bãi chung.
Cái mình thực sự sợ
Pollution + axios chỉ là một instance cụ thể. Cái khiến mình mất ngủ một lúc tối qua không phải bản thân CVE này - mà là số lượng những "gadget chain" tương tự chưa được ai viết PoC.
Bao nhiêu thư viện HTTP client trong ecosystem Node.js đang viết header values trực tiếp xuống socket? Bao nhiêu thư viện gRPC? Bao nhiêu wrapper Kafka, Redis, RabbitMQ đang merge config qua Object.assign mà chưa từng nghĩ rằng Object.prototype có thể chứa key có \r\n? Bao nhiêu lib có một method request(opts) mà opts được merge từ default + user, và cái merge đó honor prototype properties?
Mình không có câu trả lời. Mình thậm chí không có shortlist. Cái mình có là một suspicion khá khó chịu rằng câu trả lời là nhiều, và rằng CVE tiếp theo cùng pattern này sẽ không đợi lâu.
Bạn nên làm gì ngay bây giờ
Nếu bạn còn ở đây và đang chạy Node trên AWS:
Bump axios lên >= 1.15.0. Đây là việc 30 giây và nó cắt đứt chain. Đừng đợi sprint tới.
Sau đó, kiểm tra IMDS hop limit của các EC2 instance. AWS cho phép set --http-put-response-hop-limit 1 trên metadata options của instance. Với hop limit = 1, packet đến IMDS chỉ có thể đến từ chính instance đó, không thể qua container network bridge. Trong nhiều set-up Docker mặc định, hop limit 2 là cái khiến container vẫn có thể reach IMDS - và đó là cánh cửa thứ hai bạn nên đóng.
Cuối cùng, nếu bạn đang dùng IAM roles for service accounts trên EKS, hoặc IRSA tương đương, hãy disable IMDS hoàn toàn ở instance level. Cái security boundary tốt nhất là cái không tồn tại để attack.
Một suy nghĩ để ngỏ
Mình kết bài này với một thứ chưa giải quyết được trong đầu mình.
Cả ba mảnh của chain này - prototype pollution, CRLF không sanitize, IMDSv2 dựa vào "SSRF thường không gửi được PUT" - từng cái một đều là quyết định thiết kế hợp lý ở thời điểm chúng được đưa ra. JavaScript prototype mở vì nó cho phép monkey-patching, một feature từng được coi là sức mạnh. Axios không sanitize CRLF vì "header value là string mà developer kiểm soát". IMDSv2 dựa vào capability requirements vì giả định đó đúng với 99% SSRF.
Mỗi quyết định cô lập đều defendable. Nhưng khi ghép lại - qua một con đường mà không ai trong ba team kia (V8, axios, AWS) có thể nhìn thấy từ vị trí của họ - chúng tạo ra một khoảng cách mười dòng code giữa "production safe" và "cloud account gone".
Mình không nghĩ bài học là "phải predict mọi composition". Không ai làm được điều đó. Bài học mình tự rút ra là một thứ khiêm tốn hơn: bạn không thể tin rằng vì security control của bạn chưa thất bại, nó sẽ không thất bại. Threat model của bạn được build trên một tập hợp các giả định, và tất cả những gì cần là một CVE ở một thư viện bạn không control để biến giả định đó thành kỷ niệm.
Mình tắt máy đi pha cà phê. Nếu bạn đọc đến đây và đang nghĩ về dependency tree của mình theo một cách hơi khác, mình thấy đó là điều đúng.

Tôi viết bài này ở góc ngồi làm việc thân quen, cafe ngon, quán rộng, tầm nhìn khá thoáng.
- Bình
FAQ - Câu hỏi thường gặp về CVE-2026-40175
CVE-2026-40175 là gì?
CVE-2026-40175 (còn được biết với mã GHSA-fvcv-3m26-pcqx) là một lỗ hổng critical CVSS 10.0 trong axios - HTTP client phổ biến nhất của Node.js (>50 triệu downloads/tuần). Lỗ hổng cho phép một chuỗi tấn công prototype pollution → CRLF header injection → HTTP request smuggling → bypass AWS IMDSv2 → đánh cắp IAM credentials, biến một dòng axios.get() hardcoded vô hại thành đòn bẩy compromise toàn bộ AWS account.
Axios version nào bị ảnh hưởng?
Toàn bộ axios 0.x và 1.x trước phiên bản 1.15.0. Đã được vá trong [email protected]. Nếu bạn đang dùng bất kỳ version nào dưới 1.15.0, bạn vulnerable.
Làm sao để patch CVE-2026-40175?
Chạy npm install axios@^1.15.0 (hoặc yarn add axios@^1.15.0 / pnpm add axios@^1.15.0). Đây là việc 30 giây và không có breaking change. Sau đó kiểm tra IMDS hop limit của EC2 instance (--http-put-response-hop-limit 1) và disable IMDS hoàn toàn nếu đang dùng IRSA trên EKS. Đừng đợi sprint sau.
Tại sao IMDSv2 không cứu được trong CVE này?
IMDSv2 chặn SSRF cổ điển bằng một capability requirement: client phải PUT /latest/api/token kèm header X-aws-ec2-metadata-token-ttl-seconds. Một SSRF cổ điển không thể ép app gửi PUT với đúng header này. CVE-2026-40175 phá đúng giả định đó: prototype pollution + CRLF cho phép attacker nhồi nguyên một HTTP request thứ hai (PUT, đầy đủ header) vào trong header value của request gốc - trở thành request smuggling. Một khi attacker viết được bytes tuỳ ý ra socket, họ không còn bị giới hạn bởi method GET hay header value mà axios chọn nữa.
Prototype pollution là gì? Tại sao nó nguy hiểm với axios?
Prototype pollution là class lỗi đặc thù của JavaScript: vì mọi object literal {} đều thừa kế từ Object.prototype, nếu attacker ghi được vào prototype global đó (qua một merge function không filter __proto__, ví dụ ở qs, lodash.merge, minimist, body-parser cũ), thì mọi object trong process từ đó đều "có" các property bẩn. Khi axios mergeConfig() duyệt headers, nó vô tình nhặt cả property thừa kế từ prototype - dù developer không bao giờ set nó.
CVE-2026-40175 có exploit ngoài đời thật không?
PoC đầy đủ đã có trong advisory chính thức. Mức exploitation thực tế phụ thuộc vào việc trong dependency tree có lib nào còn lỗ hổng prototype pollution chưa vá, và infrastructure có proxy/service mesh parse pipelined HTTP theo cách khác với origin server (CWE-444). EPSS hiện tại ~0.24% (percentile 47), nhưng với CVSS 10.0 và proof-of-concept public, mọi production EC2 đang chạy axios <1.15.0 nên được coi là phải patch khẩn cấp.
Tôi không dùng AWS, có cần lo không?
Có. AWS IMDSv2 chỉ là một application dễ thấy của attack chain. Bản chất là HTTP request smuggling - vẫn dùng được cho cache poisoning (Host header injection), authentication bypass (Cookie/Authorization injection), pivot vào internal admin panels qua header smuggling. GCP và Azure cũng có metadata services (metadata.google.internal, 169.254.169.254) và đều có thể bị target tương tự nếu metadata service đặt yêu cầu cụ thể trên header/method.
Tài liệu tham khảo:
- GHSA-fvcv-3m26-pcqx - GitHub Security Advisory
- axios commit 3631854 - patch CRLF validation
- axios v1.15.0 release notes
- NVD CVE-2026-40175
- AWS IMDSv2 - Adding defense in depth against open firewalls, reverse proxies, and SSRF
- CWE-113: HTTP Response Splitting
- CWE-444: Inconsistent Interpretation of HTTP Requests (Smuggling)