Angular Learning
Production & Troubleshooting

Bảo mật frontend Angular

Threat model, XSS, CSP, CSRF, xác thực và vận hành bảo mật cho ứng dụng Angular trên môi trường production.

Ranh giới quan trọng

Angular giúp giảm một số rủi ro XSS trong template, nhưng frontend không phải là biên bảo mật. Mọi quyền truy cập dữ liệu, vai trò người dùng và kiểm tra nghiệp vụ vẫn phải được xác thực lại ở API.

Mục lục

Mục tiêu và nguyên tắc

Bảo mật frontend là giảm khả năng mã độc chạy trong trình duyệt, giảm dữ liệu lộ ra ngoài và làm cho request đến API có nguồn gốc rõ ràng. Đây là defense in depth: nhiều lớp phòng vệ độc lập, thay vì tin vào một lớp duy nhất.

Bốn nguyên tắc dùng xuyên suốt trang này:

  1. Mọi input đều không tin cậy. Input gồm cả nội dung người dùng nhập, HTML từ CMS, query parameter, API response, postMessage, localStorage và cấu hình runtime.
  2. Không cấp quyền ở client. Route guard, nút bị ẩn hay claim trong JWT chỉ phục vụ trải nghiệm. API phải tự kiểm tra danh tính, quyền và quyền sở hữu bản ghi.
  3. Giảm quyền mặc định. Chỉ cho phép origin, method, script source và dependency thực sự cần thiết.
  4. Ưu tiên thiết kế có thể kiểm chứng. Policy, header, lockfile và checklist phải được review, theo dõi và có đường rollback.

Cách đọc

Mỗi chủ đề đi theo cùng một nhịp: khái niệm, cơ chế, ví dụ an toàn, sau đó là lỗi và trade-off. Áp dụng các ví dụ ở backend, CDN hoặc reverse proxy tương ứng; Angular không thể tự đặt header HTTP cho tài liệu HTML ban đầu.

Threat model: frontend bảo vệ được gì?

Threat model là mô tả ngắn về thứ cần bảo vệ, ai có thể tấn công và đường tấn công. Không cần dự đoán mọi lỗi; mục tiêu là chọn đúng lớp phòng vệ trước các rủi ro có hậu quả lớn.

Tài sản, tác nhân và điểm vào

Thành phầnVí dụRủi ro chínhLớp bảo vệ chính
Phiên đăng nhậpsession cookie, refresh tokenchiếm phiên, CSRFHttpOnly, Secure, SameSite, CSRF defense, HTTPS
Dữ liệu hiển thịtên, bình luận, HTML CMSXSS, rò rỉ dữ liệuescape/sanitize, CSP, authorization ở API
Request APIPOST /api/paymentsgửi request trái phép, sửa dữ liệuauth ở API, CSRF defense, CORS tối thiểu
Bundle và pipelinepackage npm, CI artifactdependency độc hại, package bị chiếmlockfile, audit, review, pin CI action
Cấu hình publicAPI base URL, feature flaghiểu nhầm là secret, trỏ nhầm môi trườngphân loại dữ liệu, validate config, secret ở server

Tác nhân không chỉ là “hacker” bên ngoài. Một tài khoản bình thường có thể gửi nội dung độc hại để nó chạy trong trình duyệt của quản trị viên. Một extension trình duyệt hoặc dependency bị cài cắm cũng có thể đọc dữ liệu mà JavaScript của trang truy cập được.

Luồng tin cậy

Dữ liệu có thể đổi mức độ tin cậy khi đi qua nhiều hệ thống. Ví dụ, một bài viết do quản trị viên tạo vẫn nên được coi là không tin cậy khi được API trả lại: tài khoản quản trị có thể bị chiếm hoặc CMS có thể lưu HTML sai.

direction: right

n0: "Người dùng / CMS" {
  width: 152
  height: 58
}

n1: "API hoặc CDN" {
  width: 130
  height: 58
}

n2: "Angular app\nTemplate / client" {
  width: 160
  height: 58
}

n0 -> n1: "Input / HTML / URL"
n1 -> n2: "JSON / HTML"
Người dùng / CMSAPI hoặc CDNAngular appTemplate / client Input / HTML / URL JSON / HTML
Người dùng / CMSAPI hoặc CDNAngular appTemplate / client Input / HTML / URL JSON / HTML
Dữ liệu từ nguồn ngoài đến DOM browser
direction: right

