0

Giải Phẫu Sự Cố Cloudflare Edge & Next.js 16: Triệt Tiêu Thảm Họa RSC Flight Payload Cache Poisoning

Đúng 09:14 sáng một ngày đầu tuần, kênh hỗ trợ khách hàng của hệ thống tiếp nhận hàng loạt phản ánh khẩn cấp từ người dùng:

"Ứng dụng trên iPhone không mở được! Màn hình trắng xóa toàn ký tự lạ như :HL[\"/static/css/...\"] và 1:I[\"$Sreact.fragment\"...]."

Người dùng khởi động ứng dụng Progressive Web App (PWA) từ màn hình chính iOS Home Screen chỉ thấy một chuỗi dữ liệu thô dạng văn bản của giao thức React Server Component (RSC) Flight thay vì giao diện HTML hoàn chỉnh. Ngay cả người dùng máy tính để bàn khi nhấn tổ hợp phím tải lại trang (Ctrl + F5) cũng gặp hiện tượng tương tự.

Tuy nhiên, bảng điều khiển hệ thống nội bộ lại hiển thị 100% màu xanh. CPU máy chủ Node.js chỉ ở mức 4%, không có ngoại lệ nào chưa được xử lý, và API nội bộ phản hồi cực nhanh dưới 8ms.

Hệ thống đã rơi vào chiếc bẫy nguy hiểm mang tên: RSC Flight Payload Cache Poisoning tại Edge CDN.

Tôi là Dương Phúc Khánh (được biết đến trong cộng đồng kỹ sư với tên gọi khanhdp hoặc Kido, @khanhduong22). Trong vai trò Senior Systems Architect tại Tech Pulse và tác giả của 128 tập biên niên sử kỹ thuật Kido Chronicles, tôi đã trực tiếp mổ xẻ và xử lý triệt để sự cố này.


1. Cơ Chế Điều Hướng Của Next.js App Router: HTML Stream vs Flight Wire Data

Trong kiến trúc Next.js App Router, cùng một URL (ví dụ: /dashboard) có thể trả về hai định dạng dữ liệu hoàn toàn khác biệt tùy thuộc vào ngữ cảnh gửi yêu cầu:

  1. Full Document Navigation (Yêu cầu HTML ban đầu): Khi người dùng gõ URL vào thanh địa chỉ hoặc mở PWA lần đầu, trình duyệt gửi request với header Accept: text/html. Máy chủ Next.js render mã HTML hoàn chỉnh kèm theo dữ liệu Flight để thực hiện Hydration.
  2. Soft Client Navigation (Chuyển trang nội bộ qua Next Link): Khi người dùng nhấp vào thẻ <Link href="/dashboard">, router phía client của Next.js không tải lại toàn bộ trang mà chỉ gửi một yêu cầu nền kèm theo header đặc biệt:
    RSC: 1
    Next-Router-State-Tree: ...
    Accept: text/x-component
    
    Lúc này, Next.js bỏ qua phần vỏ HTML và chỉ trả về Flight Payload thuần túy chứa danh sách Virtual DOM diff để client cập nhật giao diện.

2. Điểm Mù Của Edge Cache: Tại Sao Cloudflare Lưu Nhầm Flight Data?

Theo mặc định của hầu hết các dịch vụ CDN (bao gồm Cloudflare, Fastly, AWS CloudFront), khóa định danh bộ nhớ đệm (Cache Key) chỉ bao gồm:

Cache Key=Scheme+Host+URI Path\text{Cache Key} = \text{Scheme} + \text{Host} + \text{URI Path}

