Hệ thống sau khi hoàn tất cài đặt Next.js SSR trên VPS Ubuntu kèm Redis đang vận hành ổn định thì nhận được thông báo khẩn cấp từ Vercel về lỗ hổng thực thi mã từ xa CVE-2026-94545 (GHSA-vcvr-r3jv-pc5j, điểm CVSS 9.5) công bố ngày 22/09/2026 trên Next.js từ phiên bản 16.2.0 đến 16.3.5.
Lý thuyết bảo mật luôn khuyên nâng cấp lên bản vá next@16.3.6 ngay lập tức, nhưng thực tế vận hành trên môi trường production (đặc biệt với các cụm máy chủ chạy Node.js 24 LTS Krypton) lại phức tạp hơn nhiều: dự án đang trong khung giờ freeze code, quy trình bảo mật CI/CD Pipeline trên VPS bằng cách kiểm soát outbound traffic cần hàng giờ chạy regression test, hoặc việc nhảy từ nhánh 16.2.x thẳng lên 16.3.6 gây xung đột với các package hiện hữu.
Trong khoảng trống thời gian đầy rủi ro đó, thiết lập Nginx Reverse Proxy chặn RCE Next.js đóng vai trò như một lớp vá lỗi tạm thời ở tầng mạng (Virtual Patching), giúp lọc bỏ các request chứa payload khai thác trước khi chúng chạm tới tiến trình Node.js.
Vậy làm sao để xác định nhanh những route nào trên VPS đang phơi nhiễm rủi ro chỉ với vài dòng lệnh terminal? Cấu hình bộ lọc trên Nginx 1.30+ / 1.31+ như thế nào để vừa cản được các biến thể mã độc SVG, vừa tránh được bẫy tràn bộ đệm regex và không chặn nhầm các tiêu đề bài viết tiếng Việt có dấu?
Giải phẫu lỗ hổng CVE-2026-94545 (GHSA-vcvr-r3jv-pc5j) trên Next.js và Satori SVG
Để viết được bộ rule chặn chuẩn xác trên Reverse Proxy mà không làm gãy tính năng hợp lệ của ứng dụng, trước tiên chúng ta cần nắm rõ đường đi của dữ liệu từ lúc request bước vào server cho đến khi kích hoạt lỗi thực thi mã từ xa.
Cơ chế hình thành RCE từ next/og (ImageResponse) sang chuỗi thư viện phía sau
Tính năng ImageResponse thuộc gói next/og (hoặc gói @vercel/og) được các developer sử dụng phổ biến trong các Route Handler (như /api/og) hoặc các file quy ước opengraph-image.tsx, twitter-image.tsx nhằm tạo ảnh preview động khi chia sẻ liên kết lên mạng xã hội. Luồng xử lý dẫn tới lỗ hổng diễn ra qua 4 giai đoạn:
- Tiếp nhận dữ liệu đầu vào (Input Ingestion): Ứng dụng nhận dữ liệu do người dùng hoặc kẻ tấn công kiểm soát (ví dụ: tham số trên URL query
searchParams, route segment, HTTP headers hoặc dữ liệu từ form) rồi truyền trực tiếp vào cấu trúc JSX bên trongImageResponse. - Biên dịch JSX sang SVG qua Satori:
ImageResponsechuyển cấu trúc giao diện này cho Satori (thư viện do Vercel phát triển được đóng gói sẵn bên trong Next.js) để biên dịch layout JSX/CSS thành định dạng vector SVG. - Lỗi không escape ký tự trong Satori (
GHSA-wx4j-mvgx-mqwp): Ở các phiên bản Satori trước0.33.5, thư viện này không thực hiện escape đầy đủ một số ký tự đặc biệt trước khi đưa giá trị đầu vào vào nội dung, thuộc tính (attributes) hoặc kiểu dáng (CSS styles) của thẻ SVG (chẳng hạn như nội dung nằm trong thẻ<title>). Hệ quả là chuỗi văn bản đầu vào không còn được hiểu như dữ liệu thuần túy (plain text) mà bị trình phân tích cú pháp biên dịch thành các thẻ/cấu trúc SVG chủ động (SVG Injection). - Leo thang thành RCE ở chuỗi thư viện phía sau (Downstream Processing Pipeline): Khi đứng độc lập, lỗi của Satori (
GHSA-wx4j-mvgx-mqwp) chỉ nhận mức điểm CVSS v4.0 là 5.3 (Moderate) vì bản thân Satori chỉ xuất ra chuỗi văn bản SVG chứ không tự thực thi mã lệnh. Tuy nhiên, khi đặt vào ngữ cảnhImageResponsechạy trên Node.js runtime, chuỗi SVG vừa bị chèn mã độc này lập tức được chuyển tiếp sang các thư viện xử lý hình ảnh phía sau để rasterize (vẽ) thành tệp ảnh PNG. Tại đây, các cấu trúc SVG độc hại tương tác với các điểm tiếp nhận mã thực thi (executable sinks) trong chuỗi dependency của Node.js, làm bùng nổ rủi ro lên mức CVSS 9.5 (Critical) và cho phép kẻ tấn công thực thi mã lệnh tùy ý ngay trong tiến trình máy chủ mà không cần xác thực.
Phạm vi ảnh hưởng và cảnh báo kỹ thuật về Edge Runtime:
- Môi trường chịu tác động: Các ứng dụng chạy Next.js từ 16.2.0 đến 16.3.5 sử dụng
ImageResponse(next/og) trên Node.js runtime (môi trường mặc định của App Router) và có đưa dữ liệu bên ngoài vào SVG. - Môi trường không chịu tác động RCE này: Nhánh Next.js 15.x không chứa chuỗi phụ thuộc bị lỗi này (bản cập nhật
15.5.26phát hành cùng ngày chỉ bổ sung các tinh chỉnh gia cố phòng ngừa chonext/og). - Cảnh báo việc chuyển sang Edge Runtime: Về mặt kỹ thuật, các route cấu hình
export const runtime = 'edge'không bị ảnh hưởng bởi RCE này do chạy trong V8 isolates tách biệt. Tuy nhiên, bạn không nên đổi code sang Edge runtime để xử lý tạm thời. Lý do là Vercel đã đánh dấu Edge runtime choImageResponsevào diện ngừng khuyến khích (deprecated); việc đẩy ứng dụng sang một runtime đã lỗi thời để né CVE sẽ đẩy dự án vào ngõ cụt kỹ thuật và phát sinh lỗi tương thích thư viện.
Tại sao npm audit và lệnh tìm kiếm satori trong file lock có thể bỏ sót lỗ hổng?
Ngay cả khi bạn đã triển khai giải pháp cấu hình Outbound Proxy trên VPS Linux để ngăn chặn mã độc npm và PyPI, nhiều SysAdmin và Fullstack Engineer vẫn rơi vào trạng thái chủ quan khi chạy npm audit (trên môi trường Node.js 24 LTS đi kèm npm 11) hoặc dùng lệnh grep satori package-lock.json trên VPS mà không thấy bất kỳ cảnh báo nào. Có hai nguyên nhân kỹ thuật giải thích cho hiện tượng này:
- Satori được đóng gói chìm (Bundled) bên trong Next.js: Vercel không khai báo
satorinhư một gói phụ thuộc độc lập trongpackage.jsoncủa người dùng mà đóng gói trực tiếp mã của Satori vào bên trong góinextđể phục vụ cho modulenext/og. Do đó, khi bạn tìm từ khóasatoritrongpackage-lock.json,yarn.lockhaypnpm-lock.yaml, kết quả trả về thường trống rỗng. - Độ trễ đồng bộ dữ liệu của công cụ kiểm tra tự động: Các công cụ SCA như
npm auditphụ thuộc vào thời điểm cơ sở dữ liệu cảnh báo cập nhật đầy đủ cây phụ thuộc. Bên cạnh đó, những dự án đang chạy nhánh16.2.xkhông có bản vá backport riêng cho nhánh 16.2 mà buộc phải nâng cấp lên16.3.6.
Cách rà soát nhanh trong 5 phút xem VPS của bạn có đang phơi nhiễm
Thay vì phụ thuộc vào trình kiểm tra tự động, bạn hãy SSH vào VPS và chạy ngay các lệnh kiểm tra thủ công sau tại thư mục gốc của dự án:
Bước 1: Kiểm tra chính xác phiên bản Next.js thực tế đang chạy
Kiểm tra phiên bản từ file lock (npm / yarn / pnpm):
jq '.packages["node_modules/next"].version' package-lock.json 2>/dev/null || grep -A 2 '"node_modules/next":' package-lock.json
grep -A 1 "next@" yarn.lock 2>/dev/null
Kiểm tra trực tiếp phiên bản trong tiến trình Node.js hoặc Docker Container đang chạy:
node -e "console.log(require('next/package.json').version)"
docker exec -it <ten_container> node -e "console.log(require('next/package.json').version)"
Bước 2: Khoanh vùng toàn bộ các file có gọi next/og, @vercel/og hoặc quy ước ảnh OG
Tìm các file import trực tiếp next/og, @vercel/og hoặc ImageResponse:
grep -rn "next/og\|@vercel/og\|ImageResponse" app/ src/ pages/ 2>/dev/null
Tìm các file tự động sinh ảnh theo quy ước của App Router:
find app/ src/ -type f \( -name "opengraph-image.*" -o -name "twitter-image.*" \) 2>/dev/null
Bước 3: Kiểm chứng thực tế (Black-box Test) bằng curl và cmp
Nếu bạn quản lý một hệ thống lớn với nhiều microservice và muốn xác minh nhanh xem tham số trên URL (như ?title=...) có thực sự chảy vào pipeline render ảnh hay không mà chưa cần đọc từng dòng code, hãy dùng lệnh curl tải hai bức ảnh với hai tham số khác nhau rồi đối chiếu từng byte bằng công cụ cmp.
1. Bước đối chứng (Control test) – Tải 2 lần cùng 1 tham số để kiểm tra ảnh sinh ra có ổn định không:
curl -s -H "Cache-Control: no-cache" "https://your-domain.com/api/og?title=Alpha" -o img_alpha1.png
curl -s -H "Cache-Control: no-cache" "https://your-domain.com/api/og?title=Alpha" -o img_alpha2.png
2. Bước thử nghiệm – Tải ảnh với tham số khác biệt:
curl -s -H "Cache-Control: no-cache" "https://your-domain.com/api/og?title=Beta" -o img_beta.png
3. So sánh byte-by-byte bằng cmp:
cmp -s img_alpha1.png img_alpha2.png && echo "Render ổn định" || echo "Ảnh có chứa metadata/timestamp động"
cmp -s img_alpha1.png img_beta.png && echo "Tham số KHÔNG ảnh hưởng ảnh render" || echo "CẢNH BÁO: Tham số URL đi thẳng vào luồng render ảnh!"
4. Xóa file tạm sau khi kiểm tra:
rm -f img_alpha1.png img_alpha2.png img_beta.png
Nếu bước đối chứng báo ổn định nhưng khi so sánh img_alpha1.png và img_beta.png lại khác nhau, chắc chắn dữ liệu từ query string đang đi trực tiếp vào Satori/ImageResponse.
Nguyên lý dùng Nginx Reverse Proxy chặn RCE Next.js và các giới hạn cần nắm rõ
Khi đã xác định hệ thống có route nằm trong vùng ảnh hưởng nhưng chưa thể lập tức deploy bản next@16.3.6, việc can thiệp ngay tại tầng Nginx Reverse Proxy là phương án phòng vệ khả thi để bảo vệ máy chủ.
Sức mạnh của Virtual Patching tại tầng Reverse Proxy
Virtual Patching (vá lỗi ảo ở tầng mạng) là kỹ thuật thiết lập các quy tắc kiểm soát và lọc gói tin HTTP ngay tại lớp cổng vào (Nginx Reverse Proxy / WAF) nằm phía trước ứng dụng.
- Triển khai tức thời không gây downtime: Bạn không cần sửa source code, không cần chờ build lại Docker image hay khởi động lại cụm Node.js cluster. Mọi thay đổi trên Nginx chỉ mất vài mili-giây thông qua lệnh
nginx -s reload. - Chặn sớm từ tầng mạng và bảo vệ tài nguyên VPS: Khi các botnet tự động dò tìm lỗ hổng CVE-2026-94545 bằng cách gửi hàng loạt request chứa thẻ
<svg>,<foreignObject>vào/api/og, Nginx có thể đóng kết nối TCP ngay lập tức thông qua mã nội bộ444(return 444;) mà không cần gửi lại bất kỳ byte header phản hồi nào. Tiến trình Node.js phía sau hoàn toàn không phải tiếp nhận hay tốn CPU/RAM để xử lý các request độc hại này.
Góc khuất của WAF: Bài toán “Stored Payload” và rủi ro hở Port gốc
Dù đem lại khả năng phản ứng nhanh, một kỹ sư hạ tầng thực thụ cần nhìn nhận rõ hai giới hạn kiến trúc của việc dùng Nginx Reverse Proxy chặn RCE Next.js:
1. Điểm hạn chế trước kịch bản Stored Payload (Dữ liệu độc hại lưu sẵn trong Database)
Nginx hoạt động ở ranh giới mạng (network edge), nghĩa là nó chỉ nhìn thấy những gì nằm trên đường truyền HTTP của lượt request hiện tại (URL Path, Query String, Headers, Request Body). Nginx hoàn toàn thiếu thông tin ngữ cảnh trước những dữ liệu được ứng dụng truy xuất nội bộ từ Database hoặc Redis Cache.
Hãy hình dung kịch bản sau:
- Giai đoạn nạp (Ingestion): Kẻ tấn công đăng ký tài khoản hoặc tạo một sản phẩm trên hệ thống CMS/Marketplace của bạn và đặt tên hiển thị chứa cú pháp SVG độc hại. Dữ liệu này được lưu vào Database.
- Giai đoạn kích hoạt (Trigger): Một người dùng hoặc bot mạng xã hội gọi vào đường dẫn
GET /blog/bai-viet-123/opengraph-imagehoặcGET /api/og?userId=102. - Hệ quả: Nhìn từ phía Nginx, URL
/api/og?userId=102hoàn toàn hợp lệ và an toàn, không hề có ký tự<,>hay từ khóascript,foreignObjectnào. Bộ lọc Regex trên Nginx sẽ cho phép request đi qua. Khi vào tới backend, tiến trình Node.js mới lấy chuỗi độc hại tương ứng vớiuserId=102từ Database ra và đưa vàoImageResponse, kích hoạt RCE ngay từ bên trong.
Chính vì kịch bản Stored Payload này, ở phần hướng dẫn cấu hình bên dưới, chúng ta sẽ chia thành các lớp phòng thủ riêng biệt (bao gồm cả phương án ngắt hẳn route render động sang ảnh tĩnh nếu hệ thống của bạn có nhận dữ liệu từ Database đa người dùng).
2. Lỗ hổng hạ tầng khi để hở cổng gốc của Node.js (Port 3000)
Mọi quy tắc lọc công phu trên Nginx sẽ trở nên vô nghĩa nếu tiến trình Node.js hoặc Docker container của bạn vẫn đang lắng nghe công khai trên 0.0.0.0:3000 và không được tường lửa che chắn. Khi đó, kẻ tấn công chỉ cần gọi thẳng vào http://<IP_VPS>:3000/api/og?... để vòng qua Nginx.
Trước khi viết bất kỳ rule WAF nào, bạn nên áp dụng quy trình bảo mật VPS Linux với UFW, Fail2Ban và Hardening SSH kết hợp chạy lệnh kiểm tra địa chỉ lắng nghe của cổng 3000 trên VPS để khóa chặt đường đi vòng này:
ss -tulpn | grep 3000
Nếu kết quả hiển thị 0.0.0.0:3000 hoặc *:3000, bạn cần xử lý ngay:
- Với ứng dụng chạy trực tiếp hoặc qua PM2/Systemd: Đổi cấu hình bind địa chỉ về loopback nội bộ bằng biến môi trường
HOSTNAME=127.0.0.1hoặc khởi chạy vớinpx next start -H 127.0.0.1 -p 3000. - Với tường lửa UFW trên Ubuntu/Debian: Cấu hình chỉ cho phép kết nối nội bộ qua cổng 3000 và chặn truy cập từ bên ngoài bằng các lệnh sau:
sudo ufw allow in on lo to any port 3000 sudo ufw deny 3000/tcp sudo ufw reload - Lưu ý đặc biệt nếu dùng Docker: Daemon của Docker tự động ghi đè vào bảng
FORWARDcủaiptables, có thể làm vô hiệu hóa lệnh chặn của UFW nếu bạn khai báoports: - "3000:3000". Hãy sửa lại filedocker-compose.ymlđể chỉ ánh xạ cổng vào địa chỉ loopback của máy chủ:ports: - "127.0.0.1:3000:3000"
Hướng dẫn cấu hình Nginx Reverse Proxy chặn RCE Next.js theo 3 lớp phòng thủ
Trước khi đi vào cấu hình chi tiết, có một yêu cầu kỹ thuật quan trọng về phiên bản Nginx trên VPS: Tính đến tháng 09/2026, nhánh Nginx Stable hiện hành là 1.30.5 và nhánh Mainline là 1.31.6 (cùng phát hành ngày 15/09/2026).
Bạn bắt buộc phải kiểm tra (nginx -v) và đảm bảo Nginx đã ở bản 1.30.5+ hoặc 1.31.6+ vì hai lý do an ninh quan trọng:
- Tránh lỗi tràn bộ đệm khi dùng
mapkết hợp Regex (CVE-2026-42533): Vào ngày 15/07/2026 (tại bản1.30.4và1.31.3), Nginx đã phải vá lỗ hổng tràn bộ đệm CVE-2026-42533 xảy ra ngay trong quá trình xử lý chỉ thịmapcó sử dụng biểu thức chính quy (regex). Nếu bạn áp dụng bộ lọcmapchứa đầy regex bên dưới trên một bản Nginx cũ chưa vá lỗi này, chính lớp phòng thủ của bạn lại mở ra nguy cơ bị tấn công tràn bộ đệm. - Vá lỗi tràn bộ đệm HTTP/3 QUIC (
CVE-2026-90439): Nếu bạn đã thực hiện tối ưu VPS cho website lớn thông qua việc triển khai HTTP/3 và QUIC (listen 443 quic;), bản cập nhật1.30.5/1.31.6ngày 15/09/2026 cũng đồng thời đóng luôn lỗ hổng tràn bộ đệm nghiêm trọng CVE-2026-90439 trên modulengx_http_v3_module.
Tùy thuộc vào kiến trúc ứng dụng, hãy chọn áp dụng Lớp 1 hoặc kết hợp Lớp 2 và Lớp 3 dưới đây.
Lớp 1 – chuyển hướng trả về ảnh tĩnh (Phương án an toàn cho cả Stored Payload)
Nếu ứng dụng của bạn lấy dữ liệu do người dùng khác nhập từ Database để vẽ ảnh OG (khiến bộ lọc URL không thể phát hiện Stored Payload), phương án an toàn theo đúng khuyến nghị của Satori Advisory (ngừng truyền hoàn toàn dữ liệu không tin cậy vào ImageResponse khi chưa vá) là tạm ngắt luồng render động trên Nginx và trả về một bức ảnh đại diện mặc định của thương hiệu (Static Fallback Image).
# Đặt bên trong khối server {} của file cấu hình domain
location ~* ^/(api/og|.*/opengraph-image|.*/twitter-image)$ {
# Bắt buộc dùng mã 302 (chuyển hướng tạm thời), KHÔNG dùng 301
add_header Cache-Control "no-cache, must-revalidate" always;
return 302 /static/default-og-banner.png;
}
# Phục vụ trực tiếp file ảnh tĩnh từ thư mục public mà không gọi vào Node.js
location = /static/default-og-banner.png {
root /var/www/nextjs-app/public;
expires 1h;
}
Kinh nghiệm thực tế: Bạn hãy sử dụng mã phản hồi 302 Found kết hợp header Cache-Control: no-cache thay vì 301 Moved Permanently. Nếu dùng 301, các trình thu thập dữ liệu của Facebook, Zalo hay X sẽ lưu cứng đường dẫn chuyển hướng này vào bộ nhớ đệm dài hạn, khiến các liên kết chia sẻ không thể hiển thị lại ảnh động ngay cả khi bạn đã vá xong code và gỡ rule Nginx.
Lớp 2 – thiết lập bộ lọc map chặn mẫu SVG Injection khi cần giữ tính năng tạo ảnh động
Trong trường hợp dự án của bạn chỉ nhận tham số trực tiếp từ URL/Query String và cần duy trì tính năng sinh ảnh OG động, hãy dùng module ngx_http_map_module để kiểm tra biến $request_uri (bao quát cả đường dẫn lẫn query string nguyên bản).
# BẮT BUỘC đặt khối map bên trong block http {} (ngoài block server {})
map_hash_bucket_size 128;
map_hash_max_size 4096;
map $request_uri $og_svg_attack {
default 0;
# 1. Chặn các thẻ XML/SVG/HTML nguy hiểm ở cả dạng thô (<) và URL-encoded (%3C)
"~*(<|%3c)\s*(svg|script|foreignobject|image|use|iframe|embed|object|animate|set|style|title|desc|defs|symbol)\b" 1;
"~*(%3c%2f|</)\s*(svg|script|style|title|desc)" 1;
# 2. Chặn các ký tự đóng/mở thẻ và phá vỡ ngữ cảnh thuộc tính/style (", ', &)
"~*[<>\"']" 1;
"~*(%3c|%3e|%22|%27)" 1;
# 3. Chặn các Event Handler tự động kích hoạt (onload=, onerror=...)
"~*on(load|error|click|mouseover|activate|begin|end)\s*(=|%3d)" 1;
# 4. Chặn các giao thức và thuộc tính tham chiếu ngoại lai (xlink:href, XInclude, ENTITY, CSS url)
"~*xlink(:|%3a)href" 1;
"~*(javascript|data|vbscript)\s*(:|%3a)" 1;
"~*(xinclude|xi:include|<!entity|system\s+)" 1;
}
Mẹo hiện đại cho người dùng Nginx Mainline 1.31.5+ / 1.31.6: Kể từ bản
1.31.5(phát hành ngày 02/09/2026), Nginx đã bổ sung tính năng Predicate Locations (định tuyếnlocationdựa trên điều kiện/vị từ). Nếu đang chạy nhánh Mainline 1.31.6, bạn có thể áp dụng trực tiếp Predicate Locations để tách luồng xử lý biến$og_svg_attackngay từ cấp định tuyến mà không cần lồng nhiều khốiiftruyền thống bên tronglocation.
Lưu ý quan trọng khi xử lý tiêu đề tiếng Việt có dấu trên Nginx:
Nhiều tài liệu bảo mật nước ngoài thường khuyên dùng phương pháp Allow-list trên Nginx theo mẫu regex ~*^[\w\s\.,!\?\-]{0,120}$ để chỉ cho phép chữ cái và chữ số đi vào tham số title. Tuy nhiên, trên các website tiếng Việt, cách làm này sẽ gây lỗi chặn nhầm (false-positive) hàng loạt:
- Thư viện PCRE trên Nginx mặc định hiểu ký tự
\wchỉ gồm các ký tự ASCII[a-zA-Z0-9_]. - Khi người dùng chia sẻ bài viết có tiêu đề tiếng Việt (ví dụ:
?title=Hướng dẫn cấu hình VPS), trình duyệt sẽ mã hóa UTF-8 multi-byte thành các chuỗi phần trăm như%C6%B0,%E1%BB%9B. - Khi đối chiếu với
^[\w\s]+$, Nginx sẽ coi các ký tự tiếng Việt này là không hợp lệ và ngắt kết nối ngay lập tức.
Do đó, ở tầng Nginx Reverse Proxy, việc sử dụng Deny-list (chặn đích danh các ký tự cấu trúc thẻ <, >, ", ' và các từ khóa SVG độc hại như khối map ở trên) giúp toàn bộ chuỗi văn bản tiếng Việt UTF-8 đi qua bình thường mà vẫn khóa chặt các ký tự dùng để phá vỡ ngữ cảnh thẻ SVG.
Lớp 3 – giới hạn HTTP Method, kích thước Request và Rate Limiting thông minh
Để ngăn chặn kẻ tấn công gửi payload qua phương thức POST hoặc dùng công cụ tự động fuzzing làm cạn kiệt tài nguyên VPS, chúng ta cần thiết lập thêm lớp kiểm soát tần suất (ngx_http_limit_req_module) và giới hạn phương thức HTTP.
Khi cấu hình Rate Limit cho route OG Image, bạn cần xử lý hai bài toán thực tế sau:
- Bẫy gom chung IP khi đứng sau Cloudflare / CDN: Nếu VPS của bạn đặt sau Cloudflare mà không cấu hình
real_ip_header, biến$binary_remote_addrsẽ lấy IP của máy chủ Cloudflare thay vì IP thực của người dùng. Chỉ cần vài chục người dùng truy cập cùng lúc, Nginx sẽ khóa nhầm toàn bộ lưu lượng đi qua nút CDN đó. - Bẫy chặn nhầm Social Crawlers (Facebook, Zalo, X, Telegram): Khi một bài viết đang viral, máy chủ của Facebook (
facebookexternalhit) hoặc Zalo (ZaloBot) sẽ gửi liên tiếp nhiều request để lấy ảnh preview. Để không khóa nhầm các bot này, chúng ta dùng kỹ thuật gán khóa rỗng ("") trongmapcho các Social Crawler (trong Nginx, khi key truyền vàolimit_req_zonelà một chuỗi rỗng, bộ đếm giới hạn sẽ tự động bỏ qua request đó).
File cấu hình Nginx hoàn chỉnh (Chuẩn Nginx 1.30.5 / 1.31.6)
Một nâng cấp đáng chú ý cho các SysAdmin: Kể từ nhánh Nginx 1.30.0 Stable (và 1.29.7 Mainline phát hành tháng 04/2026), Nginx đã chuyển giao thức mặc định của proxy_pass sang HTTP/1.1 với Keep-Alive được bật sẵn cho khối upstream. Vì vậy, trên Nginx 1.30.5 / 1.31.6, bạn không còn phải lặp lại dòng proxy_http_version 1.1; và proxy_set_header Connection ""; một cách thủ công ở từng khối location chỉ để giữ kết nối keep-alive tới Node.js backend nữa (chỉ cần khai báo khi dùng WebSockets).
Dưới đây là bản cấu hình tổng hợp hoàn chỉnh đã đồng bộ đầy đủ 8 quy tắc lọc SVG:
# =====================================================================
# VIRTUAL PATCH TẠM THỜI CHO NEXT.JS CVE-2026-94545 (GHSA-vcvr-r3jv-pc5j)
# Yêu cầu phiên bản: Nginx >= 1.30.5 (Stable) hoặc >= 1.31.6 (Mainline)
# Đặt phần khai báo map, real_ip và limit_req_zone trong khối http {}
# tại file /etc/nginx/nginx.conf
# =====================================================================
http {
# --- 1. Khôi phục IP thực nếu VPS đứng sau Cloudflare ---
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 103.21.244.0/22;
set_real_ip_from 103.22.200.0/22;
set_real_ip_from 103.31.4.0/22;
set_real_ip_from 141.101.64.0/18;
set_real_ip_from 108.162.192.0/18;
set_real_ip_from 190.93.240.0/20;
set_real_ip_from 188.114.96.0/20;
set_real_ip_from 197.234.240.0/22;
set_real_ip_from 198.41.128.0/17;
set_real_ip_from 162.158.0.0/15;
set_real_ip_from 104.16.0.0/13;
set_real_ip_from 104.24.0.0/14;
set_real_ip_from 172.64.0.0/13;
set_real_ip_from 131.0.72.0/22;
real_ip_header CF-Connecting-IP;
real_ip_recursive on;
# --- 2. Bộ lọc phát hiện mẫu SVG Injection trên URI & Query String ---
map_hash_bucket_size 128;
map_hash_max_size 4096;
map $request_uri $og_svg_attack {
default 0;
# 1. Chặn các thẻ XML/SVG/HTML nguy hiểm ở cả dạng thô (<) và URL-encoded (%3C)
"~*(<|%3c)\s*(svg|script|foreignobject|image|use|iframe|embed|object|animate|set|style|title|desc|defs|symbol)\b" 1;
"~*(%3c%2f|</)\s*(svg|script|style|title|desc)" 1;
# 2. Chặn các ký tự đóng/mở thẻ và phá vỡ ngữ cảnh thuộc tính/style (", ', &)
"~*[<>\"']" 1;
"~*(%3c|%3e|%22|%27)" 1;
# 3. Chặn các Event Handler tự động kích hoạt (onload=, onerror=...)
"~*on(load|error|click|mouseover|activate|begin|end)\s*(=|%3d)" 1;
# 4. Chặn các giao thức và thuộc tính tham chiếu ngoại lai (xlink:href, XInclude, ENTITY, CSS url)
"~*xlink(:|%3a)href" 1;
"~*(javascript|data|vbscript)\s*(:|%3a)" 1;
"~*(xinclude|xi:include|<!entity|system\s+)" 1;
}
# --- 3. Phân tách Rate Limit: Miễn trừ cho Social Crawlers hợp lệ ---
map $http_user_agent $og_limit_key {
default $binary_remote_addr;
"~*facebookexternalhit" "";
"~*Twitterbot" "";
"~*ZaloBot" "";
"~*TelegramBot" "";
"~*LinkedInBot" "";
"~*Slackbot" "";
"~*WhatsApp" "";
}
# Cấp phát vùng nhớ 10MB (~160.000 IP), giới hạn 5 requests/giây
limit_req_zone $og_limit_key zone=og_protection_zone:10m rate=5r/s;
# Định dạng log riêng để theo dõi các request bị WAF chặn
log_format waf_og_log '$remote_addr - [$time_local] "$request" '
'status=$status ua="$http_user_agent" '
'svg_attack=$og_svg_attack';
upstream nextjs_backend {
server 127.0.0.1:3000;
keepalive 32; # Trên Nginx 1.30+, HTTP/1.1 keep-alive tới upstream đã được bật mặc định
}
# =================================================================
# KHỐI SERVER BẢO VỆ ỨNG DỤNG NEXT.JS
# =================================================================
server {
listen 443 ssl;
listen 443 quic; # Hỗ trợ HTTP/3 (Đảm bảo Nginx >= 1.30.5 để an toàn trước CVE-2026-90439)
http2 on;
server_name your-domain.com;
# ... (Cấu hình chứng chỉ SSL của bạn) ...
# Ghi log riêng cho các request có dấu hiệu tấn công SVG
access_log /var/log/nginx/og_waf_blocked.log waf_og_log if=$og_svg_attack;
# Khoanh vùng bảo vệ các endpoint tạo ảnh Open Graph động
location ~* ^/(api/og|.*/opengraph-image|.*/twitter-image)$ {
# Ngắt kết nối TCP lập tức (mã 444) nếu phát hiện mẫu SVG Injection
# (Hoặc đổi thành 'return 403;' nếu bạn muốn trả về mã HTTP chuẩn khi debug)
if ($og_svg_attack = 1) {
return 444;
}
# Chặn các query string dài bất thường (vượt quá 1024 ký tự)
if ($query_string ~ "^.{1024,}$") {
return 414;
}
# Chỉ cho phép phương thức GET và HEAD
limit_except GET HEAD {
deny all;
}
# Áp dụng Rate Limiting, ghi log mức warn cho Fail2ban và ngắt kết nối (444) hoặc trả 429
limit_req zone=og_protection_zone burst=10 nodelay;
limit_req_log_level warn;
limit_req_status 429; # Có thể đặt là 444 để đóng kết nối ngay với bot fuzzing
# Giới hạn kích thước body và timeout chống tấn công ngâm kết nối (Slowloris)
client_max_body_size 1k;
client_body_timeout 10s;
client_header_timeout 10s;
# Tăng cường Header bảo mật: Chặn thực thi script
add_header Content-Security-Policy "default-src 'none'; script-src 'none'; object-src 'none'; sandbox;" always;
add_header X-Content-Type-Options "nosniff" always;
# TÙY CHỌN: Chỉ bỏ comment dòng dưới nếu endpoint của bạn trả về định dạng SVG thô thay vì PNG
# (Tránh bật mặc định với ảnh PNG vì một số crawler mạng xã hội có thể từ chối render khi thấy header attachment)
# add_header Content-Disposition "attachment; filename=og-preview.svg" always;
proxy_pass http://nextjs_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Các route còn lại của ứng dụng Next.js hoạt động bình thường
location / {
proxy_pass http://nextjs_backend;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
}
}
Trước khi nạp lại dịch vụ theo hướng dẫn sử dụng systemctl để quản lý dịch vụ và sửa lỗi VPS, hãy sao lưu file cấu hình, kiểm tra cú pháp bằng nginx -t và kết hợp công cụ phân tích tĩnh gixy để rà soát xem có lỗi logic bảo mật hay xung đột chỉ thị add_header nào không:
Sao lưu file cấu hình hiện tại:
sudo cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.bak_$(date +%F)
Kiểm tra cú pháp Nginx và phân tích cấu hình bằng gixy:
sudo nginx -t
gixy /etc/nginx/nginx.conf
Nạp lại dịch vụ Nginx để áp dụng thay đổi:
sudo systemctl reload nginx
Kiểm thử (Testing), tự động khóa IP và lộ trình khắc phục tận gốc
Sau khi reload Nginx thành công, bạn cần chạy ngay các bài test thực tế để đảm bảo lớp lọc hoạt động đúng thiết kế và thiết lập cơ chế tự động khóa các IP cố tình tấn công.
Kiểm thử phản hồi bằng curl và tự động khóa IP rà quét bằng Fail2ban
Hãy mở terminal và chạy 3 kịch bản kiểm thử dưới đây:
Kịch bản 1 – Kiểm thử request hợp lệ có chứa tiếng Việt có dấu (Kỳ vọng trả về HTTP/2 200):
curl -I "https://your-domain.com/api/og?title=C%E1%BA%A5u%20h%C3%ACnh%20VPS%20an%20to%C3%A0n"
Kịch bản 2 – Kiểm thử request chứa payload SVG/Script bị mã hóa URL (Kỳ vọng báo lỗi Empty reply from server do mã 444 đóng thẳng kết nối TCP, hoặc HTTP 403 nếu dùng return 403):
curl -I "https://your-domain.com/api/og?title=%3Csvg%20onload=alert(1)%3E"
Kịch bản 3 – Gửi request bằng phương thức POST trái phép (Kỳ vọng trả về HTTP/2 403 do limit_except chặn):
curl -I -X POST "https://your-domain.com/api/og?title=Hello"
Để theo dõi trực tiếp các địa chỉ IP đang dò tìm lỗ hổng CVE-2026-94545 trên VPS thông qua file /var/log/nginx/og_waf_blocked.log (hoặc đẩy dữ liệu về hệ thống quản lý log VPS bằng Grafana Loki), bạn có thể chạy lệnh thống kê nhanh 10 địa chỉ IP gửi nhiều payload tấn công vào endpoint OG:
sudo awk '{print $1}' /var/log/nginx/og_waf_blocked.log | sort | uniq -c | sort -nr | head -n 10
Thay vì phải lọc IP thủ công rồi chèn từng dòng vào tường lửa bằng tay, bạn nên tự động hóa hoàn toàn quy trình khóa IP ở tầng mạng bằng Fail2ban kết hợp với filter nginx-limit-req có sẵn, tham khảo giải pháp cài đặt CrowdSec trên VPS để thay thế Fail2ban, hoặc sử dụng module hiện đại nginx-module-dynamic-limit-req / nginx-module-redis-rate-limit nếu bạn vận hành cụm nhiều máy chủ Nginx. Chỉ cần mở file /etc/fail2ban/jail.local và thêm 6 dòng cấu hình sau:
[nginx-limit-req]
enabled = true
filter = nginx-limit-req
logpath = /var/log/nginx/error.log
maxretry = 10
findtime = 60
bantime = 3600
Ngay khi một nguồn truy cập vượt ngưỡng limit_req 10 lần trong vòng 60 giây (được Nginx ghi nhận vào error.log nhờ chỉ thị limit_req_log_level warn;), Fail2ban sẽ tự động đẩy quy tắc xuống iptables / nftables để khóa chặt IP đó trong 1 giờ mà bạn không cần can thiệp thủ công.
Tại sao không nên chủ quan với hàm Sanitize tự viết và quy trình nâng cấp next@16.3.6
Một sai lầm phổ biến của nhiều đội ngũ phát triển khi đối phó với CVE-2026-94545 là tự viết một hàm .replace() đơn giản để mã hóa 5 ký tự XML (&, <, >, ", ') trong code TypeScript rồi cho rằng hệ thống đã an toàn trước cả dữ liệu từ Database.
Tuy nhiên, chính Advisory gốc của Satori (GHSA-wx4j-mvgx-mqwp) đã cảnh báo rõ: Không có bất kỳ biện pháp tạm thời (workaround) hay hàm lọc đầu vào thủ công nào có thể thay thế hoàn toàn cho việc nâng cấp bản vá. Nguyên nhân là do quá trình phân tích cú pháp SVG và biên dịch CSS styles của Satori sang chuỗi thư viện phía sau rất phức tạp; nếu dữ liệu người dùng đi vào các thuộc tính hoặc thuộc tính style, kẻ tấn công có thể chèn payload mà không cần dùng tới dấu đóng mở thẻ < hay >.
Nếu bắt buộc phải giữ code chạy tạm trong vài giờ trước khi nâng cấp, bên cạnh việc dùng hàm escape ký tự XML cơ bản, cách phòng vệ an toàn ở tầng code là ngừng truyền trực tiếp mọi biến văn bản do người dùng kiểm soát vào ImageResponse (chỉ truyền các giá trị đã qua đối chiếu danh sách trắng cố định). Đồng thời, nếu dự án của bạn cũng sử dụng Next.js Image Optimization API (/_next/image) và có cho phép phục vụ ảnh SVG tĩnh (dangerouslyAllowSVG: true), hãy gia cố thêm chính sách bảo mật cho luồng Image Optimization trong next.config.ts (lưu ý cấu hình này bảo vệ cho API /_next/image, tách biệt với luồng next/og):
// next.config.ts - Gia cố bảo mật cho Next.js Image Optimization API (/_next/image) khi có dùng SVG
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
images: {
contentDispositionType: 'attachment',
contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;",
},
};
export default nextConfig;
Quy trình nâng cấp triệt để lên bản vá chính thức (Ngày 22/09/2026):
Ngay khi pipeline CI/CD và môi trường staging đã sẵn sàng, hãy tiến hành cập nhật đồng bộ cả gói next, satori lẫn gói @vercel/og (nếu dự án có cài đặt độc lập, vì việc nâng cấp Next.js sẽ không tự động cập nhật gói @vercel/og cài riêng bên ngoài).
1. Đối với dự án chạy Next.js nhánh 16.x (từ 16.2.0 đến 16.3.5):
npm install next@16.3.6
2. Đối với dự án chạy Next.js nhánh 15.x (cập nhật bản gia cố phòng ngừa):
npm install next@15.5.26
3. Quan trọng – Nếu dự án có cài đặt độc lập gói @vercel/og hoặc satori trong package.json:
npm install @vercel/og@1.0.3 satori@0.33.5
Sau khi chạy lệnh cài đặt, hãy thực hiện quy trình Clean Rebuild chuẩn để đảm bảo không còn mã cũ lưu trong bộ nhớ đệm:
- Xóa sạch thư mục build cũ:
rm -rf .next - Kiểm tra lại phiên bản trong lockfile:
jq '.packages["node_modules/next"].version' package-lock.json(kết quả trả về"16.3.6"). - Biên dịch lại ứng dụng:
npm run buildvà khởi động lại dịch vụ PM2 / Docker Container. - Xác nhận trực tiếp trên runtime đang chạy:
node -e "console.log(require('next/package.json').version)".
Khi xác nhận hệ thống đã chạy ổn định trên phiên bản 16.3.6, bạn có thể gỡ bỏ rule chuyển hướng ảnh tĩnh (Lớp 1) hoặc nới lỏng bộ lọc map (Lớp 2), nhưng nên duy trì cấu hình limit_req_zone kết hợp Fail2ban (Lớp 3) trên Nginx như một lớp bảo vệ thường trực cho hạ tầng VPS của mình.
Kết luận
Sự cố CVE-2026-94545 là một minh chứng rõ nét cho thấy một tính năng tưởng chừng đơn giản như tạo ảnh preview mạng xã hội (next/og) vẫn có thể mở ra cánh cửa thực thi mã từ xa nghiêm trọng khi dữ liệu đầu vào đi qua chuỗi thư viện render phức tạp trên máy chủ. Việc làm chủ kỹ thuật cấu hình Nginx Reverse Proxy chặn RCE Next.js giúp các System Admin và Fullstack Developer chủ động dựng ngay rào chắn bảo vệ VPS chỉ trong vài phút, giữ cho hệ thống an toàn trước các đợt tấn công dò tìm tự động trong suốt thời gian chờ kiểm thử và triển khai bản cập nhật next@16.3.6 cùng @vercel/og@1.0.3.
Tài liệu tham khảo
- Next.js Security Update for a Critical Upstream Issue | Next.js
- Improper escaping in Satori-generated SVG · Advisory · vercel/satori · GitHub
- Remote Code Execution in next/og ImageResponse · Advisory · vercel/next.js · GitHub
- CVE-2026-94545 | Tenable®
- CVE-2026-94545: When Your OG Image Generator Becomes an RCE (Next.js ImageResponse)
- nginx news: 2026
- Module ngx_http_map_module
- Module ngx_http_limit_req_module
- Module ngx_http_realip_module
