Clickjacking, postMessage và Tabnabbing Đe Dọa Website - Tìm Hiểu và Phòng Tránh

Website của bạn có thể bị nhúng vào iframe độc hại, người dùng bị lừa click vào các nút ẩn, hoặc tab trình duyệt bị chiếm quyền điều khiển mà không hề hay biết. Ba kiểu tấn công Clickjacking, postMessageTabnabbing đang âm thầm đe dọa hàng triệu website, khai thác lỗ hổng từ iframe và cơ chế giao tiếp cross-domain.

Nếu không phòng tránh kịp thời, kẻ tấn công có thể đánh cắp thông tin đăng nhập, thực hiện giao dịch trái phép, hoặc chuyển hướng người dùng đến trang lừa đảo. Trong bài này, chúng ta sẽ tìm hiểu cơ chế từng loại tấn công và cách phòng tránh hiệu quả bằng X-Frame-Options, CSP, và các biện pháp bảo mật khác.

#1. Tấn công Clickjacking

Người dùng nghĩ rằng họ đang click nút “Xem video miễn phí” hoặc “Tải tài liệu”, nhưng thực tế đang vô tình chuyển tiền, thay đổi mật khẩu, hoặc cấp quyền truy cập cho ứng dụng độc hại. Đây chính là Clickjacking - một trong những kỹ thuật lừa đảo nguy hiểm nhất khai thác lỗ hổng iframe.

#1.1. Cách thức hoạt động

Clickjacking (hay còn gọi là UI Redress Attack) xảy ra khi kẻ tấn công “che giấu” một trang web hợp lệ bên dưới hoặc bên trên một trang web giả mạo bằng iframe trong suốt. Người dùng tưởng rằng họ đang nhấp chuột vào một nút hoặc liên kết thông thường, nhưng thực chất lại nhấp vào nội dung của trang khác, kích hoạt hành động không mong muốn.

Ví dụ minh họa:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- file: malicious-page.html -->
<!DOCTYPE html>
<html>
<head>
  <title>Malicious Page</title>
</head>
<body>
  <h1>Trang giả mạo</h1>
  <p>Bạn nghĩ rằng bạn đang nhấp vào nút “Xem thêm” trên website tin tức, nhưng thực tế...</p>
  <iframe src="https://example.com/real-login" style="opacity: 0.01; position: absolute; top: 50px; left: 50px;"
          width="300" height="200"></iframe>
  <!-- iframe trên dẫn đến trang đăng nhập thực của nạn nhân, đặt ở vị trí trùng với nút -->
</body>
</html>

Khi người dùng bấm vào khu vực “tính toán sẵn”, hành động click sẽ thực hiện trên iframe (trang thật) chứ không phải nút giả mạo, vô tình kích hoạt các hành động nguy hiểm (như đổi mật khẩu, mua hàng, v.v.).

Luồng tấn công Clickjacking:

View Mermaid diagram code
flowchart TD
    Start([Kẻ tấn công tạo trang giả mạo]) --> Embed[Nhúng website nạn nhân vào iframe]
    Embed --> Hide[Đặt iframe trong suốt opacity: 0.01]
    Hide --> Position[Căn chỉnh vị trí iframe trùng với nút giả]
    Position --> User[Người dùng truy cập trang giả mạo]
    User --> Think{Người dùng nghĩ đang click nút giả}
    Think --> Click[Click vào vị trí đã tính toán]
    Click --> Actual[Thực tế click vào iframe website thật]
    Actual --> Action[Kích hoạt hành động nguy hiểm]
    Action --> End([Đổi mật khẩu, chuyển tiền, v.v.])

#1.2. Phòng tránh Clickjacking

#1.2.1. Sử dụng X-Frame-Options

X-Frame-Options là một HTTP header được thiết kế để ngăn chặn các trang web khác nhúng (embed) trang của bạn bằng thẻ iframe. Các giá trị thường dùng:

  • DENY: Không cho phép hiển thị trang trong bất kỳ iframe nào.
  • SAMEORIGIN: Chỉ cho phép hiển thị nếu cùng domain.
  • ALLOW-FROM: Chỉ cho phép hiển thị từ một URI chỉ định (nhiều trình duyệt mới không còn hỗ trợ hiệu quả).

Ví dụ cấu hình trên máy chủ Node.js (Express):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// file: server.js
const express = require('express');
const app = express();

// Thiết lập header X-Frame-Options để ngăn Clickjacking
app.use((req, res, next) => {
  // Có thể dùng 'DENY' nếu muốn chặn hoàn toàn
  res.setHeader('X-Frame-Options', 'SAMEORIGIN');
  next();
});

