Web Security for Frontend Devs · Part 1 — The Browser Security Model & Same-Origin Policy
The foundation every frontend dev must own: what an origin is, what the Same-Origin Policy protects (and what it does not), how cookies and credentials cross the wire, and the mental model for the whole series — with exercises.
Đây là Phần 1 của series 10 bài về những kiến thức bảo mật web mà mọi frontend dev nên biết — và chủ động phòng tránh. Mỗi phần giải thích một mối đe dọa thật, cho xem code lỗ hổng, rồi cách sửa, và kết thúc bằng bài tập.
Bảo mật không phải việc riêng của backend. Trình duyệt là một môi trường chạy đa-người-thuê đầy thù địch, nơi code của bạn, script bên thứ ba, và trang do kẻ tấn công kiểm soát chạy cạnh nhau. Trước bất kỳ tấn công cụ thể nào, bạn cần mô hình nền tảng: origin và Same-Origin Policy (SOP).
Origin là gì?
Một origin là bộ ba scheme + host + port. Hai URL cùng origin chỉ khi cả ba khớp chính xác:
https://app.example.com:443/dashboard
└─┬──┘ └──────┬───────┘ └┬┘
scheme host port → origin = https://app.example.com:443
So với https://app.example.com:
https://app.example.com/users → SAME origin (port 443 implied)
http://app.example.com/users → different (scheme http vs https)
https://api.example.com/users → different (host api vs app)
https://app.example.com:8443/users → different (port 8443 vs 443)
Một cái bẫy phổ biến: app.example.com và api.example.com là hai origin khác nhau, dù cùng domain đăng ký example.com. ""Same site” và “same origin” không giống nhau — khác biệt này rất quan trọng với cookie (Phần 4).
Same-Origin Policy
Same-Origin Policy là quy tắc cô lập cốt lõi của trình duyệt: một document hay script từ origin này không thể đọc dữ liệu của origin khác trừ khi origin đó cho phép rõ ràng.
Không có SOP, bất kỳ trang nào bạn ghé đều có thể đọc tab Gmail đang mở, dashboard ngân hàng, hay công cụ admin nội bộ của bạn. SOP là thứ khiến web đa-tab sống nổi.
SOP chặn gì — và KHÔNG chặn gì
Đây là phần hay bị hiểu nhầm nhất, nên hãy chính xác.
SOP chặn đọc cross-origin:
- Đọc body phản hồi của
fetch/XHR cross-origin (trừ khi CORS cho phép — Phần 6). - Đọc DOM của
<iframe>cross-origin. - Đọc
localStorage,IndexedDB, hay cookie của origin khác bằng JS.
SOP KHÔNG chặn gửi / nhúng cross-origin:
- Trình duyệt vẫn gửi request cross-origin (kèm cookie) — chỉ là không cho JS đọc phản hồi. Đúng khe hở này là thứ CSRF lợi dụng (Phần 4).
<img>,<script>,<link>,<form>,<iframe>đều nạp được tài nguyên cross-origin. Một<script src="https://cdn.com/x.js">chạy với toàn quyền của origin bạn — gốc rễ của rủi ro supply-chain (Phần 9).
Mô hình tư duy: SOP là biên giới đọc, không phải biên giới gửi. Phần lớn tấn công frontend nằm trong khe hở giữa “trình duyệt đã gửi” và “ai được phép đọc”.
Thông tin xác thực nằm ở đâu
Trình duyệt tự đính kèm thông tin xác thực môi trường: cookie cho origin đích, HTTP auth, và chứng chỉ client TLS. ""Tự động” là từ nguy hiểm: một request do trang độc hại kích hoạt vẫn mang theo cookie của nạn nhân cho site đích.
Với fetch, bạn điều khiển bằng tùy chọn credentials:
// same-origin (default): cookies sent only to our own origin
await fetch('/api/profile');
// include: send cookies even cross-origin — needs CORS to allow credentials
await fetch('https://api.example.com/profile', {
credentials: 'include',
});
// omit: never send cookies (good for calling untrusted third parties)
await fetch('https://third-party.com/data', {
credentials: 'omit',
});
Hiểu ai gửi gì, khi nào là xương sống của CSRF (Phần 4), CORS (Phần 6), và lưu token (Phần 5).
Ba trụ cột phòng thủ frontend
Gần như mọi thứ trong series này quy về ba ý:
- Đừng tin input hay client. Người dùng sửa được JS, DOM, và mọi request. Kiểm tra phía client là UX, không phải bảo mật (Phần 10).
- Kiểm soát thứ gì chạy và thứ gì nạp. XSS (Phần 2), CSP (Phần 3), SRI (Phần 9) đều xoay quanh việc quyết định code nào được chạy.
- Phòng thủ nhiều lớp. Không header hay kiểm tra đơn lẻ nào là đủ; bạn xếp nhiều lớp độc lập để một lớp hỏng không gây thảm họa.
Tự kiểm tra nhanh
Mở một app bạn đang duy trì và trả lời:
- Nó nói chuyện với bao nhiêu origin khác nhau (CDN, API, analytics, font)? Mỗi cái là một niềm tin bạn đang trao đi.
- Bạn đặt
dangerouslySetInnerHTML/innerHTML/v-htmlở đâu? Mỗi cái là một điểm XSS tiềm tàng (Phần 2). - Token auth lưu ở đâu, và có script nào trên trang đọc được không (Phần 5)?
Nếu mấy câu đó còn mơ hồ, series này dành cho bạn.
Bài tập / Exercises
1. Với mỗi cặp, nói same-origin hay khác, và vì sao:
(a) https://shop.com/a vs https://shop.com/b
(b) https://shop.com vs http://shop.com
(c) https://shop.com vs https://www.shop.com
(d) https://shop.com vs https://shop.com:8443
Lời giải
Cùng — chỉ khác path. Khác — scheme. Khác — host. Khác — port.
2. Đúng hay sai: “SOP ngăn evil.com khiến trình duyệt POST tới bank.com.” Giải thích.
Lời giải
Sai. SOP chặn evil.com đọc phản hồi, nhưng trình duyệt vẫn gửi POST và đính kèm cookie bank.com. Khe hở “gửi” đó chính là CSRF — phòng bằng SameSite cookie + token (Phần 4).
3. Viết ba lệnh fetch: một tới origin của bạn kèm cookie, một cross-origin có credentials, một tới bên thứ ba không tin cậy không credentials.
Lời giải
await fetch('/api/me'); // same-origin, cookies by default
await fetch('https://api.example.com/me', { credentials: 'include' }); // cross-origin + cookies
await fetch('https://widget.thirdparty.com/x', { credentials: 'omit' }); // no cookies leakedNâng cao:trong DevTools, mở tab ngân hàng/email, chạy fetch('https://other-site.com').then(r => r.text()) từ console, và đọc lỗi. Giải thích biên giới nào chặn bạn và biên giới nào không.
Điểm chính
- Một origin là
scheme + host + port— cả ba phải khớp. - SOP chặn đọc cross-origin, không chặn gửi/nhúng — phần lớn tấn công nằm trong khe hở đó.
- Trình duyệt tự gửi credentials môi trường; điều khiển bằng
credentialscủafetch. - Bảo mật frontend = đừng tin client + kiểm soát thứ gì chạy + phòng thủ nhiều lớp.
Tiếp theo
Phần 2 — Cross-Site Scripting (XSS): lỗ hổng frontend số 1 — cách XSS stored, reflected, và DOM hoạt động, các sink nguy hiểm, và cách escape, sanitize, khóa chặt bằng Trusted Types.