Script Lạ Chạy Trên Website? Ngăn Chặn XSS Với CSP

Bạn mở DevTools và phát hiện có những đoạn script đang chạy mà bạn chưa bao giờ viết? Hoặc user báo cáo trang web “hành động lạ” - đó có thể là dấu hiệu XSS (Cross-Site Scripting) đang xảy ra. Mã độc được chèn vào website qua form input, URL, hoặc từ bên thứ ba không an toàn, khiến dữ liệu người dùng bị đánh cắp hoặc session bị hijack.

Content Security Policy (CSP) là giải pháp giúp bạn kiểm soát chặt chẽ những nguồn tài nguyên (script, style, image…) được phép chạy trên website. Bằng cách thiết lập whitelist rõ ràng, CSP chặn các script không đáng tin cậy ngay từ trình duyệt, bảo vệ người dùng khỏi XSS và session hijacking.

#1. Vấn đề: Script độc hại chạy trên website như thế nào?

#1.1. XSS (Cross-Site Scripting) hoạt động ra sao?

Hacker chèn mã JavaScript độc hại vào website thông qua:

  • Input không được validate: Form comment, search box, user profile
  • URL parameters: ?name=<script>alert('XSS')</script>
  • Third-party scripts: CDN bị tấn công, thư viện không rõ nguồn gốc

Khi script này thực thi, nó có thể:

  • Đánh cắp cookie/session token
  • Redirect người dùng sang trang lừa đảo
  • Thay đổi nội dung trang (defacement)
  • Gửi dữ liệu nhạy cảm về server của hacker

Luồng tấn công XSS điển hình:

View Mermaid diagram code
sequenceDiagram
    participant Hacker
    participant Website
    participant Victim
    participant HackerServer

    Hacker->>Website: Chèn script độc vào comment/form
    Website->>Website: Lưu vào database (không sanitize)
    Victim->>Website: Truy cập trang có script độc
    Website-->>Victim: Trả về HTML kèm script độc
    Victim->>Victim: Trình duyệt thực thi script
    Victim->>HackerServer: Gửi cookie/session token
    HackerServer-->>Hacker: Nhận được thông tin nhạy cảm

#1.2. Tại sao validate input không đủ?

Dù bạn đã làm sạch input ở backend, vẫn có rủi ro:

  • Lỗi trong logic validation
  • Script từ third-party (ads, analytics) bị tấn công
  • Browser extension độc hại inject code

CSP giải quyết vấn đề này như thế nào? Nó hoạt động ở tầng trình duyệt - chặn mọi script không nằm trong whitelist, kể cả khi chúng đã lọt qua validation.

#2. Giải pháp: Content Security Policy (CSP) là gì?

CSP là một cơ chế bảo mật cho phép bạn quy định danh sách các nguồn nội dung được phép tải. Thông qua HTTP header hoặc meta tag có tên Content-Security-Policy, trình duyệt chỉ thực thi script từ những nguồn bạn tin tưởng.

Cách CSP bảo vệ website:

View Mermaid diagram code
flowchart TD
    Start([Website gửi response + CSP header]) --> Browser[Trình duyệt nhận trang HTML]
    Browser --> Parse[Parse HTML và CSP policy]
    Parse --> Script{Gặp thẻ script}
    Script --> Check{Nguồn script có trong whitelist?}
    Check -->|Có: 'self', nonce, CDN tin cậy| Allow[✅ Cho phép thực thi]
    Check -->|Không: inline, domain lạ| Block[❌ Chặn và báo lỗi console]
    Allow --> Safe[Website an toàn]
    Block --> Safe

Ví dụ thực tế:

  • Script từ mywebsite.com/app.js → ✅ Cho phép (nếu dùng 'self')
  • Script inline <script>alert('XSS')</script> → ❌ Chặn (không có nonce)
  • Script từ evil.com/hack.js → ❌ Chặn (không nằm trong whitelist)

#3. Cách triển khai CSP trên website

Chọn chiến lược CSP phù hợp:

View Mermaid diagram code
flowchart TD
    Start{Bạn có script inline không?}
    Start -->|Không, dùng file .js| Simple[Dùng script-src 'self']
    Start -->|Có| Inline{Script có thể tách ra file?}
    Inline -->|Được| Simple
    Inline -->|Không: CMS, framework| Strategy{Chọn cách xử lý inline}
    Strategy --> Nonce[Dùng nonce<br/>Tốt nhất, random mỗi request]
    Strategy --> Hash[Dùng hash<br/>Cho script cố định]
    Simple --> CDN{Cần tải từ CDN?}
    Nonce --> CDN
    Hash --> CDN
    CDN -->|Có| AddCDN[Thêm domain CDN vào whitelist]
    CDN -->|Không| Done([Hoàn tất CSP])
    AddCDN --> Done

