Chuyển hướng mở (open redirect) là khi ứng dụng của bạn nhận một URL từ người dùng rồi chuyển hướng trình duyệt tới đó — mà không kiểm URL đó dẫn đi đâu. Nghe vô hại, nhưng nó là công cụ ưa thích của lừa đảo: một link trông như bank.com/di?next=... thực chất đẩy nạn nhân sang trang giả mạo. Bài này đo open redirect bằng trình duyệt thật, rồi nối nó với một cái bẫy liên quan — rò rỉ qua header Referer — và đo xem trình duyệt hiện đại đã vá được bao nhiêu.
Toàn bộ chạy trên hai ứng dụng tự dựng trong container, tự dọn. Không nhắm vào hệ thống của ai.
Open redirect: khi "bắt đầu bằng /" là không đủ
Ứng dụng có route /di?next=... chuyển hướng tới giá trị next. Câu hỏi: kẻ tấn công có đưa nạn nhân ra host khác không? Tôi thử với Chromium thật và xem URL cuối cùng trong thanh địa chỉ.
next |
URL cuối trong trình duyệt |
|---|---|
/dich |
http://app:5000/dich — nội bộ |
//evil:5001/gia-mao |
http://evil:5001/gia-mao — ra ngoài! |
Cách chặn ngây thơ thường gặp là "chỉ cho phép URL bắt đầu bằng /". Nhưng //evil:5001/gia-mao bắt đầu bằng / — và trình duyệt hiểu nó là một URL theo giao thức tương đối: dấu // mở đầu một tên miền mới. Phép kiểm ngây thơ cho nó qua, và trình duyệt thật điều hướng ra host của kẻ tấn công. Các biến thể khác cũng vượt được cách kiểm đó: /\evil.com (dấu gạch chéo ngược mà nhiều trình duyệt chuẩn hoá thành //), https://evil.com trực tiếp, javascript:....
Số đo: cách chặn nào đứng vững
Tôi thử ba cách xử lý tham số next trên năm payload:
| Payload | v0 không kiểm | v1 kiểm "bắt đầu /" |
v2 danh sách trắng |
|---|---|---|---|
/ho-so |
nội bộ | nội bộ | nội bộ |
https://evil.com |
ra ngoài | nội bộ | nội bộ |
//evil.com |
ra ngoài | ra ngoài | nội bộ |
/\evil.com |
ra ngoài | ra ngoài | nội bộ |
javascript:alert(1) |
ra ngoài | nội bộ | nội bộ |
v1 chặn được URL tuyệt đối và javascript:, nhưng thủng với // và /\ — đúng cái bẫy phổ biến nhất. Chỉ v2 — chỉ chấp nhận các đường dẫn nội bộ nằm trong một danh sách định trước — chặn sạch. Lại là bài học danh sách trắng thắng danh sách chặn, lần này áp cho URL đích. Điều đáng chú ý: v1 không phải viết cẩu thả — "phải bắt đầu bằng /" là một trực giác hợp lý, và nó chặn được ba trong năm payload. Nó thua đúng ở chỗ trực giác không lường: // cũng bắt đầu bằng / nhưng mang nghĩa hoàn toàn khác với trình duyệt. Đây là lý do kiểm URL bằng khớp chuỗi luôn mong manh — cú pháp URL có quá nhiều lối vào mà một phép kiểm tiền tố không thể bao hết.
Cái bẫy Referer: một nửa đã được vá
Open redirect thường đi kèm một rò rỉ tinh tế hơn. Khi trình duyệt tải một tài nguyên từ trang khác, nó gửi header Referer cho biết trang hiện tại. Nếu trang hiện tại có bí mật trong URL — như app.com/reset?token=... — bí mật đó có thể rò sang trang ngoài. Tôi đo header Referer mà một trang ngoài thật sự nhận được, qua bốn cấu hình:
| Chính sách của trang chứa token | Referer mà trang ngoài nhận |
|---|---|
| Mặc định (trình duyệt hiện đại) | http://app:5000/ — chỉ origin, không token |
| Same-origin (tài nguyên cùng site) | .../reset-same?token=BI-MAT-... — rò đầy đủ |
Referrer-Policy: unsafe-url |
.../reset-unsafe?token=BI-MAT-... — rò đầy đủ |
Referrer-Policy: no-referrer |
(không có) — không rò gì |
Điều đáng nhớ
Trình duyệt hiện đại đã vá cái bẫy Referer kinh điển — một nửa. Dòng đầu là bất ngờ dễ chịu: với chính sách mặc định (strict-origin-when-cross-origin, mặc định từ khoảng 2020), khi gửi Referer cross-origin, trình duyệt tự cắt bỏ đường dẫn và query — chỉ gửi origin trần http://app:5000/. Token trong query không rò sang trang ngoài. Đây là một chiến thắng của phòng thủ mặc định: hàng triệu trang được bảo vệ khỏi rò token qua Referer mà không đổi một dòng mã.
Nhưng "một nửa" nghĩa là token vẫn rò trong hai trường hợp thật. Dòng hai và ba: nếu tài nguyên tải từ cùng site (same-origin), Referer gửi đầy đủ — token đi kèm. Và nếu ai đó đặt Referrer-Policy: unsafe-url (đôi khi để phân tích lưu lượng), token rò cross-origin như thời xưa. Nên bài học gốc vẫn nguyên giá trị: đừng bao giờ đặt bí mật trong URL. Trình duyệt hiện đại giảm rủi ro, không xoá nó — và bạn không kiểm soát được trình duyệt của mọi người dùng, cũng không nên phụ thuộc vào một mặc định có thể bị ghi đè.
Open redirect + token trong URL là một combo. Ghép hai chủ đề: nếu ứng dụng có open redirect và đặt token trong URL, kẻ tấn công dùng redirect để đưa nạn nhân (đang mang token) sang trang của nó. Dù Referer cross-origin hiện chỉ rò origin, chính cú điều hướng đã là vấn đề — nạn nhân giờ ở trang giả mạo, và mọi kỹ thuật phishing tiếp theo bắt đầu từ đó.
Vì sao
Cả hai lỗ có chung một gốc với mọi bài trong sê-ri: tin một giá trị do người dùng hoặc ngữ cảnh cung cấp mà không kiểm.
- Open redirect tin tham số
nextlà một đích an toàn. Cách chặn: danh sách trắng đích, không kiểm bằng tiền tố chuỗi (dễ vượt như đã đo). - Cái bẫy Referer tin rằng URL là nơi kín đáo để mang trạng thái. Cách chặn: bí mật đi trong thân request hoặc cookie, không trong URL — URL bị ghi vào log, lịch sử, và header Referer.
Nghĩa là gì trong thực tế
- Với tham số redirect, dùng danh sách trắng đích — hoặc dùng một mã tra cứu (
next=trang-chu) thay vì URL trực tiếp. Không kiểm bằng "bắt đầu bằng /". - Không đặt bí mật trong URL. Token đặt lại mật khẩu nên đi trong đường dẫn dùng-một-lần rồi hết hạn nhanh, và tốt nhất không xuất hiện ở các trang nhúng tài nguyên ngoài.
- Đặt
Referrer-Policychặt (strict-origin-when-cross-originhoặcno-referrercho trang nhạy cảm) một cách tường minh, đừng dựa vào mặc định. - Cảnh báo người dùng khi rời site, hoặc chèn một trang trung gian "bạn đang rời khỏi..." cho các redirect ra ngoài không tránh được.
Thử ba mươi giây
Tìm trong ứng dụng của bạn tham số điều khiển chuyển hướng — thường tên là next, redirect, return_to, url, continue. Thử đặt giá trị //example.com (trên môi trường thử).
Nếu trình duyệt điều hướng tới example.com, bạn có một open redirect mà cách kiểm "bắt đầu bằng /" không bắt được. Và trong lúc đó, nhìn thanh địa chỉ các trang nhạy cảm của bạn — trang đặt lại mật khẩu, xác nhận email: có token hay bí mật nào nằm trong URL không? Nếu có, nó đang đi vào lịch sử trình duyệt và log máy chủ, dù Referer hiện đã kín hơn xưa.