app.get('/', (req, res) => {
  res.send(`
    <h1>Trang chủ được bảo vệ khỏi Clickjacking</h1>
    <p>Nội dung này sẽ không bị nhúng bởi iframe từ domain khác.</p>
  `);
});

app.listen(3000, () => {
  console.log('Server chạy ở cổng 3000');
});

Ví dụ cấu hình trên Apache (.htaccess):

1
2
3
<IfModule mod_headers.c>
  Header set X-Frame-Options "SAMEORIGIN"
</IfModule>

Ví dụ cấu hình trên Nginx (trong file cấu hình server block hoặc http block):

1
add_header X-Frame-Options "SAMEORIGIN";
#Các thư viện hỗ trợ thiết lập header bảo mật
  • Helmet (dành cho Node.js): Một middleware phổ biến, hỗ trợ đặt nhiều header bảo mật (gồm X-Frame-Options, CSP, v.v.) chỉ với vài dòng cấu hình.
    1
    npm install helmet
    1
    2
    3
    const helmet = require('helmet');
    app.use(helmet.frameguard({ action: 'sameorigin' }));
    // Hoặc action: 'deny' nếu muốn chặn hoàn toàn
  • CORS (Cross-Origin Resource Sharing): Chủ yếu kiểm soát request từ domain khác, không chặn việc nhúng iframe trực tiếp. Vẫn nên kết hợp với X-Frame-Options hoặc CSP để ngăn Clickjacking hiệu quả.

#1.2.2. Sử dụng Content-Security-Policy (CSP)

Ngoài X-Frame-Options, bạn cũng có thể triển khai CSP để kiểm soát chặt chẽ hơn việc trang web của mình bị nhúng hay không. Thuộc tính frame-ancestors trong CSP quy định domain nào được phép nhúng trang của bạn.

Ví dụ cấu hình CSP trên Apache (.htaccess):

1
2
3
<IfModule mod_headers.c>
  Header set Content-Security-Policy "frame-ancestors 'self'"
</IfModule>
  • frame-ancestors 'self' nghĩa là chỉ cho phép iframe từ cùng domain.
  • Nếu muốn chặn toàn bộ, bạn có thể dùng "frame-ancestors 'none'".

Ví dụ cấu hình CSP trên Node.js (Express):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// file: server.js
const express = require('express');
const app = express();

app.use((req, res, next) => {
  // Chỉ cho phép iframe từ chính domain (self)
  res.setHeader('Content-Security-Policy', "frame-ancestors 'self'");
  next();
});

app.get('/', (req, res) => {
  res.send(`
    <h1>Trang chủ với CSP</h1>
    <p>Nội dung này chỉ có thể được nhúng từ cùng một domain.</p>
  `);
});

app.listen(3000, () => {
  console.log('Server chạy ở cổng 3000');
});

Nếu dùng Helmet, bạn có thể cấu hình CSP thông qua thư viện này (tránh cấu hình thủ công):

1
2
3
4
5
6
7
8
9
app.use(
  helmet.contentSecurityPolicy({
    directives: {
      defaultSrc: ["'self'"],
      frameAncestors: ["'self'"],
      // ...
    },
  })
);

#1.2.3. Kiểm tra Referer/Origin

Ngoài X-Frame-Options và CSP, bạn cũng có thể chủ động kiểm tra giá trị Referer hoặc Origin trong các request nhạy cảm (như thanh toán, đổi mật khẩu). Nếu Referer hoặc Origin không thuộc domain tin cậy, bạn có thể từ chối xử lý.

Ví dụ kiểm tra Referer/Origin thủ công (Node.js/Express):

1
2
3
4
5
6
7
8
app.post('/payment', (req, res) => {
  const origin = req.headers.origin || req.headers.referer || '';
  if (!origin.includes('trusteddomain.com')) {
    return res.status(403).send('Forbidden request - không chấp nhận domain này!');
  }
  // Xử lý thanh toán an toàn
  res.send('Payment processed!');
});
#Sử dụng thư viện CORS (hạn chế domain không mong muốn)

CORS giúp khai báo domain hoặc danh sách domain hợp lệ. Dù không ngăn Clickjacking trực tiếp qua iframe, CORS kiểm soát request HTTP đến server, hạn chế các domain giả mạo.

1
npm install cors
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const express = require('express');
const cors = require('cors');
const app = express();

app.use(cors({
  origin: 'https://trusteddomain.com',
  methods: ['GET','POST','PUT','PATCH','DELETE'],
  optionsSuccessStatus: 200
}));

app.post('/payment', (req, res) => {
  // Nếu request không từ https://trusteddomain.com,
  // CORS sẽ chặn trước khi logic bên trong được thực thi
  res.send('Payment processed with CORS check!');
});

app.listen(3000, () => {
  console.log('Server đang chạy tại cổng 3000');
});