browser: "Browser\nCookie + DOM" {
  width: 130
  height: 58
}

app: "Angular client" {
  width: 136
  height: 58
}

other: "Origin khác" {
  width: 130
  height: 58
}

api: "API / database" {
  width: 136
  height: 58
}

app -> browser: "DOM escape / sanitize"
browser -> app: "Request có auth"
browser -> other: "CSP, cookie policy,\nbrowser CORS"
app -> api: "Authz + validation"
BrowserCookie + DOMAngular clientOrigin khácAPI / database DOM escape / sanitize Request có auth CSP, cookie policy,browser CORS Authz + validation
BrowserCookie + DOMAngular clientOrigin khácAPI / database DOM escape / sanitize Request có auth CSP, cookie policy,browser CORS Authz + validation
Chính sách browser và xác thực ở API

Biểu đồ cho thấy browser thực thi một phần chính sách, nhưng API mới là nơi quyết định request có được phép hay không. CORS không thay thế authorization; nó chỉ là quy tắc browser áp dụng trước khi JavaScript đọc response cross-origin.

Ưu tiên xử lý

Bắt đầu từ các tình huống có khả năng xảy ra và tác động lớn:

  • Nội dung HTML từ CMS hoặc rich-text đi vào [innerHTML].
  • Ứng dụng dùng cookie session và có endpoint thay đổi dữ liệu.
  • Token sống lâu được đặt trong localStorage.
  • CSP đang có unsafe-inline, unsafe-eval hoặc * mà không có lý do rõ ràng.
  • CORS phản chiếu mọi Origin, đồng thời bật credentials.
  • NPM dependency hoặc CI action chưa được pin/version review.

Không tin route guard

CanActivate có thể bị bỏ qua bằng cách gọi API trực tiếp. API phải trả 401 khi chưa xác thực và 403 khi đã xác thực nhưng không có quyền, kể cả khi frontend chưa hề render route đó.

XSS và sanitization

Cross-site scripting (XSS) là việc dữ liệu không tin cậy trở thành JavaScript hoặc markup có khả năng thực thi trong origin của ứng dụng. Nếu XSS thành công, mã độc có thể thao tác DOM, gửi request bằng cookie hiện có, đọc token mà JavaScript truy cập được và thay đổi nội dung hiển thị.

Cơ chế an toàn mặc định của Angular

Interpolation và phần lớn property binding của Angular không chèn input như HTML thực thi được. Ví dụ {{ comment.body }} được escape; chuỗi <img ...> hiện thành chữ thay vì trở thành element. Khi binding vào các ngữ cảnh nhạy cảm như HTML, URL, style và resource URL, Angular áp dụng quy tắc sanitization riêng.

@Component({
  selector: 'app-comment',
  template: `
    <p>{{ comment.author }}</p>
    <p>{{ comment.body }}</p>
  `,
})
export class CommentComponent {
  comment = {
    author: 'Mai',
    body: '<img src=x onerror="alert(document.domain)">',
  };
}

Đoạn trên an toàn hơn ghép chuỗi HTML vì body đi qua interpolation. Kết quả người dùng nhìn thấy là ký tự < và > chứ không phải thẻ img.

Không biến dữ liệu thành code bằng các API DOM trực tiếp. Tránh ElementRef.nativeElement.innerHTML = ..., document.write, eval, new Function, và các thư viện nhận chuỗi HTML rồi tự gắn vào DOM. Những API này có thể bỏ qua lớp bảo vệ của Angular.

Ví dụ render dữ liệu không tin cậy

Khi chỉ cần hiển thị text, dùng interpolation là lựa chọn mặc định:

<!-- An toàn: Angular render dữ liệu như text -->
<h2>{{ article.title }}</h2>
<p>{{ article.summary }}</p>
<a [routerLink]="['/articles', article.id]">Đọc bài</a>

Khi sản phẩm thật sự cần rich-text, hãy sanitize ở hai nơi:

  1. Ở server trước khi lưu hoặc trước khi phát trả, với allowlist tag, attribute và URL scheme phù hợp nghiệp vụ.
  2. Ở client trước khi render như một lớp dự phòng, đồng thời không dùng giá trị “trusted” để vượt sanitizer.