#3.1. Thêm CSP header

Bạn có thể thêm CSP vào website bằng cách bổ sung HTTP header sau trên máy chủ (server):

1
Content-Security-Policy: script-src 'self'
  • script-src 'self': chỉ cho phép tải script từ cùng domain (chẳng hạn mywebsite.com).
  • Với thiết lập này, mọi script nội tuyến (inline script) sẽ bị chặn nếu không có thêm từ khóa hoặc cài đặt phù hợp.

Ví dụ minh họa:

1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-Security-Policy" content="script-src 'self'">
</head>
<body>
    <h1>Nội dung trang</h1>
    <!-- Giả sử main.js nằm cùng domain -->
    <script src="/js/main.js"></script>
</body>
</html>
  • Khi chạy trang này, script tại file /js/main.js (nằm cùng domain) sẽ được tải và thực thi bình thường.
  • Nếu bạn thử đặt script nội tuyến hoặc tải script từ domain khác, CSP sẽ chặn và báo lỗi.

#3.2. Sử dụng nonce

Để cho phép một đoạn mã nội tuyến an toàn, bạn có thể dùng nonce:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!DOCTYPE html>
<html>
<head>
    <!-- script-src 'self' và nonce -->
    <meta http-equiv="Content-Security-Policy" content="script-src 'self' 'nonce-ABC123'">
</head>
<body>
    <h1>Nội dung trang</h1>
    <!-- Đoạn script nội tuyến có nonce khớp với CSP -->
    <script nonce="ABC123">
        console.log("Script nội bộ được phép chạy với nonce.");
    </script>
</body>
</html>
  • Khi chạy trang này, bạn sẽ thấy thông báo trong console (trình duyệt) là “Script nội bộ được phép chạy với nonce.”không gặp lỗi CSP.
  • Nếu bỏ nonce="ABC123" hoặc đặt nonce sai, trình duyệt sẽ từ chối thực thi đoạn script nội tuyến.

#3.3. Tải script từ nguồn bên ngoài uy tín (ví dụ CDN jQuery)

Trong nhiều trường hợp, bạn cần tải thư viện JavaScript từ CDN. Lúc này, bạn phải cập nhật chính sách CSP để cho phép tải script từ domain của CDN đó. Ví dụ:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!DOCTYPE html>
<html>
<head>
    <!-- Cho phép script từ chính domain ('self') và từ cdnjs.cloudflare.com -->
    <meta http-equiv="Content-Security-Policy" content="script-src 'self' 'nonce-ABC123' https://cdnjs.cloudflare.com">
</head>
<body>
    <h1>Trang sử dụng jQuery từ CDN</h1>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
    <script nonce="ABC123">
        $(document).ready(function() {
            alert("jQuery từ CDN đã được tải thành công!");
        });
    </script>
</body>
</html>
  • Kết quả: Khi bạn mở trang, nếu CSP được thiết lập đúng cách, jQuery sẽ được tải từ cdnjs.cloudflare.com và trang sẽ hiển thị một thông báo “jQuery từ CDN đã được tải thành công!”.

#Lưu ý quan trọng:

Bạn nên tin tưởng vào những CDN uy tín, chẳng hạn cdnjs.cloudflare.com, cdn.jsdelivr.net, hoặc những nhà cung cấp được cộng đồng sử dụng phổ biến. Đồng thời, hãy đảm bảo không vô tình mở rộng script-src quá “rộng” (ví dụ * hoặc http://*) làm giảm hiệu quả bảo mật.

#3.4. Sử dụng hashed script

Ngoài nonce, bạn có thể dùng băm nội dung (hashed script) để chỉ cho phép đúng đoạn mã nội tuyến đã khai báo. Ví dụ:

1
Content-Security-Policy: script-src 'self' 'sha256-<hash_của_script>'

Bạn cần tính trước giá trị SHA-256 của đoạn script nội tuyến. Chỉ những script có mã băm (hash) khớp mới được thực thi.

#4. Các lỗi thường gặp và cách khắc phục

#4.1. Lỗi sai nonce

  • Triệu chứng: Bạn thêm nonce="ABC123" trong script, nhưng header CSP lại khai báo nonce="XYZ999". Trình duyệt chặn script.
  • Cách khắc phục: Đảm bảo nonce trong thẻ <script> trùng khớp với giá trị nonce trong CSP header hoặc meta tag. Nonce thường được sinh ngẫu nhiên trên server mỗi lần request.

#4.2. Vấn đề vòng lặp vô hạn

Thỉnh thoảng, lập trình viên viết mã tự động chèn thêm script hoặc reload trang liên tục, dẫn đến vòng lặp vô hạn (infinite loop):

  • Nguyên nhân: Có một đoạn code cố gắng nạp lại script từ nguồn không được phép, khi bị chặn thì lại thử nạp lại vô hạn.
  • Phòng tránh vòng lặp vô hạn: Kiểm tra logic, hạn chế việc reload trang vô tội vạ hoặc tự chèn script động mà không kiểm soát. Đặc biệt khi dùng CSP, chỉ chèn script từ nguồn tin cậy.

#4.3. Phạm vi biến trong Execution Context

  • Lỗi phổ biến: Dùng biến toàn cục hoặc khai báo thiếu var, let, const khiến các script “giẫm đạp” lẫn nhau, nhất là khi bị thay đổi do script ngoài ý muốn.
  • Liên quan đến CSP: CSP giúp chặn hoặc hạn chế script lạ, nhưng nó không thể thay thế hoàn toàn cho việc tổ chức code tốt. Nếu bạn để quá nhiều biến trong phạm vi toàn cục, một script (được phép hoặc vô tình lọt qua) cũng có thể ghi đè biến, gây lỗi hoặc lỗ hổng bảo mật.
  • Cách khắc phục:
    • Gói gọn biến trong function, module hoặc dùng cú pháp khai báo chuẩn (let, const).
    • Hạn chế biến toàn cục, tránh tình trạng “đụng độ” biến do các script khác nhau.
    • Kết hợp CSP với code “sạch” giúp tăng cường bảo mật từ hai phía:
      1. CSP: Ngăn script độc hại từ bên ngoài.
      2. Phạm vi biến: Giữ cho luồng thực thi bên trong ứng dụng gọn gàng, giảm nguy cơ bị ghi đè biến.

#5. Tối ưu mã trong dự án thực tế

#5.1. Tích hợp CSP trong các framework phổ biến

Nhiều framework Node.js, PHP, Python… cho phép bạn thêm header CSP một cách dễ dàng:

  • Với Express.js (Node.js), dùng helmet:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    const helmet = require("helmet");
    app.use(helmet.contentSecurityPolicy({
      directives: {
        defaultSrc: ["'self'"],
        // Thêm domain được phép để tải thư viện CDN
        scriptSrc: ["'self'", "https://cdnjs.cloudflare.com"]
        // ...
      }
    }));
  • Với PHP, bạn có thể dùng:
    1
    header("Content-Security-Policy: script-src 'self' https://cdnjs.cloudflare.com");
    Tương tự, các framework khác cũng có những plugin/hàm hỗ trợ cài đặt CSP.

#5.2. Phòng tránh vòng lặp vô hạn

Như đã nhắc ở mục 4.2. Phòng tránh vòng lặp vô hạn, bạn nên:

  • Kiểm soát các điều kiện khi chèn script động.
  • Kiểm tra header phản hồi để xác định script được phép nạp lại hay không.
  • Giới hạn số lần gọi lại (retry) nếu bạn thực sự cần nạp script từ nguồn bên ngoài.

#Lưu ý quan trọng:

  • Chỉ thêm nonce hoặc hash khi thực sự cần script nội tuyến. Nếu được, bạn nên sử dụng file .js riêng để dễ quản lý và tránh phải thêm nonce/hash thủ công.

Vậy là bạn đã có giải pháp để chặn script độc hại chạy trên website. CSP hoạt động ở tầng trình duyệt, bảo vệ người dùng ngay cả khi validation backend có lỗ hổng. Trong thực tế, hãy bắt đầu với script-src 'self' rồi dần mở rộng cho CDN tin cậy - đừng dùng * vì sẽ mất hết tác dụng. Kết hợp CSP với code validation chặt chẽ, bạn có thể ngăn chặn XSS và session hijacking hiệu quả!