Lưu ý:

  • CORS chỉ giải quyết nguồn gốc ở cấp request. Để ngăn giao diện bị nhúng (iframe) từ site khác, vẫn cần X-Frame-Options và/hoặc CSP.
  • RefererOrigin có thể bị giả mạo trong một số tình huống không phải trình duyệt. Đó là lý do cần nhiều lớp bảo vệ khác nhau.

#2. Tấn công postMessage

Website của bạn đang lắng nghe message từ iframe mà không kiểm tra nguồn gốc? Kẻ tấn công có thể gửi dữ liệu độc hại, thay đổi nội dung trang, đánh cắp token xác thực, hoặc thực thi JavaScript trái phép. Tấn công postMessage khai thác lỗ hổng trong giao tiếp cross-domain khi developer không validate event.origin.

#2.1. Cách thức hoạt động

postMessage là API cho phép các tài liệu (hoặc iframe) từ các domain khác nhau trao đổi dữ liệu mà không cần refresh trang. Dù tiện lợi cho việc tích hợp cross-domain, nếu không kiểm soát cẩn thận, dữ liệu có thể bị kẻ xấu chèn vào, dẫn đến chiếm quyền kiểm soát giao diện hoặc đánh cắp thông tin nhạy cảm.

Ví dụ minh họa:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!-- file: sender.html -->
<!DOCTYPE html>
<html>
<head>
  <title>Sender Page</title>