@Component({
  selector: 'app-article-body',
  template: `<section [innerHTML]="articleHtml"></section>`,
})
export class ArticleBodyComponent {
  // API chỉ trả HTML đã được server sanitize theo allowlist.
  articleHtml = '<p>Nội dung <strong>đã kiểm duyệt</strong>.</p>';
}

Angular sẽ sanitize giá trị gắn vào [innerHTML]. Tuy vậy, sanitizer client không phải bộ lọc nghiệp vụ đầy đủ và có thể bỏ những markup mà editor mong muốn giữ lại. Allowlist ở server là nơi xác định chính sách nội dung lâu dài, nhất quán cho mọi client.

Khi nào mới dùng DomSanitizer

DomSanitizer.bypassSecurityTrustHtml() không sanitize. Nó gắn nhãn “tôi đã tin giá trị này” để Angular ngừng bảo vệ ngữ cảnh đó. Chỉ dùng khi giá trị đến từ nguồn được kiểm soát hoàn toàn, đã được sanitize bằng chính sách có test, và ranh giới đó được ghi chú ngay tại code.

Ví dụ dưới đây vẫn giữ trách nhiệm kiểm tra ở một service riêng. Không nhận string trực tiếp từ component rồi bypass.

@Injectable({ providedIn: 'root' })
export class TrustedContentService {
  constructor(private readonly sanitizer: DomSanitizer) {}

  fromReviewedTemplate(template: string): SafeHtml {
    // `template` phải lấy từ artifact nội bộ đã review, không phải API/user input.
    return this.sanitizer.bypassSecurityTrustHtml(template);
  }
}

Đặt tên để lộ rủi ro

Nếu bắt buộc bypass, đặt tên như fromReviewedTemplate thay vì trustHtml. Tên hàm phải nói rõ điều kiện tin cậy và review cần thiết. Lý tưởng nhất là không cần bypass.

URL cũng cần allowlist. Với link do người dùng cung cấp, chỉ chấp nhận scheme cần thiết như https: hoặc mailto: sau khi parse bằng URL; không tự nối chuỗi rồi gán vào href.

function safeExternalUrl(value: string): string | null {
  try {
    const url = new URL(value);
    return ['https:', 'mailto:'].includes(url.protocol) ? url.toString() : null;
  } catch {
    return null;
  }
}

Nếu safeExternalUrl() trả null, hiển thị text hoặc bỏ link. Không thay null bằng URL gốc “cho tiện”.

Lỗi phổ biến và trade-off

LỗiVì sao nguy hiểmCách thay thếTrade-off
Gọi bypassSecurityTrustHtml cho response APIbiến input thành HTML tin cậyserver sanitize + [innerHTML] bình thườngcần duy trì allowlist
Dùng innerHTML để format textmở đường parser HTMLinterpolation, CSS, component riêngít tự do markup hơn
Dùng eval() để parse “template”thực thi code tùy ýJSON schema hoặc component map tĩnhphải thiết kế schema
Tin URL bất kỳcó thể dẫn tới scheme nguy hiểm hoặc phishingparse URL, allowlist protocol/hostcần quy định link hợp lệ
Chỉ sanitize ở clientclient khác hoặc API consumer vẫn nhận dữ liệu bẩnsanitize server là nguồn chuẩnbackend phức tạp hơn

Nói ngắn gọn: coi HTML là một định dạng đặc quyền. Nếu không có yêu cầu rich-text rõ ràng, không render HTML từ dữ liệu động.

Content Security Policy (CSP)

CSP là HTTP response header nói với browser rằng script, style, ảnh, kết nối mạng, frame và nhiều loại tài nguyên khác được phép tải từ đâu. CSP không sửa lỗi XSS, nhưng làm giảm hậu quả khi một đường chèn script bị sót.

CSP chặn điều gì

Một policy tốt có thể ngăn script inline không có nonce/hash, ngăn ứng dụng kết nối đến API lạ và ngăn trang bị nhúng vào iframe của origin khác. CSP chỉ có hiệu lực khi browser nhận được nó từ response document; đặt CSP qua JavaScript sau khi app chạy là quá muộn.

Các directive thường cần xem xét:

DirectiveVai tròHướng mặc định
default-srcfallback cho loại tài nguyên chưa khai báo'self'
script-srcnguồn JavaScript'self' + nonce/hash cần thiết
style-srcnguồn stylesheet'self'; nonce/hash nếu có inline style
connect-srcfetch, XHR, WebSocket'self' và API/telemetry cụ thể
img-srcảnh'self', data: nếu thật sự cần, CDN cụ thể
font-srcfont'self' và font CDN cụ thể
frame-ancestorsai được nhúng trang trong iframe'none' hoặc allowlist
base-uringuồn cho thẻ <base>'self'
object-srcplugin cũ như Flash'none'
form-actionnơi form có thể submit'self' hoặc allowlist

Thiết kế policy cho Angular

Hãy bắt đầu policy hẹp và thêm từng nguồn có lý do. Ví dụ khi SPA và API cùng origin, có thể bắt đầu như sau:

Content-Security-Policy:
  default-src 'self';
  base-uri 'self';
  object-src 'none';
  frame-ancestors 'none';
  form-action 'self';
  img-src 'self' data:;
  font-src 'self';
  style-src 'self';
  script-src 'self';
  connect-src 'self';
  upgrade-insecure-requests

Nếu app dùng API https://api.example.com và Sentry, khai báo tường minh thay vì mở *:

connect-src 'self' https://api.example.com https://o123.ingest.sentry.io;

Không thêm 'unsafe-inline' vào script-src để “sửa nhanh” lỗi CSP. Giá trị này làm inline script dễ chạy hơn và giảm ý nghĩa của CSP trước XSS. Khi cần script inline do server tạo, dùng nonce ngẫu nhiên theo từng response hoặc hash nội dung cố định. Nonce phải khó đoán, không tái sử dụng và được đưa đồng thời vào header lẫn thẻ script hợp lệ.

Angular có cơ chế nonce cho style động. Nếu ứng dụng dùng CSP nonce, truyền cùng nonce từ document vào cấu hình Angular theo cách phù hợp phiên bản và kiến trúc đang dùng, rồi xác nhận các style runtime không bị browser chặn. Đừng tắt CSP chỉ vì style injection bị lỗi; hãy tìm đúng nguồn inline cần nonce.

CSP và development

Dev server, HMR, source map và extension có thể cần localhost, WebSocket hoặc unsafe-eval. Policy development không nên được sao chép nguyên trạng sang production. Kiểm tra header trên response HTML production thực tế.

Đưa CSP vào production

CSP nên đặt ở nơi phát index.html: reverse proxy, CDN, backend hoặc hosting platform. Với ứng dụng có SSR, server render response là nơi phù hợp để tạo nonce theo request. Với static hosting, dùng policy không cần nonce nếu bundle và style không đòi inline script; nếu không, chọn hosting có hỗ trợ header/edge logic phù hợp.

<meta http-equiv="Content-Security-Policy"> chỉ là phương án dự phòng hạn chế. Nó không hỗ trợ toàn bộ directive như HTTP header và được browser đọc sau khi bắt đầu parse tài liệu. Ưu tiên response header.

Báo cáo và rollout

Rollout CSP có thể làm vỡ analytics, CDN font hoặc widget bên thứ ba. Triển khai theo thứ tự:

  1. Lập danh sách tất cả origin tải script, connect, font, ảnh và frame.
  2. Đặt Content-Security-Policy-Report-Only trong staging hoặc một tỷ lệ traffic nhỏ.
  3. Thu report, đối chiếu với DevTools Console và loại bỏ nguồn không cần thiết.
  4. Chuyển sang Content-Security-Policy enforce.
  5. Theo dõi vi phạm sau deploy; mọi origin mới phải qua review.

Endpoint nhận CSP report không được tin dữ liệu report như dữ liệu nghiệp vụ. Giới hạn tốc độ, lọc log và không đưa payload thô vào dashboard có khả năng render HTML.

CSRF, CORS và request đến API

Phân biệt CSRF và CORS

CSRF (cross-site request forgery) xảy ra khi browser tự đính kèm credential dựa trên cookie vào request do trang độc hại khởi tạo. Nếu API chỉ dựa vào cookie và không có lớp xác minh bổ sung, hành động thay đổi dữ liệu có thể bị thực hiện thay người dùng.

