HTTP là một giao thức văn bản, và cấu trúc của nó dựa trên một ký tự vô hình: cặp xuống dòng \r\n (CRLF) ngăn cách các header, và \r\n\r\n kết thúc phần header. Chèn tiêu đề HTTP (HTTP header injection, hay CRLF injection) là khi kẻ tấn công đưa được \r\n vào một giá trị mà máy chủ đặt vào header — tạo ra một ranh giới giả, và từ đó chèn header tuỳ ý hoặc cả một phản hồi giả. Bài này dựng lại lỗ và đo cách các thư viện hiện đại chặn nó.
Toàn bộ chạy trong container tự dựng, tự dọn. Không nhắm vào hệ thống của ai.
Cuộc tấn công
Giả sử máy chủ đặt một giá trị người dùng vào header Set-Cookie — ví dụ một mã phiên. Nếu nó nối chuỗi giá trị đó vào response thô, và giá trị chứa \r\n, ranh giới header bị phá. Payload:
phien=abc\r\nSet-Cookie: admin=true\r\n\r\n<html>trang giả mạo
Số đo
Ba lần chạy, kết quả giống hệt.
Server ngây thơ (nối chuỗi) — response bị chèn và chia:
HTTP/1.1 200 OK
Set-Cookie: phien=abc
Set-Cookie: admin=true <- HEADER GIẢ do kẻ tấn công chèn
<- dòng trống: kết thúc header, bắt đầu thân
<html>trang giả mạo <- THÂN GIẢ
Content-Type: text/html
...
Từ một giá trị, kẻ tấn công tạo ra hai header Set-Cookie (đáng lẽ chỉ một) và một thân phản hồi giả xen vào giữa.
Thư viện HTTP hiện đại — chặn ở tầng:
| Cách đặt header | Kết quả |
|---|---|
http.client với giá trị chứa \r\n |
ValueError: Invalid header value |
Werkzeug (Flask) với giá trị chứa \r\n |
ValueError: Header values must not contain newline characters |
Điều đáng nhớ
Cùng một payload: chèn được header giả và chia được phản hồi. Hai Set-Cookie trong kết quả cho thấy sức mạnh: kẻ tấn công đặt được cookie tuỳ ý (ví dụ admin=true) vào trình duyệt nạn nhân. Và \r\n\r\n giữa chừng còn tệ hơn — nó kết thúc phần header sớm và bắt đầu một thân phản hồi do kẻ tấn công viết. Đây gọi là HTTP response splitting: từ một phản hồi, tách ra nội dung giả mà trình duyệt hiển thị như thật. Kết hợp với cache, một phản hồi giả có thể bị lưu và phục vụ cho nhiều người dùng khác.
Thư viện hiện đại đã vá lỗ này ở tầng — lại một mặc-định-an-toàn. Giống XXE và cái bẫy Referer ở các bài trước, đây là kết quả đáng mừng: cả http.client chuẩn của Python lẫn Werkzeug (nền của Flask) đều từ chối đặt một header chứa \r hay \n, ném ValueError ngay. Bạn không thể vô tình chèn header injection nếu dùng các thư viện này qua API bình thường — chúng kiểm mọi giá trị header và chặn ký tự xuống dòng. Xu hướng an-toàn-theo-mặc-định lặp lại: các lỗ kinh điển được đóng ở tầng thư viện, không để lập trình viên phải nhớ.
Nhưng nó thủng ngay khi bạn tự dựng response bằng nối chuỗi. Phần một của phép đo là một server "tự viết HTTP" — nối trực tiếp giá trị vào chuỗi response. Đó là nơi lỗ mở. Ngày nay ít ai tự viết HTTP thô, nhưng nó vẫn xảy ra: một proxy tự chế, một script sinh header thủ công, một đoạn mã cũ đặt Location: hay Set-Cookie: bằng cách ghép chuỗi. Ở đâu giá trị người dùng chạm vào một header mà không qua thư viện kiểm, ở đó CRLF injection sống lại.
Hậu quả không dừng ở một phản hồi
Đáng nói vì sao response splitting nguy hiểm hơn vẻ ngoài "chèn một cookie". Ba hậu quả leo thang:
- Đặt cookie tuỳ ý. Như trong phép đo —
Set-Cookie: admin=truegiả. Kết hợp với session fixation (bài phiên đăng nhập), kẻ tấn công gieo được một session ID nó biết trước. - Chèn nội dung giả mà trình duyệt tin. Thân phản hồi giả đến từ chính tên miền của bạn, nên nó qua được mọi kiểm same-origin — một dạng XSS phản chiếu qua header thay vì qua thân.
- Đầu độc cache. Nếu một cache (CDN, proxy) lưu phản hồi bị chia, nó phục vụ nội dung giả cho mọi người dùng sau đó — biến một lỗ ảnh hưởng một nạn nhân thành một lỗ ảnh hưởng hàng loạt. Đây là biến thể nguy hiểm nhất, vì nó không cần dụ từng nạn nhân.
Điểm chung: một chèn được không chỉ thêm một dòng — nó cho kẻ tấn công viết lại cấu trúc của phản hồi, và mọi thứ dựa vào cấu trúc đó (trình duyệt, cache) đều bị lừa theo.
Vì sao
CRLF injection là một biến thể hoàn hảo của bài học xuyên suốt sê-ri: ký tự điều khiển của giao thức lẫn vào dữ liệu người dùng. Danh sách các lỗ cùng cấu trúc giờ đã dài:
- SQL injection:
'là ký tự điều khiển của SQL. - XSS:
<là ký tự điều khiển của HTML. - Command injection:
;\nlà ký tự điều khiển của shell. - Header injection:
\r\nlà ký tự điều khiển của HTTP.
Mọi trường hợp đều là cùng một sai lầm: một chuỗi vừa mang dữ liệu (giá trị người dùng) vừa mang cấu trúc (cú pháp giao thức), và không có ranh giới rõ giữa hai thứ. Cách chặn cũng cùng nguyên tắc: hoặc tách dữ liệu khỏi cấu trúc (dùng API thư viện, không nối chuỗi), hoặc vô hiệu hoá ký tự điều khiển trong dữ liệu (loại bỏ \r\n). Thư viện HTTP hiện đại chọn cách sau — kiểm và từ chối \r\n trong mọi giá trị header.
Nghĩa là gì trong thực tế
- Đặt header qua API thư viện, đừng nối chuỗi response thô.
response.set_cookie(...),response.headers[...] = ...— chúng kiểm CRLF cho bạn. Tự ghép chuỗi"Set-Cookie: " + gia_trilà bỏ qua lớp bảo vệ đó. - Cảnh giác với header lấy từ đầu vào người dùng:
Location(redirect),Set-Cookie, các header tuỳ biến. Nếu giá trị đến từ tham số hay dữ liệu người dùng, đảm bảo nó đi qua một API kiểm CRLF. - Loại bỏ
\rvà\nkhỏi mọi giá trị trước khi đưa vào header, như một lớp phòng thủ thêm — kể cả khi bạn tin thư viện đã làm. - Kiểm phiên bản thư viện. "Mặc định an toàn" là thành quả cập nhật; thư viện HTTP cũ có thể vẫn để lọt CRLF.
Chỗ tôi không kết luận được
Tôi đo trên Python với http.client và Werkzeug. Các nền tảng khác có mức bảo vệ khác nhau, và một số máy chủ/proxy tầng thấp (viết bằng C) từng có lỗ CRLF ngay trong bản thân server, không chỉ ở mã ứng dụng. "Thư viện Python hiện đại chặn" không suy ra được cho mọi thành phần trong đường đi của một request.
Và tôi mô phỏng server ngây thơ bằng cách nối chuỗi trong Python để minh hoạ cơ chế — tôi không dựng một máy chủ HTTP thô nhận request thật rồi chia response qua mạng. Cơ chế thì rõ, và điểm mấu chốt — \r\n phá cấu trúc header — không đổi dù đo ở đâu.
Thử ba mươi giây
Tìm trong mã của bạn mọi chỗ đặt giá trị động vào một HTTP header — thường là Location cho redirect, Set-Cookie, hay header tuỳ biến mang thông tin từ request.
Với mỗi chỗ, xem giá trị đó đi qua API của framework (redirect(...), set_cookie(...)) hay được ghép chuỗi thủ công. Nếu là ghép chuỗi, thử đưa vào một giá trị chứa %0d%0a (CRLF sau khi giải mã URL) theo sau là một header giả. Nếu header giả xuất hiện trong phản hồi, bạn có một CRLF injection — và với header Set-Cookie, đó là đường đặt cookie tuỳ ý vào trình duyệt nạn nhân.