jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

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: originSame-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.comapi.example.comhai 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}.

page https://app.com https://app.com/api same origin allowed https://evil.com different origin → read blocked blocked origin = scheme + host + port
Same-origin reads are allowed; cross-origin reads are blocked by default — origin = scheme + host + port

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ủa fetch/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 {Đọc localStorage, 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 ý}:

  1. 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)}.
  2. 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}.
  3. 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 đặt dangerouslySetInnerHTML / 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 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 leaked

Stretch {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 originscheme + 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 fetch credentials {Trình duyệt tự gửi credentials môi trường; điều khiển bằng credentials của fetch}.
  • 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).