</head>
<body>
  <h1>Trang Gửi Dữ Liệu</h1>
  <button onclick="sendMessage()">Gửi thông điệp</button>

  <script>
    function sendMessage() {
      const receiver = document.getElementById('receiverFrame').contentWindow;
      // Gửi thông điệp sang iframe đích
      receiver.postMessage('Hello from Sender!', 'https://trusted.com');
    }
  </script>

  <iframe id="receiverFrame" src="https://trusted.com/receiver.html"></iframe>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!-- file: receiver.html (thuộc domain https://trusted.com) -->
<!DOCTYPE html>
<html>
<head>
  <title>Receiver Page</title>
</head>
<body>
  <h1>Trang Nhận Dữ Liệu</h1>
  <div id="message"></div>

  <script>
    window.addEventListener('message', (event) => {
      // Kiểm tra origin để chắc chắn rằng dữ liệu đến từ nguồn hợp lệ
      if (event.origin === 'https://yourdomain.com') {
        document.getElementById('message').innerText = 'Received: ' + event.data;
      } else {
        console.warn('Nguồn không hợp lệ:', event.origin);
      }
    }, false);
  </script>
</body>
</html>

Ở ví dụ trên, nếu bạn không kiểm tra event.origin (hoặc domain đích trong hàm postMessage), kẻ tấn công có thể gửi dữ liệu độc hại từ một trang khác đến receiver.html và lợi dụng lỗ hổng này để tấn công.

Luồng tấn công postMessage:

View Mermaid diagram code
sequenceDiagram
    participant Attacker as Trang Độc Hại
    participant Victim as Website Nạn Nhân
    participant User as Người Dùng

    Note over Attacker: Kẻ tấn công tạo trang với iframe
    Attacker->>Victim: Nhúng website nạn nhân vào iframe
    Attacker->>Victim: postMessage(dữ liệu độc hại, '*')

    alt Không kiểm tra origin
        Victim->>Victim: Nhận message mà không validate
        Victim->>Victim: Thực thi dữ liệu độc hại
        Victim-->>Attacker: Trả về thông tin nhạy cảm
        Note over User: Người dùng bị đánh cắp dữ liệu
    else Có kiểm tra origin
        Victim->>Victim: Kiểm tra event.origin
        Victim->>Victim: Từ chối message không hợp lệ
        Note over Victim: Tấn công bị ngăn chặn
    end

#2.2. Phòng tránh postMessage

  • Kiểm tra origin: Chỉ xử lý dữ liệu khi event.origin khớp với danh sách domain đáng tin cậy.
  • Xác thực dữ liệu: Không bao giờ tin tưởng dữ liệu đầu vào, nên tiến hành kiểm tra và lọc trước khi sử dụng.
  • Thiết lập targetOrigin: Khi gọi postMessage, chỉ định chính xác domain đích thay vì dùng '*'.

#Lưu ý quan trọng

  • postMessage thuận tiện cho giao tiếp cross-domain, nhưng cũng có thể trở thành điểm yếu nếu không giới hạn domain cẩn thận.
  • Luôn sử dụng kiểm tra origin và xác thực dữ liệu để tránh bị tấn công chéo site.

#3. Tấn công Tabnabbing

Bạn click vào link “Đọc thêm” mở tab mới, tiếp tục làm việc ở tab cũ. Vài phút sau quay lại tab ban đầu, bạn thấy trang đăng nhập quen thuộc và vô tư nhập mật khẩu - không biết rằng tab đã bị thay đổi thành trang lừa đảo. Đây là Tabnabbing, kỹ thuật tấn công lợi dụng window.opener để chiếm quyền kiểm soát tab gốc.

#3.1. Cách thức hoạt động

Tabnabbing xảy ra khi bạn mở một liên kết trong tab mới (target="_blank") mà không có thuộc tính rel="noopener". Trang mới được mở có thể truy cập window.opener để thay đổi nội dung hoặc chuyển hướng tab cũ đến trang giả mạo. Người dùng không để ý, quay lại tab ban đầu và nhập thông tin nhạy cảm vào trang phishing.

Ví dụ minh họa:

1
2
3
4
5
6
7
8
9
10
11
<!-- file: suspicious-link.html -->
<!DOCTYPE html>
<html>
<head>
  <title>Tabnabbing Demo</title>
</head>
<body>
  <h1>Trang chứa liên kết nghi vấn</h1>
  <a href="malicious-redirect.html" target="_blank">Mở trang mới</a>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- file: malicious-redirect.html -->
<!DOCTYPE html>
<html>
<head>
  <title>Trang Lừa Đảo</title>
  <script>
    // Sau vài giây, đổi URL của tab cũ
    setTimeout(() => {
      window.opener.location = 'phishing-site.html';
    }, 3000);
  </script>
</head>
<body>
  <h1>Đang tải...</h1>
</body>
</html>

Ở trên, sau 3 giây, trang “Tabnabbing” sẽ thay đổi nội dung tab cũ (mà người dùng tưởng là trang gốc) thành một trang lừa đảo.

Luồng tấn công Tabnabbing:

View Mermaid diagram code
flowchart LR
    Start([Người dùng ở Tab A]) --> Click[Click link với target='_blank']
    Click --> NewTab[Tab B mở ra - trang độc hại]
    NewTab --> Access{Tab B có window.opener?}
    Access -->|Có| Control[Tab B kiểm soát Tab A]
    Access -->|Không - có rel='noopener'| Safe[Tab A an toàn]
    Control --> Change[window.opener.location = phishing-site]
    Change --> Fake[Tab A chuyển sang trang giả mạo]
    Fake --> User[Người dùng quay lại Tab A]
    User --> Trick[Tưởng là trang cũ, nhập mật khẩu]
    Trick --> Steal([Kẻ tấn công đánh cắp thông tin])
    Safe --> Protected([Tấn công bị ngăn chặn])

#3.2. Phòng tránh Tabnabbing

  • Sử dụng rel="noopener noreferrer": Khi mở liên kết trong tab mới, hãy thêm thuộc tính này để ngăn trang mới truy cập window.opener.
  • Kiểm soát chặt chẽ liên kết ra ngoài: Chỉ cho phép các liên kết tin cậy và thường xuyên kiểm tra, rà soát mã nguồn.

#Lưu ý quan trọng

  • Dù Tabnabbing kém phổ biến hơn, nhưng một khi người dùng mất cảnh giác, hậu quả có thể nghiêm trọng.
  • Tập thói quen thêm rel="noopener noreferrer" cho mọi liên kết target="_blank" để đảm bảo an toàn.

#4. Sự khác nhau giữa Referer và Origin

  • Referer (hoặc “Referrer”):

    • Chứa URL đầy đủ (hoặc rút gọn) của trang web đã dẫn đến request kế tiếp.
    • Mang tính lịch sử: hiển thị “chúng ta đang đến từ đâu?”.
    • Có thể bị chặn hoặc chỉnh sửa (bởi cài đặt trình duyệt, extension bảo mật).
  • Origin:

    • Chỉ chứa scheme (http/https), domain, và port (nếu không mặc định).
    • Mục đích chính: dùng trong CORS và các tình huống bảo mật để xác định domain nguồn.
    • Trình duyệt thường bắt buộc gửi Origin với các request phức tạp (POST, PUT, DELETE) trong môi trường cross-domain, cung cấp cách kiểm soát domain tốt hơn.

Nhờ đó, khi xét đến bảo mật:

  • Origin thường được coi là đáng tin cậy hơn khi bạn chỉ cần xác thực domain.
  • Referer lại hữu ích trong phân tích hành vi người dùng (chẳng hạn thống kê lượng truy cập), nhưng dễ bị che giấu hoặc mất nếu người dùng tắt/trình duyệt ẩn danh.

Vậy là bạn đã hiểu cách phòng tránh Clickjacking, postMessage và Tabnabbing. Trong thực tế, luôn sử dụng X-Frame-Options, CSP, kiểm tra origin và thêm rel="noopener noreferrer" cho link external. Hãy bảo vệ website của bạn nhé!