Nếu quản trị viên thiết lập một quy tắc lưu đệm Edge Cache (ví dụ: Cache-Control: public, max-age=3600) cho các trang công khai mà không phân biệt header RSC, thảm họa sẽ xảy ra theo kịch bản sau:

  1. Bước 1: Một người dùng đang lướt trang web nhấp vào link nội bộ. Trình duyệt gửi request tới /articles/ với header RSC: 1.
  2. Bước 2: Yêu cầu đi qua Edge CDN và chạm tới máy chủ Next.js. Next.js trả về nội dung Flight Payload kèm header Content-Type: text/x-component.
  3. Bước 3: Edge CDN lưu trữ nội dung Flight Payload này vào Cache Key: https://khanhdp.com/articles/.
  4. Bước 4 (Đầu độc thành công): Một người dùng mới truy cập trực tiếp bằng Safari hoặc mở PWA. Trình duyệt mong đợi nhận được HTML (Accept: text/html), nhưng Edge CDN ngay lập tức trả về Flight Payload dạng text đang được lưu trong cache! Trình duyệt render chuỗi text thô ra màn hình thay vì giao diện Web.

3. Kiến Trúc Phòng Thủ 3 Lớp (3-Layer Edge Defense Architecture)

Để triệt tiêu vĩnh viễn nguy cơ đầu độc bộ nhớ đệm mà không làm mất đi lợi thế tăng tốc của Edge CDN, chúng tôi đã triển khai kiến trúc bảo vệ đa tầng:

[Client Request] 
       │
       ▼
[Layer 1: Cloudflare Custom Cache Key] ➔ Tách biệt Cache Key theo Header (RSC: 0 vs 1)
       │
       ▼
[Layer 2: Reverse Proxy (Caddy 2)]    ➔ Thêm Vary: RSC, Next-Router-State-Tree, Accept
       │
       ▼
[Layer 3: Next.js Response Guard]     ➔ Tự động hạ cấp Cache-Control: private nếu có RSC header

Lớp 1: Cấu Hình Cache Key Partitioning Trên Cloudflare

Sử dụng Cloudflare Cache Rules để phân tách khóa lưu đệm dựa trên sự hiện diện của header RSC:

Expression: (http.request.uri.path eq "/articles" and http.request.headers["rsc"] eq "1")
Action:
  - Cache Key: Include Header "RSC"
  - Edge Cache TTL: 0 (Bypass Cache for dynamic RSC transitions)

Lớp 2: Chuẩn Hóa Header Vary Tại Reverse Proxy (Caddyfile)

Reverse Proxy phải luôn luôn đính kèm chỉ thị Vary để báo hiệu cho mọi tầng proxy trung gian biết rằng nội dung thay đổi theo header:

khanhdp.com {
    @is_rsc {
        header RSC 1
    }

    handle {
        header {
            # Bắt buộc CDN và Browser phải tách cache theo RSC và Accept
            Vary "Accept, RSC, Next-Router-State-Tree"
            # Bảo vệ chống clickjacking và mime-sniffing
            X-Content-Type-Options "nosniff"
        }
        reverse_proxy localhost:3000
    }
}

Lớp 3: Response Header Interceptor Tại Next.js Middleware

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const isRSC = request.headers.has('rsc');
  const response = NextResponse.next();

  // Đảm bảo header Vary luôn hiện diện
  response.headers.append('Vary', 'RSC');
  response.headers.append('Vary', 'Next-Router-State-Tree');
  response.headers.append('Vary', 'Accept');

  // Nếu là RSC request, tuyệt đối không cho phép CDN cache công khai
  if (isRSC) {
    response.headers.set(
      'Cache-Control',
      'private, no-cache, no-store, max-age=0, must-revalidate'
    );
  }

  return response;
}

4. Kết Quả Xác Minh Bằng Kiểm Thử Tự Động Playwright E2E

Sau khi triển khai bộ lọc 3 lớp, chúng tôi thiết lập kịch bản kiểm thử hồi quy tự động trong Playwright để đảm bảo lỗi không bao giờ tái diễn:

  1. Thực hiện Soft Navigation giả lập header RSC: 1 để ghi cache tại Edge.
  2. Thực hiện Hard Navigation ngay sau đó với header Accept: text/html.
  3. Kiểm tra mã phản hồi HTTP phải là 200, header Content-Type phải chứa text/html, và DOM phải render đầy đủ thẻ <main> mà không chứa bất kỳ ký tự raw Flight nào.

Toàn bộ 100% ca kiểm thử đã vượt qua xuất sắc trên cả Safari di động và Chrome máy tính để bàn.


Tài liệu tham khảo & Hệ sinh thái kỹ thuật


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí