Tác giả: Rachid.A
Dịch: Yewlne
01 Bản dịch chính thức
Gần đây, Yasser Allam và tôi, người có bút danh inzo, đã quyết định hợp tác nghiên cứu. Sau khi thảo luận về một số mục tiêu tiềm năng, chúng tôi quyết định tập trung vào Next.js (có 130.000 Sao trên GitHub và hiện có hơn 9,4 triệu lượt tải xuống mỗi tuần). Đó là một khuôn khổ mà tôi rất quen thuộc và tôi đã có một trải nghiệm tuyệt vời với nó, bằng chứng là nghiên cứu trước đây của tôi. Do đó, “chúng tôi” trong bài viết này tự nhiên đề cập đến cả hai chúng tôi.
Next.js là một framework JavaScript toàn diện dựa trên React, với nhiều tính năng phong phú - là nơi lý tưởng để nghiên cứu chi tiết. Với niềm tin, sự tò mò và sự kiên cường, chúng tôi bắt đầu hành trình, khám phá những góc khuất ít người biết đến, tìm kiếm những kho báu ẩn giấu bên trong.
Không lâu sau đó, chúng tôi đã phát hiện ra một vấn đề nghiêm trọng trong phần mềm trung gian. Phạm vi ảnh hưởng rất rộng, tất cả các phiên bản đều bị ảnh hưởng và việc khai thác lỗ hổng này không cần bất kỳ điều kiện tiên quyết nào - chúng tôi sẽ sớm trình bày chi tiết.
Mục lục
Middleware trong Next.js
Công cụ ủy quyền: Mã cũ cấp kho báu
Thứ tự thực thi với middlewareInfo.name
Công cụ cấp phép: Hôm qua đã thành thơ, hôm nay còn đáng giá hơn.
/src thư mục
Độ sâu đệ quy tối đa
Khai thác lỗ hổng
Vượt qua ủy quyền/Ghi đè
Vượt qua CSP
Thực hiện DoS thông qua tấn công cache poisoning (Cái gì?)
làm rõ
Thông báo an toàn - CVE-2025-29927
Tuyên bố từ chối trách nhiệm
Kết luận
Middleware trong Next.js
Middleware cho phép bạn thực thi mã trước khi yêu cầu hoàn tất. Sau đó, bạn có thể thay đổi nội dung phản hồi bằng cách ghi đè, chuyển hướng, sửa đổi yêu cầu hoặc tiêu đề phản hồi, hoặc trả về phản hồi trực tiếp dựa trên yêu cầu đến (trích dẫn từ tài liệu Next.js).
Là một khuôn khổ hoàn chỉnh, Next.js có phần mềm trung gian riêng - một tính năng quan trọng và được sử dụng rộng rãi. Nó có nhiều kịch bản ứng dụng, trong đó quan trọng nhất bao gồm:
Viết lại đường dẫn (Path rewriting)
Chuyển hướng phía máy chủ (Server-side redirects)
Thêm thông tin tiêu đề vào phản hồi (như CSP, v.v.)
Điều quan trọng nhất là: xác thực (Authentication) và ủy quyền (Authorization)
Một ứng dụng phổ biến của middleware là thực hiện ủy quyền, điều này liên quan đến việc bảo vệ các đường dẫn cụ thể dựa trên các điều kiện nhất định.
Xác thực và ủy quyền: Trước khi cấp quyền truy cập vào các trang hoặc API cụ thể, hãy đảm bảo danh tính người dùng và kiểm tra cookie phiên (tài liệu Next.js).
Ví dụ: Khi người dùng cố gắng truy cập vào /dashboard/admin, yêu cầu sẽ được xử lý qua middleware trước tiên, middleware sẽ kiểm tra liệu cookie phiên của người dùng có hợp lệ và có quyền cần thiết hay không. Nếu xác thực thành công, middleware sẽ chuyển tiếp yêu cầu; ngược lại, middleware sẽ chuyển hướng người dùng đến trang đăng nhập:
Công cụ ủy quyền: Mã cũ cấp kho báu
Như một người vĩ đại từng nói, “nói thì rẻ, hãy cho tôi thấy lỗi”, hãy để chúng ta tránh việc miêu tả quá nhiều và đi thẳng vào vấn đề; khi chúng tôi xem phiên bản cũ của khung (v12.0.7), chúng tôi đã phát hiện đoạn mã này:
Khi ứng dụng Next.js sử dụng middleware, hàm runMiddleware sẽ được gọi. Ngoài chức năng chính của nó, hàm này cũng sẽ lấy giá trị của tiêu đề x-middleware-subrequest và sử dụng nó để xác định xem có nên áp dụng middleware hay không. Giá trị tiêu đề này sẽ được tách thành danh sách bằng dấu hai chấm (:) làm dấu phân cách, sau đó kiểm tra xem danh sách này có chứa giá trị middlewareInfo.name hay không. Điều này có nghĩa là, nếu chúng ta thêm tiêu đề x-middleware-subrequest với giá trị chính xác vào yêu cầu, thì middleware - bất kể mục đích của nó là gì - sẽ hoàn toàn bị bỏ qua, yêu cầu sẽ được chuyển tiếp qua NextResponse.next() và sẽ hoàn thành đường đi đến điểm đến ban đầu mà không bị ảnh hưởng bởi bất kỳ middleware nào. Tiêu đề này và giá trị của nó giống như một “chìa khóa vạn năng”, có thể vượt qua tất cả các quy tắc. Lúc này, chúng ta đã nhận ra rằng đã phát hiện ra một vấn đề đáng kinh ngạc, và tiếp theo cần hoàn thành vài mảnh ghép cuối cùng.
Để khóa “vạn năng” của chúng ta có hiệu lực, giá trị của nó phải bao gồm middlewareInfo.name, nhưng giá trị này thực sự là gì?
Thứ tự thực thi với middlewareInfo.name
Giá trị của middlewareInfo.name rất dễ đoán, nó chỉ đơn giản là đường dẫn nơi middleware nằm. Để hiểu điều này, chúng ta cần tìm hiểu một chút về cách cấu hình middleware trong các phiên bản cũ.
Đầu tiên, trước phiên bản 12.2 - trong phiên bản này, các quy ước về middleware đã thay đổi - tệp phải được đặt tên là _middleware.ts. Ngoài ra, bộ định tuyến app chỉ được giới thiệu trong phiên bản 13 của Next.js. Bộ định tuyến duy nhất tồn tại vào thời điểm đó là bộ định tuyến pages, vì vậy tệp này phải được đặt trong thư mục pages (cụ thể cho bộ định tuyến).
Với những thông tin này, chúng ta có thể suy luận ra đường dẫn chính xác của middleware, từ đó đoán giá trị của trường x-middleware-subrequest. Giá trị này chỉ được cấu thành từ tên thư mục (tức là tên router duy nhất tồn tại vào thời điểm đó) và tên tệp, tuân theo quy tắc đặt tên bắt đầu bằng dấu gạch dưới vào thời điểm đó:
x-middleware-subrequest: pages/_middleware
Khi chúng tôi cố gắng bỏ qua những middleware được cấu hình để hệ thống chuyển hướng các cố gắng truy cập từ /dashboard/team/admin đến /dashboard:
Thành công, chúng ta đã xâm nhập ⚔️
Chúng ta bây giờ có thể hoàn toàn bỏ qua phần mềm trung gian, từ đó bỏ qua bất kỳ hệ thống bảo vệ nào dựa trên nó, điển hình nhất là ủy quyền, giống như ví dụ ở trên. Phát hiện này khá đáng kinh ngạc, nhưng còn nhiều điểm khác cần xem xét.
Các phiên bản trước 12.2 cho phép đặt một hoặc nhiều tệp _middleware vào bất kỳ vị trí nào trong cây thư mục (bắt đầu từ thư mục pages) với thứ tự thực thi, như chúng ta thấy trong hình chụp tài liệu cũ được lấy từ Web Archive:
Điều này có ý nghĩa gì đối với việc khai thác lỗ hổng của chúng ta?
Khả năng = Số lượng cấp bậc trong con đường
Vì vậy, để truy cập /dashboard/panel/admin (được bảo vệ bởi middleware), có ba khả năng cho giá trị của middlewareInfo.name và theo đó cho giá trị của x-middleware-subrequest:
pages/_middleware
hoặc
pages/dashboard/_middleware
hoặc
pages/dashboard/panel/_middleware
Công cụ cấp phép: Hôm qua đã thành thơ, hôm nay còn đáng giá hơn.
Cho đến nay, chúng tôi cho rằng chỉ có các phiên bản trước phiên bản 13 dễ bị tấn công, vì phần mềm trung gian đã được di chuyển trong mã nguồn và chúng tôi vẫn chưa bao quát tất cả các khía cạnh của nó. Chúng tôi suy đoán rằng các nhà bảo trì chắc chắn đã chú ý đến lỗ hổng này và đã khắc phục nó trước những thay đổi lớn trong phiên bản 13, vì vậy chúng tôi đã báo cáo lỗ hổng này cho các nhà bảo trì khung và tiếp tục nghiên cứu của mình.
Thật ngạc nhiên, chỉ hai ngày sau khi phát hiện ban đầu, chúng tôi đã phát hiện ra rằng tất cả các phiên bản của Next.js - bắt đầu từ phiên bản 11.1.4 - đều có lỗ hổng! Mã không còn ở cùng một vị trí, và logic khai thác cũng có chút thay đổi.
Như đã đề cập trước đó, bắt đầu từ phiên bản 12.2, tệp không còn bao gồm dấu gạch dưới, mà phải được đặt tên đơn giản là middleware.ts. Ngoài ra, nó không còn nằm trong thư mục pages (điều này rất tiện lợi cho chúng tôi vì bắt đầu từ phiên bản 13, bộ định tuyến ứng dụng đã được giới thiệu, điều này sẽ nhân đôi khả năng).
Với điều đó trong tâm trí, tải trọng cho các phiên bản đầu tiên bắt đầu từ phiên bản 12.2 rất đơn giản:
Xét đến điều này, phiên bản đầu tiên của tải trọng bắt đầu từ phiên bản 12.2 rất đơn giản:
x-middleware-subrequest: middleware
/src thư mục
Cũng cần xem xét rằng Next.js cung cấp khả năng tạo thư mục /src:
(Tài liệu Next.js) Là một phương pháp thay thế cho việc có thư mục ứng dụng hoặc trang Next.js đặc biệt trong thư mục gốc của dự án, Next.js cũng hỗ trợ việc đặt mã ứng dụng trong thư mục src theo mô hình phổ biến. (Tài liệu Next.js)
Trong trường hợp này, trọng tải sẽ là:
x-middleware-subrequest: src/middleware
Do đó, bất kể có bao nhiêu tầng trong con đường, chỉ có hai khả năng tổng cộng. Điều này làm giảm độ khó của việc khai thác lỗ hổng đối với các phiên bản liên quan.
Trong phiên bản mới nhất, nó đã có một chút thay đổi (chúng tôi đảm bảo, lần cuối cùng).
Độ sâu đệ quy tối đa
Trong phiên bản cập nhật, logic đã có chút thay đổi, xin hãy xem đoạn mã này:
v15.1.7
Giống như trước đây, hệ thống sẽ truy xuất giá trị của tiêu đề x-middleware-subrequest và sử dụng dấu hai chấm làm dấu phân cách để tạo thành một danh sách. Nhưng lần này, điều kiện để chuyển tiếp yêu cầu trực tiếp - tức là bỏ qua các quy tắc của middleware - đã khác:
Giá trị của hằng số depth phải lớn hơn hoặc bằng giá trị của hằng số MAX_RECURSION_DEPTH (tức là 5). Trong quá trình gán giá trị, mỗi khi một giá trị trong danh sách subrequests (tức là giá trị tiêu đề được phân cách bằng :) bằng params.name (tức là đường dẫn của middleware), hằng số depth sẽ tăng lên 1. Như đã đề cập trước đó, ở đây chỉ có hai khả năng: middleware hoặc src/middleware.
Vì vậy, để vượt qua middleware, chúng ta chỉ cần thêm các tiêu đề/giá trị sau vào yêu cầu:
x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware
hoặc
x-middleware-subrequest: src/middleware:src/middleware:src/middleware:src/middleware:src/middleware
Đoạn mã này ban đầu được dùng để làm gì?
Đoạn mã này dường như được thiết kế để ngăn chặn các yêu cầu đệ quy rơi vào vòng lặp vô hạn.
Khai thác lỗ hổng
Vì chúng tôi biết bạn thích nội dung này, đây là một số trường hợp thực tế từ Chương trình Bug Bounty.
Vượt qua ủy quyền/Ghi đè
Trong ví dụ này, khi chúng tôi cố gắng truy cập /admin/login, chúng tôi nhận được phản hồi 404. Từ tiêu đề phản hồi có thể thấy rằng middleware đã thực hiện việc viết lại đường dẫn để ngăn chặn người dùng không được phép hoặc không phù hợp truy cập:
Nhưng sử dụng công cụ ủy quyền của chúng tôi:
Chúng tôi có thể truy cập điểm cuối này mà không gặp bất kỳ trở ngại nào, phần mềm trung gian đã bị hoàn toàn bỏ qua. Phiên bản Next.js mục tiêu: 15.1.7
Vượt qua CSP
Lần này, trang web sử dụng middleware để thiết lập - ngoài các chức năng khác - CSP và cookie:
Hãy để chúng ta vượt qua nó:
Target next.js phiên bản: 15.0.3Target next.js version: 15.0.3
Lưu ý: Hãy chú ý đến sự khác biệt trong payload của hai mục tiêu, một cái sử dụng thư mục src/ trong khi cái kia thì không.
Thực hiện DoS thông qua tấn công cache poisoning (Cái gì?)
Có, thông qua lỗ hổng này cũng có thể thực hiện tấn công DoS bằng cách tiêm độc vào bộ nhớ cache. Điều này rõ ràng không phải là điều mà chúng ta muốn tìm kiếm trước tiên, nhưng nếu không có đường dẫn nhạy cảm nào được bảo vệ và không có điểm khai thác nào thú vị hơn, thì một số trường hợp có thể dẫn đến từ chối dịch vụ tiêm độc vào bộ nhớ cache (CPDoS):
Giả sử một trang web viết lại đường dẫn của người dùng dựa trên vị trí địa lý của họ, thêm (/en, /fr, v.v.), và không cung cấp trang hoặc tài nguyên nào trên đường dẫn gốc (/). Nếu chúng ta vượt qua lớp giữa, chúng ta sẽ tránh được việc viết lại và cuối cùng đến trang gốc. Do các nhà phát triển không có ý định cho phép người dùng truy cập trang gốc, nên họ không cung cấp trang tương ứng, chúng ta sẽ nhận được mã lỗi 404 (hoặc có thể là 500 tùy thuộc vào cấu hình/loại viết lại khác nhau).
Nếu trang web sử dụng hệ thống cache/CDN, có thể sẽ buộc phải lưu trữ phản hồi 404, dẫn đến trang không khả dụng, ảnh hưởng nghiêm trọng đến khả năng sử dụng của trang.
làm rõ
Kể từ khi thông báo an toàn được phát hành, chúng tôi đã nhận được một số câu hỏi từ mọi người, họ lo lắng về sự an toàn của ứng dụng của mình và không hiểu rõ về phạm vi của cuộc tấn công. Cần phải làm rõ rằng, các yếu tố dễ bị tấn công là phần mềm trung gian. Nếu bạn không sử dụng phần mềm trung gian (hoặc ít nhất không sử dụng nó cho các mục đích nhạy cảm), thì không cần phải lo lắng (mặc dù, hãy kiểm tra các khía cạnh DoS được đề cập ở trên), vì việc vượt qua phần mềm trung gian sẽ không vượt qua bất kỳ cơ chế an ninh thực tế nào.
Nếu không, hậu quả có thể là thảm khốc, chúng tôi khuyên bạn nên nhanh chóng thực hiện các biện pháp hướng dẫn trong thông báo an toàn.
Thông báo an toàn - CVE-2025-29927
bản vá
Vấn đề này đã được sửa trong phiên bản 15.2.3 của Next.js 15.x
Vấn đề này đã được sửa trong Next.js 14.2.25.
Đối với phiên bản Next.js từ 11.1.4 đến 13.5.6, chúng tôi khuyên bạn nên tham khảo các giải pháp sau.
Giải pháp
Nếu không thể nâng cấp lên phiên bản an toàn, chúng tôi khuyên bạn nên chặn các yêu cầu của người dùng bên ngoài có chứa tiêu đề yêu cầu x-middleware-subrequest truy cập vào ứng dụng Next.js của bạn.
Mức độ nghiêm trọng
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N(Mức độ nghiêm trọng: 9.1/10, mức độ nghiêm trọng cao)
Thông tin thêm
Tại thời điểm viết bài, rõ ràng là các ứng dụng được triển khai trên Vercel và Netlify không còn bị ảnh hưởng bởi lỗ hổng này (cập nhật: Do số lượng dương tính giả cao, Cloudflare đã điều chỉnh quy tắc để chỉ có hiệu lực khi người dùng chủ động bật nó - những dương tính giả này không phân biệt hiệu quả giữa các yêu cầu từ người dùng hợp pháp và yêu cầu từ những kẻ tấn công tiềm năng).
Tuyên bố từ chối trách nhiệm
Nghiên cứu này được phát hành chỉ với mục đích giáo dục, nhằm giúp các nhà phát triển hiểu được nguyên nhân cơ bản của vấn đề, hoặc cung cấp sự gợi ý cho các nhà nghiên cứu / người săn lỗi trong công việc nghiên cứu trong tương lai. Bài viết này như một tài liệu bổ sung cho thông báo an ninh, cung cấp thêm giải thích và làm rõ về bản chất của lỗ hổng - vì trong thông báo đã công khai các tiêu đề yêu cầu dẫn đến lỗ hổng này (cũng như sự khác biệt liên quan đến yêu cầu đó).
Chúng tôi tuyên bố rõ ràng rằng không hỗ trợ bất kỳ việc sử dụng nào không đạo đức đối với nội dung này.
Kết luận
Như bài viết này đã nhấn mạnh, lỗ hổng này đã tồn tại trong mã nguồn của Next.js trong nhiều năm, thay đổi theo sự phát triển của middleware và các phiên bản của nó. Bất kỳ phần mềm nào cũng có thể xuất hiện lỗ hổng nghiêm trọng, nhưng khi nó ảnh hưởng đến một trong những framework phổ biến nhất, nó trở nên đặc biệt nguy hiểm và có thể gây ra hậu quả nghiêm trọng cho hệ sinh thái rộng lớn hơn. Như đã đề cập trước đó, vào thời điểm viết bài này, số lượt tải xuống của Next.js gần 10 triệu mỗi tuần. Nó được ứng dụng rộng rãi trong các lĩnh vực quan trọng từ dịch vụ ngân hàng đến blockchain. Khi lỗ hổng ảnh hưởng đến các chức năng trưởng thành mà người dùng phụ thuộc (như ủy quyền và xác thực), rủi ro càng trở nên lớn hơn.
Nhóm Vercel đã mất vài ngày để giải quyết lỗ hổng này, nhưng điều đáng chú ý là, ngay khi họ nhận ra vấn đề, bản sửa lỗi đã được gửi và hợp nhất vào phiên bản mới trong vòng vài giờ (bao gồm cả việc chuyển sang các phiên bản trước đó).
Thời gian :
Ngày 27 tháng 2 năm 2025: Đã báo cáo lỗ hổng cho các nhà phát triển (khi đó chúng tôi nghĩ rằng chỉ có các phiên bản từ 12.0.0 đến 12.0.7 bị ảnh hưởng và đã ghi chú điều này trong báo cáo).
Ngày 1 tháng 3 năm 2025: Đã gửi thư thứ hai, giải thích rằng thực tế tất cả các phiên bản đều có lỗ hổng, bao gồm cả phiên bản ổn định mới nhất.
Ngày 5 tháng 3 năm 2025: Nhận được phản hồi ban đầu từ đội ngũ Vercel, cho biết phiên bản 12.x không còn được duy trì (có thể họ chưa đọc mẫu thông báo bảo mật mà chúng tôi đính kèm trong email thứ hai, không chú ý đến việc tất cả các phiên bản đều bị ảnh hưởng).
Ngày 5 tháng 3 năm 2025: Gửi email lần nữa, xin đội ngũ nhanh chóng xem xét email thứ hai và mẫu thông báo an toàn.
Ngày 11 tháng 3 năm 2025: Gửi lại email, xác nhận liệu thông tin mới đã được chấp nhận hay chưa.
Ngày 17 tháng 3 năm 2025: Nhận được phản hồi từ đội ngũ Vercel, xác nhận đã tiếp nhận thông tin liên quan.
Ngày 18 tháng 3 năm 2025: Nhận được email từ đội ngũ Vercel thông báo rằng báo cáo đã được chấp nhận và bản vá đã hoàn thành. Chỉ vài giờ sau, phiên bản 15.2.3 đã được phát hành, bao gồm cả bản sửa lỗi.
Ngày 21 tháng 3 năm 2025: Thông báo an toàn chính thức được phát hành.
Nói chung, quá trình tìm kiếm lỗ hổng zero-day chỉ trở nên thú vị và adrenaline tăng vọt vào khoảnh khắc phát hiện ra manh mối; thời gian còn lại giống như một hành trình đầy bất định - đối với những người tò mò, nó mang lại những kiến thức quý giá; đối với những người thiếu kiên nhẫn, hành trình này dường như kéo dài vô tận. Đừng ngần ngại, hành động theo nhóm luôn dễ dàng hơn so với việc vượt qua sa mạc một mình.