CORS (cross-origin resource sharing) là cơ chế browser yêu cầu server cho phép JavaScript từ origin khác đọc response. CORS không ngăn request đơn giản được gửi, không bảo vệ non-browser client và không thay cho kiểm tra authentication/authorization.

Ví dụ: https://evil.example có thể thử gửi một form POST tới https://app.example/api/email. CORS có thể khiến JavaScript ở evil.example không đọc được response, nhưng không tự đảm bảo request không thay đổi email. CSRF defense là lớp giải quyết việc đó.

Khi xác thực bằng cookie, đặt cookie session ở server với các thuộc tính phù hợp:

Set-Cookie: __Host-session=<opaque-session-id>; Path=/; Secure; HttpOnly; SameSite=Lax
  • Secure chỉ gửi cookie qua HTTPS.
  • HttpOnly ngăn JavaScript đọc cookie, nên giảm hậu quả của XSS đối với việc đánh cắp cookie.
  • SameSite=Lax chặn nhiều request cross-site tự động nhưng không phải lý do để bỏ CSRF protection.
  • Prefix __Host- yêu cầu Secure, Path=/ và không có Domain; nó giảm rủi ro cookie bị subdomain ghi đè.

Với endpoint thay đổi trạng thái (POST, PUT, PATCH, DELETE), kết hợp cookie với CSRF token. Một mẫu phổ biến là signed double-submit: server phát token không nhạy cảm trong cookie có thể đọc được, Angular gửi token đó ở custom header, và server xác minh chữ ký/liên kết token với session. Trang cross-site có thể làm browser gửi cookie, nhưng không thể tùy ý đọc cookie để đặt custom header do same-origin policy.

Angular HttpClient hỗ trợ XSRF cho request cùng origin. Cấu hình tên cookie/header cần khớp hoàn toàn với backend:

import { provideHttpClient, withXsrfConfiguration } from '@angular/common/http';

export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(
      withXsrfConfiguration({
        cookieName: 'XSRF-TOKEN',
        headerName: 'X-XSRF-TOKEN',
      }),
    ),
  ],
};

Không gửi XSRF token sang origin API khác chỉ vì dùng chung backend. Nếu frontend và API khác origin, hãy thiết kế rõ mô hình credentials, CORS và CSRF với backend. Cách đơn giản, ít sai nhất là phục vụ API qua cùng site/origin bằng reverse proxy khi phù hợp kiến trúc.

CSRF token không thay thế XSS defense

JavaScript chạy được trong origin hợp lệ thường có thể gửi token CSRF. Vì vậy XSS và CSRF là hai lớp khác nhau: vẫn cần escape/sanitize, CSP và CSRF defense.

CORS phải được cấu hình ở server

Chỉ server/API mới có thể gửi Access-Control-Allow-*. Code Angular không thể “bật CORS”; withCredentials: true chỉ yêu cầu browser gửi credential, không cấp phép nó.

Cấu hình an toàn cho frontend https://app.example.com gọi https://api.example.com cần cụ thể như sau:

Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE
Access-Control-Allow-Headers: Content-Type, X-XSRF-TOKEN
Vary: Origin

Không dùng Access-Control-Allow-Origin: * cùng Access-Control-Allow-Credentials: true; browser sẽ từ chối tổ hợp này. Cũng không phản chiếu Origin vô điều kiện. Server phải so origin với allowlist và chỉ trả header khi khớp.

Preflight OPTIONS không thay thế authorization. API vẫn phải xác minh cookie/token, CSRF token (nếu dùng) và quyền cho request thật.

Token, session và xác thực

Chọn mô hình

Không có một nơi lưu token phù hợp cho mọi ứng dụng. Chọn dựa trên mức độ nhạy cảm, loại client, domain deployment và khả năng chống XSS/CSRF của hệ thống.

Mô hìnhƯu điểmRủi ro/cái giáPhù hợp khi
Opaque session trong HttpOnly cookieJS không đọc được session ID; revoke dễ ở servercần CSRF defense và vận hành session storeweb app first-party
Access token ngắn hạn trong bộ nhớmất khi reload, không bền trong XSS sau reloadrefresh flow phức tạp; tab mới cần khôi phụcSPA có OAuth/OIDC
BFF (backend for frontend) + cookietoken OAuth không vào browser JS; policy tập trungthêm service/hạ tầngứng dụng nhạy cảm hoặc nhiều upstream API
Token trong localStoragedễ triển khai và bền qua reloadXSS đọc/đánh cắp đượcchỉ cân nhắc khi đã chấp nhận rủi ro rõ ràng

