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.
This is Part 1 of a 10-part series on the web-security essentials every frontend developer should know — and actively prevent {Đâ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}. Each part explains a real threat, shows vulnerable code, then the fix, and ends with exercises {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}.
Security is not a backend-only job {Bảo mật không phải việc riêng của backend}. The browser is a hostile, multi-tenant runtime where your code, third-party scripts, and attacker-controlled pages all run side by side {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}. Before any specific attack, you need the model that everything else builds on: the origin and the Same-Origin Policy (SOP) {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)}.
What is an origin? {Origin là gì?}
An origin is the triple scheme + host + port {Một origin là bộ ba scheme + host + port}. Two URLs share an origin only if all three match exactly {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
Compare against https://app.example.com {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)
A common trap {Một cái bẫy phổ biến}: app.example.com and api.example.com are different origins, even though they share the registrable domain example.com {app.example.com và api.example.com là hai origin khác nhau, dù cùng domain đăng ký example.com}. “Same site” and “same origin” are not the same thing — a distinction that matters a lot for cookies (Part 4) {“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)}.
The Same-Origin Policy {Same-Origin Policy}
The Same-Origin Policy is the browser’s core isolation rule: a document or script from one origin cannot read data from a different origin unless that origin explicitly allows it {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}.
Without SOP, any site you visit could read your open Gmail tab, your bank dashboard, or your internal admin tool {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 is what makes the multi-tab web survivable {SOP là thứ khiến web đa-tab sống nổi}.
What SOP blocks — and what it does NOT {SOP chặn gì — và KHÔNG chặn gì}
This is the most misunderstood part, so be precise {Đây là phần hay bị hiểu nhầm nhất, nên hãy chính xác}.
SOP blocks cross-origin reading {SOP chặn đọc cross-origin}:
- Reading the response body of a cross-origin
fetch/XHR (unless CORS allows it — Part 6) {Đọc body phản hồi củafetch/XHR cross-origin (trừ khi CORS cho phép — Phần 6)}. - Reading the DOM of a cross-origin
<iframe>{Đọc DOM của<iframe>cross-origin}. - Reading another origin’s
localStorage,IndexedDB, or cookies via JS {ĐọclocalStorage,IndexedDB, hay cookie của origin khác bằng JS}.
SOP does NOT block cross-origin sending / embedding {SOP KHÔNG chặn gửi / nhúng cross-origin}:
- Your browser will send a cross-origin request (and attach cookies) — it just won’t let JS read the reply. This gap is exactly what CSRF abuses (Part 4) {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>, and<iframe>can all load cross-origin resources. A<script src="https://cdn.com/x.js">runs with your origin’s full privileges — the basis of supply-chain risk (Part 9) {<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)}.
Mental model {Mô hình tư duy}: SOP is a read boundary, not a send boundary {SOP là biên giới đọc, không phải biên giới gửi}. Most frontend attacks live in the gap between “the browser sent it” and “who’s allowed to read it.” {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”}.
Where credentials fit in {Thông tin xác thực nằm ở đâu}
The browser attaches ambient credentials automatically: cookies for the target origin, HTTP auth, and TLS client certs {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}. “Automatically” is the dangerous word {“Tự động” là từ nguy hiểm}: a request triggered by a malicious page still carries the victim’s cookies for the target site {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}.
For fetch, you control this with the credentials option {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',
});
Understanding who sends what, when is the spine of CSRF (Part 4), CORS (Part 6), and token storage (Part 5) {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)}.
The three pillars of frontend defense {Ba trụ cột phòng thủ frontend}
Almost everything in this series maps to three ideas {Gần như mọi thứ trong series này quy về ba ý}:
- Never trust input or the client. The user can edit your JS, the DOM, and every request. Client-side checks are UX, not security (Part 10) {Đừ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)}.
- Control what executes and what loads. XSS (Part 2), CSP (Part 3), and SRI (Part 9) are all about deciding which code is allowed to run {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}.
- Defense in depth. No single header or check is enough; you stack independent layers so one failure isn’t fatal {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}.
A quick self-audit {Tự kiểm tra nhanh}
Open any app you maintain and answer {Mở một app bạn đang duy trì và trả lời}:
- How many distinct origins does it talk to (CDNs, APIs, analytics, fonts)? Each is trust you’re extending {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}.
- Where do you put
dangerouslySetInnerHTML/innerHTML/v-html? Each is a potential XSS sink (Part 2) {Bạn đặtdangerouslySetInnerHTML/innerHTML/v-htmlở đâu? Mỗi cái là một điểm XSS tiềm tàng (Phần 2)}. - Where is the auth token stored, and could any script on the page read it (Part 5)? {Token auth lưu ở đâu, và có script nào trên trang đọc được không (Phần 5)?}
If those questions feel fuzzy, this series is for you {Nếu mấy câu đó còn mơ hồ, series này dành cho bạn}.
Bài tập / Exercises
1. For each pair, say same-origin or different, and why {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
Solution {Lời giải}
(a) Same — only the path differs; scheme/host/port match {Cùng — chỉ khác path}.
(b) Different — scheme https vs http {Khác — scheme}.
(c) Different — host shop.com vs www.shop.com {Khác — host}.
(d) Different — port 443 (implied) vs 8443 {Khác — port}.
2. True or false: “SOP stops evil.com from making my browser POST to bank.com.” Explain {Đúng hay sai: “SOP ngăn evil.com khiến trình duyệt POST tới bank.com.” Giải thích}.
Solution {Lời giải}
False {Sai}. SOP blocks evil.com from reading the response, but the browser will still send the POST and attach bank.com cookies {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}. That sending gap is CSRF — defended with SameSite cookies + tokens (Part 4) {Khe hở “gửi” đó chính là CSRF — phòng bằng SameSite cookie + token (Phần 4)}.
3. Write three fetch calls: one to your own origin with cookies, one cross-origin with credentials, one to an untrusted third party without credentials {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}.
Solution {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 leakedStretch {Nâng cao}: in DevTools, open your bank/email tab, run fetch('https://other-site.com').then(r => r.text()) from the console, and read the error {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}. Explain which boundary blocked you and which one didn’t {Giải thích biên giới nào chặn bạn và biên giới nào không}.
Key takeaways {Điểm chính}
- An origin is
scheme + host + port— all three must match {Một origin làscheme + host + port— cả ba phải khớp}. - SOP blocks cross-origin reads, not sends or embeds — most frontend attacks live in that gap {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ở đó}.
- The browser sends ambient credentials automatically; control them with
fetchcredentials{Trình duyệt tự gửi credentials môi trường; điều khiển bằngcredentialscủafetch}. - Frontend security = never trust the client + control what executes + defense in depth {Bảo mật frontend = đừng tin client + kiểm soát thứ gì chạy + phòng thủ nhiều lớp}.
Next up {Tiếp theo}
Part 2 — Cross-Site Scripting (XSS): the #1 frontend vulnerability — how stored, reflected, and DOM-based XSS work, the dangerous sinks, and how to escape, sanitize, and lock down with Trusted Types {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}. Continue to Part 2 — Cross-Site Scripting (XSS).