Đối với web frontend có dữ liệu nhạy cảm, session HttpOnly cookie hoặc BFF thường tạo ranh giới tốt hơn việc giữ refresh token trong localStorage. HttpOnly không chữa XSS: mã độc vẫn có thể thực hiện hành động bằng phiên hiện tại. Nó chỉ làm khó việc mang credential đi nơi khác.

Access token cần ngắn hạn. Refresh token cần rotation, phát hiện reuse và cơ chế revoke ở authorization server. Không log token, authorization header, cookie hoặc payload chứa token vào console, telemetry hay error tracker.

Lưu token an toàn theo ngữ cảnh

Nếu buộc phải dùng bearer token phía browser, giữ access token trong service bộ nhớ thay vì persistent storage, và bảo đảm các request đến đúng origin. Không gắn Authorization vào URL: URL có thể xuất hiện trong history, log proxy, Referer hoặc analytics.

@Injectable({ providedIn: 'root' })
export class AccessTokenStore {
  private token: string | null = null;

  set(token: string): void {
    this.token = token;
  }

  get(): string | null {
    return this.token;
  }

  clear(): void {
    this.token = null;
  }
}

Service này không tự làm token an toàn. Nó chỉ khiến token biến mất khi tab đóng/reload, từ đó giảm persistence. Vẫn cần bảo vệ XSS và có flow đăng nhập/refresh được thiết kế ở identity provider hoặc BFF.

Interceptor và vòng đời đăng nhập

Interceptor là vị trí nhất quán để đính kèm bearer token cho API allowlist, không phải cho mọi request. Ví dụ sau chỉ thêm header khi URL có cùng origin API đã cấu hình:

export const authInterceptor: HttpInterceptorFn = (request, next) => {
  const tokenStore = inject(AccessTokenStore);
  const apiOrigin = 'https://api.example.com';
  const target = new URL(request.url, apiOrigin);
  const token = tokenStore.get();

  if (token && target.origin === apiOrigin) {
    request = request.clone({
      setHeaders: { Authorization: `Bearer ${token}` },
    });
  }

  return next(request);
};

apiOrigin là base URL cấu hình thay vì window.location, nên việc kiểm tra origin không chạm browser global khi Angular SSR. Không tự động retry 401 vô hạn. Một refresh flow cần single-flight (nhiều request cùng chờ một lần refresh), giới hạn retry và logout/xóa state khi refresh thất bại. Phân biệt 401 với 403: 401 thường là thiếu/hết hạn xác thực; 403 là đã xác thực nhưng không được phép. Không cố refresh token chỉ vì nhận 403.

Authorization luôn nằm ở API

Frontend có thể đọc role để ẩn nút “Xóa”, nhưng endpoint xóa vẫn phải kiểm tra quyền và quyền sở hữu tài nguyên. UI chỉ là hướng dẫn, không phải cổng bảo vệ.

Dependency, supply chain và secret

Kiểm soát dependency

Mỗi package chạy trong build hoặc bundle đều là một phần của bề mặt tấn công. Một dependency transitive có thể có lỗ hổng hoặc bị chiếm quyền phát hành.

Thực hành tối thiểu:

  • Commit lockfile và dùng cài đặt xác định, ví dụ npm ci trong CI thay vì npm install tùy ý.
  • Review package mới: maintainer, mức độ sử dụng, license, quyền postinstall và lý do cần package.
  • Cập nhật dependency theo đợt nhỏ, có test; ưu tiên bản vá lỗ hổng có mức độ ảnh hưởng thực tế.
  • Chạy npm audit như tín hiệu, sau đó đánh giá đường thực thi và bản vá. Không dùng npm audit fix --force mù quáng vì có thể nâng breaking change.
  • Pin phiên bản GitHub Action/CI action theo commit SHA khi chính sách tổ chức yêu cầu supply-chain hardening.
  • Bảo vệ npm organization bằng MFA và token publish có scope, thời hạn ngắn nếu đội ngũ phát hành package.

npm audit không chứng minh ứng dụng an toàn. Nó dựa trên advisory đã biết và có thể báo false positive theo đường code không dùng. Ngược lại, bỏ qua mọi báo cáo vì “app không gọi package đó” cũng nguy hiểm. Ghi nhận quyết định, owner và thời hạn xem lại cho từng ngoại lệ.

Không đặt secret trong bundle

Mọi giá trị được Angular build vào JavaScript đều có thể bị người dùng tải và đọc. Các biến môi trường frontend chỉ là cấu hình public, dù có tên API_KEY, SECRET hay nằm trong file environment.

// Có thể public: endpoint, ID public, cờ tính năng không nhạy cảm.
export const environment = {
  production: true,
  apiBaseUrl: 'https://api.example.com',
  analyticsSiteId: 'public-site-id',
};

Không đặt private API key, database password, signing key, OAuth client secret hoặc credential service-to-service vào environment.ts, JSON runtime config hay source map. Đưa secret vào server, secret manager hoặc CI secret store; client gọi backend/BFF để thực hiện thao tác đặc quyền.

Source map giúp debug nhưng có thể lộ cấu trúc và source code. Chọn policy theo sản phẩm: không public source map, upload source map riêng vào error tracker với access control, hoặc chấp nhận public khi code không nhạy cảm. Dù chọn cách nào, source map không bao giờ được chứa secret.

Security headers và triển khai

Bộ header tối thiểu

Headers được gửi từ CDN, reverse proxy hoặc backend phát index.html và API. Dưới đây là baseline để thảo luận, không phải chuỗi copy-paste bất biến.

HeaderGiá trị/ý nghĩa gợi ýLưu ý
Content-Security-Policypolicy hẹp theo phần CSProllout Report-Only trước
Strict-Transport-Securitymax-age=31536000; includeSubDomainschỉ bật sau khi mọi subdomain hỗ trợ HTTPS; cân nhắc preload riêng
X-Content-Type-Optionsnosnifftránh MIME sniffing
Referrer-Policystrict-origin-when-cross-origingiảm URL/path bị gửi sang origin khác
Permissions-Policyví dụ camera=(), microphone=(), geolocation=()chỉ tắt capability app không dùng
Cross-Origin-Opener-Policysame-origin khi tương thíchcó thể ảnh hưởng popup OAuth
Cross-Origin-Resource-Policysame-site hoặc phù hợp assetkiểm tra CDN và resource cross-origin

X-Frame-Options: DENY vẫn hữu ích cho browser cũ, nhưng frame-ancestors 'none' trong CSP là cơ chế linh hoạt hơn. Nếu ứng dụng cần chạy trong iframe của portal đối tác, allowlist chính xác portal đó; không dùng * hoặc bỏ toàn bộ bảo vệ clickjacking chỉ để tích hợp nhanh.

HSTS buộc browser dùng HTTPS cho domain đã thấy header. Bật HSTS khi HTTP redirect và certificate coverage đã ổn định. Nếu có subdomain cũ không HTTPS, includeSubDomains có thể làm chúng không truy cập được.

Ví dụ cấu hình reverse proxy

Ví dụ Nginx dưới đây minh họa nơi đặt header. Thay origin, policy, asset CDN và API theo môi trường thật; không triển khai nguyên văn nếu chưa kiểm thử asset, OAuth popup và third-party widget.

location / {
  try_files $uri $uri/ /index.html;

  add_header Content-Security-Policy "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'none'; form-action 'self'; img-src 'self' data:; font-src 'self'; style-src 'self'; script-src 'self'; connect-src 'self' https://api.example.com" always;
  add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
  add_header X-Content-Type-Options "nosniff" always;
  add_header Referrer-Policy "strict-origin-when-cross-origin" always;
  add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
}

Dùng always để header vẫn xuất hiện ở response lỗi, nơi browser cũng cần policy. Xác minh bằng Network tab hoặc curl -I https://app.example.com/; kiểm tra response HTML, route deep-link và trang lỗi, không chỉ file JavaScript.

Quy trình xử lý phát hiện bảo mật

Khi phát hiện XSS, token lộ hoặc CSP bị bypass, ưu tiên giảm tác động trước khi tối ưu nguyên nhân gốc:

  1. Khoanh vùng: xác định URL, phiên bản deploy, origin bị ảnh hưởng và dữ liệu/credential có thể đã lộ. Không dán token thật vào ticket hoặc chat.
  2. Chặn: gỡ nội dung độc hại, disable feature, thu hẹp CSP/CORS, rotate credential hoặc revoke session/token khi cần.
  3. Sửa gốc: loại bỏ sink nguy hiểm, thêm server-side validation/sanitization, sửa header hoặc cập nhật dependency.
  4. Xác minh: tái tạo bằng payload vô hại trong môi trường an toàn; kiểm tra response header, log và test regression.
  5. Rút kinh nghiệm: thêm test, lint rule, review checklist hoặc monitoring để lỗi không lặp lại.

Sự cố token/session

Nếu nghi ngờ refresh token, signing key hoặc session store bị lộ, hãy làm theo playbook incident của tổ chức ngay. “Đổi token ở client” không đủ; cần revoke/rotate tại identity provider hoặc server phát hành.

Checklist trước khi phát hành

Dùng checklist này cho release có thay đổi frontend, auth, hosting hoặc dependency:

Ranh giới và quyền

  • API kiểm tra authentication, authorization và quyền sở hữu resource cho mọi endpoint nhạy cảm.
  • Route guard và UI role chỉ được xem là cải thiện UX, không phải security control.
  • Các origin API, CDN, analytics và iframe đã được liệt kê, có owner và lý do sử dụng.

XSS và dữ liệu không tin cậy

  • Dữ liệu động được render bằng interpolation/property binding; không dùng DOM API trực tiếp.
  • Mọi [innerHTML] có nguồn dữ liệu, policy sanitize và test rõ ràng.
  • Không có bypassSecurityTrust* với input từ API, URL, CMS hoặc người dùng.
  • URL động được parse và allowlist protocol/host khi cần.
  • Không có eval, new Function, document.write hay template runtime không được review.

CSP và headers

  • CSP được trả bằng HTTP header cho index.html, không chỉ meta tag.
  • script-src, connect-src, frame-ancestors và object-src hẹp theo nhu cầu.
  • Không thêm unsafe-inline/unsafe-eval nếu chưa có ngoại lệ được review và kế hoạch loại bỏ.
  • CSP đã chạy Report-Only hoặc được kiểm tra trên luồng production quan trọng.
  • HSTS, nosniff, Referrer-Policy và Permissions-Policy đã được đánh giá cho domain.

CSRF, CORS và auth

  • Cookie session có Secure, HttpOnly, SameSite phù hợp và HTTPS ở mọi môi trường production.
  • Endpoint thay đổi dữ liệu có CSRF defense khi dùng cookie credential.
  • Tên XSRF cookie/header trong Angular khớp chính xác với server.
  • CORS allowlist origin cụ thể; không phản chiếu Origin vô điều kiện.
  • Access-Control-Allow-Credentials không được kết hợp với wildcard origin.
  • Bearer token chỉ được gửi đến API origin allowlist và không xuất hiện trong URL/log/telemetry.
  • Luồng refresh có giới hạn retry, logout khi thất bại và không retry 403 như 401.

Dependency, artifact và secret

  • Lockfile được commit; CI dùng cài đặt xác định.
  • Dependency mới, advisory mức cao và script cài đặt đã được review.
  • Không có private key, password, client secret hoặc token publish trong bundle, source map hay runtime config public.
  • Source map có policy phát hành và access control phù hợp.
  • Response header thực tế đã được kiểm tra ở CDN/proxy production, gồm route deep-link và response lỗi.

Tóm tắt

Angular tạo mặc định tốt cho template, nhưng bảo mật production cần nhiều lớp phối hợp. Render text thay vì HTML, chỉ trust nội dung có ranh giới kiểm duyệt rõ ràng và dùng CSP để giảm hậu quả của XSS. Với cookie, triển khai CSRF defense; với CORS, cấu hình allowlist ở server chứ không ở Angular.

Session hoặc BFF thường giảm việc token nhạy cảm nằm trong JavaScript, nhưng không thay thế chống XSS và authorization tại API. Cuối cùng, giữ dependency có kiểm soát, không đưa secret vào bundle, và xác minh headers trên response thực tế trước mỗi phát hành.

On this page