Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Angular frontend security best practices for building secure, production-grade applications. Use when writing or reviewing security-sensitive Angular code, configuring authentication, handling user input, setting up CSP, securing Angular SSR, or hardening an Angular application. Covers OWASP 2025, XSS prevention, DomSanitizer, template injection, CSRF/XSRF, HttpClient security, Angular SSR vulnerabilities (CVE-2025-59052), XSRF token leakage (CVE-2025-66035), stored XSS via SVG (CVE-2025-66412),
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-07 | ✗→✓ | ▲ Improved | 222% | 0% |
| case-01 | ✗→✓ | ▲ Improved | 111% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 268% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 285% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 149% | 0% |
A comprehensive guide for building secure, production-grade Angular applications. Covers Angular 19+, @angular/ssr, and aligns with OWASP Top 10:2025.
typescript// Store tokens in memory only — never localStorage @Injectable({ providedIn: "root" }) export class AuthService { private accessToken: string | null = null; setToken(token: string) { this.accessToken = token; // Memory only } getToken(): string | null { return this.accessToken; } clearToken() { this.accessToken = null; } } // Functional interceptor for token injection (Angular 17+) export const authInterceptor: HttpInterceptorFn = (req, next) => { const authService = inject(AuthService); const token = authService.getToken(); if (token) { req = req.clone({ setHeaders: { Authorization: `Bearer ${token}` }, }); } return next(req).pipe( catchError((error: HttpErrorResponse) => { if (error.status === 401) { return inject(AuthService) .refresh() .pipe(switchMap(() => next(req))); } return throwError(() => error); }), ); }; // Register in app config provideHttpClient(withInterceptors([authInterceptor]));
HttpInterceptor (deprecated — use functional interceptors)Angular automatically sanitizes values in templates across five security contexts: HTML, Style, URL, Resource URL, and Script. This is the primary XSS defense.
ngJitMode is false in server builds.If {{ 7*7 }} entered as user input renders as 49, Client-Side Template Injection (CSTI) is present. AOT prevents this; JIT does not.
Each bypass creates a potential XSS vector. Audit every usage:
typescript// ❌ DANGEROUS — bypasses all sanitization this.sanitizer.bypassSecurityTrustHtml(userInput); // ✅ SAFE — sanitize first, then bypass const sanitized = DOMPurify.sanitize(userInput); this.sanitizer.bypassSecurityTrustHtml(sanitized); // ✅ BEST — use Angular's auto-sanitization { { userInput; } } // Angular auto-escapes in templates
When Trusted Types are enforced, bypass calls use the angular#unsafe-bypass policy, making them auditable.
Angular's template compiler had an incomplete security schema for SVG animation attributes (animate, set, animateMotion, animateTransform) and MathML attributes. Malicious javascript: URLs in these attributes bypassed sanitization.
Fix: Update to Angular 19.2.17+.
bypassSecurityTrust* calls — each is a potential XSS vector[innerHTML]="userContent" (auto-sanitized) over bypass methodsAngular HttpClient automatically reads XSRF-TOKEN cookie and sets X-XSRF-TOKEN header on mutating requests (POST/PUT/DELETE).
typescriptprovideHttpClient( withXsrfConfiguration({ cookieName: "CUSTOM_XSRF_TOKEN", headerName: "X-Custom-Xsrf-Header", }), );
Protocol-relative URLs (//evil.com/api) were incorrectly treated as same-origin, causing Angular to attach XSRF tokens to attacker-controlled domains.
Fix: Update to Angular 19.2.16+.
withFetch() switches from XMLHttpRequest to the Fetch API:
credentials: "include" vs XHR's withCredentials: trueClass-based interceptors (HttpInterceptor interface) still work but functional interceptors via withInterceptors() are the modern pattern:
Angular auto-strips the )]}',\n prefix from JSON responses (XSSI prevention).
typescript// Configure secure HTTP client provideHttpClient( withInterceptors([authInterceptor, errorInterceptor]), withXsrfConfiguration({ cookieName: "XSRF-TOKEN", headerName: "X-XSRF-TOKEN", }), withFetch(), // For SSR );
typescriptexport const authGuard: CanActivateFn = (route, state) => { const authService = inject(AuthService); const router = inject(Router); if (!authService.isAuthenticated()) { return router.createUrlTree(["/login"], { queryParams: { returnUrl: state.url }, }); } const requiredRole = route.data["role"]; if (requiredRole && !authService.hasRole(requiredRole)) { return router.createUrlTree(["/unauthorized"]); } return true; }; // Route configuration { path: "admin", canActivate: [authGuard], data: { role: "admin" }, loadComponent: () => import("./admin.component"), }
CanLoad is deprecated — use CanMatch insteadAngular's SSR platform reused a shared global injector across concurrent requests. Under load, User A's auth tokens, session data, or query results could leak to User B.
Fix: Update to Angular 19.2.17+. bootstrapApplication now requires per-request BootstrapContext.
Angular's URL reconstruction trusted Host, X-Forwarded-Host, X-Forwarded-Port, X-Forwarded-Proto, X-Forwarded-Prefix headers without validation. Attackers could steer internal requests and steal Authorization headers.
Fix: Configure allowedHosts in angular.json:
json{ "security": { "allowedHosts": ["example.com", "*.example.com"] } }
Or via NG_ALLOWED_HOSTS environment variable.
Enabled phishing and SEO hijacking. Patched in same advisory.
allowedHosts to prevent SSRFngJitMode: false in server buildsreq.headers for URL constructionAngular 19 introduced automatic hash-based Strict CSP generation. Enable in angular.json:
json{ "projects": { "my-app": { "architect": { "build": { "options": { "security": { "autoCsp": true } } } } } } }
This generates CSP hashes for inline scripts in index.html, preventing unauthorized script execution.
Zone.js monkey-patches async APIs, which can conflict with strict CSP policies banning unsafe-eval.
Solution: Go Zoneless
typescript// Angular 19 (experimental) provideExperimentalZonelessChangeDetection(); // Angular 20+ (stable) provideZonelessChangeDetection();
Benefits:
unsafe-eval CSP conflictRemove zone.js from polyfills in angular.json (both build and test targets).
Configure these policies in your CSP header:
angular — core framework codeangular#bundler — lazy-loaded chunk filesangular#unsafe-bypass — DomSanitizer bypass calls (audit these!)angular#unsafe-jit — JIT compiler (should NOT appear in production)default-src 'self';
style-src 'self' 'nonce-{random}';
script-src 'self' 'nonce-{random}';Security headers should be configured at the server/CDN level:
Content-Security-Policy: [see above]
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: geolocation=(), microphone=(), camera=()
Strict-Transport-Security: max-age=31536000; includeSubDomains; preloadtypescriptimport { Validators, FormBuilder } from "@angular/forms"; import DOMPurify from "dompurify"; // Reactive form with validation this.form = this.fb.group({ email: ["", [Validators.required, Validators.email]], age: ["", [Validators.required, Validators.min(18), Validators.max(120)]], username: [ "", [ Validators.required, Validators.minLength(3), Validators.maxLength(20), Validators.pattern(/^[a-zA-Z0-9_]+$/), ], ], }); // Sanitize HTML (pipe) @Pipe({ name: "safeHtml", standalone: true }) export class SafeHtmlPipe implements PipeTransform { private sanitizer = inject(DomSanitizer); transform(html: string): SafeHtml { const sanitized = DOMPurify.sanitize(html, { ALLOWED_TAGS: ["b", "i", "em", "strong", "a"], ALLOWED_ATTR: ["href"], }); return this.sanitizer.bypassSecurityTrustHtml(sanitized); } } // Usage in template <div [innerHTML]="userContent | safeHtml"></div>
Angular Signals (stable in v19) do not introduce new security vulnerabilities:
{{ mySignal() }} is sanitized like {{ myProperty }}.signal() holds sensitive data (tokens, PII), it's accessible to any code with a reference. Follow principle of least privilege.computed() signals: Lazily evaluated — no stale security state leaking.effect() timing: Runs after change detection. Be cautious about effects that write sensitive data to localStorage.linkedSignal(): Derived writable signals — ensure they don't expose source data to broader scopes.typescript// MUST disable in production provideStoreDevtools({ maxAge: 25, logOnly: environment.production, // Exclude sensitive properties even in dev actionSanitizer: (action) => ({ ...action, payload: action.payload?.password ? { ...action.payload, password: "***" } : action.payload, }), });
withDevtools() has the same exposure concerns — apply production exclusion patterns.
ngrx-store-localstorage persist to localStorage — never store tokens or PII| CVE | Package | Impact | Fix | | -------------- | ------------------- | --------------------------- | -------- | | CVE-2025-66412 | @angular/compiler | Stored XSS via SVG/MathML | 19.2.17+ | | CVE-2025-66035 | @angular/common | XSRF token leakage | 19.2.16+ | | CVE-2025-59052 | @angular/ssr | SSR cross-request data leak | 19.2.17+ |
CVE-2024-23334 affected esbuild <= 0.24.2 (dev server exposed files). Angular CLI uses esbuild internally. Force upgrade:
json// package.json { "overrides": { "esbuild": ">=0.25.0" } }
npm audit after updatesnpm audit / pnpm audit in CItypescript// environment.prod.ts — compiled into the bundle! export const environment = { production: true, apiUrl: "https://api.example.com", // ❌ NEVER put secrets here — they are bundled into client code }; // ✅ Use runtime config fetched from server for sensitive values
json// angular.json — production configuration { "configurations": { "production": { "optimization": true, "sourceMap": false, "namedChunks": false, "extractLicenses": true, "outputHashing": "all", "security": { "autoCsp": true } } } }
ng serve with esbuild may inject inline source maps even when disabled (angular/angular-cli#31331)typescriptconst maskCreditCard = (cc: string): string => cc.replace(/\d(?=\d{4})/g, "*"); // "1234567890123456" → "************3456" const maskEmail = (email: string): string => { const [local, domain] = email.split("@"); return `${local.slice(0, 2)}***@${domain}`; }; // "user@example.com" → "us***@example.com"
typescriptimport * as Sentry from "@sentry/angular"; // Global error handler @Injectable() export class GlobalErrorHandler implements ErrorHandler { handleError(error: Error) { Sentry.captureException(error); // Don't log sensitive data console.error("Application error:", error.message); } } // Register (provideAppInitializer(() => { Sentry.init({ dsn: environment.sentryDsn, beforeSend(event) { if (event.user) delete event.user.email; return event; }, }); }), { provide: ErrorHandler, useClass: GlobalErrorHandler });
| Layer | Tool | | ----------------- | ------------------------------------- | | Unit/Component | Vitest or Karma + Jasmine | | Security scanning | Socket.dev, Snyk, npm audit | | E2E | Playwright | | Accessibility | axe-core |
typescriptdescribe("AuthGuard", () => { it("should redirect unauthenticated users to login", () => { TestBed.configureTestingModule({ providers: [ { provide: AuthService, useValue: { isAuthenticated: () => false } }, ], }); const guard = TestBed.runInInjectionContext(() => authGuard({} as any, { url: "/dashboard" } as any), ); expect(guard).toEqual( jasmine.objectContaining({ queryParams: { returnUrl: "/dashboard" } }), ); }); }); describe("XSS Prevention", () => { it("should sanitize dangerous HTML", () => { const malicious = '<img src=x onerror=alert("XSS")>'; const sanitized = DOMPurify.sanitize(malicious); expect(sanitized).not.toContain("onerror"); }); });
| Rank | Category | Angular Relevance | | ------- | -------------------------------------------- | -------------------------------------------------------------------------- | | A01 | Broken Access Control | High — route guards are client-side only; server must enforce | | A02 | Security Misconfiguration | High — CSP, CORS, JIT in production, debug flags | | A03 | Supply Chain Failures (NEW) | Critical — SVG-based XSS, esbuild CVE, npm attacks | | A04 | Cryptographic Failures | Medium — token handling, HTTPS | | A05 | Injection | Angular-specific — template injection (CSTI) if JIT; SVG/MathML bypass | | A06 | Insecure Design | High — SSR shared injector (CVE-2025-59052) | | A07 | Auth Failures | High — see Section 1 | | A08 | Software & Data Integrity | High — CI/CD, ng add schematics | | A09 | Logging & Alerting Failures | Medium — client-side error reporting | | A10 | Mishandling Exceptional Conditions (NEW) | Medium — error handler, fail closed |
Before deploying to production:
bypassSecurityTrust* calls audited and paired with DOMPurifyallowedHosts configuredngJitMode: false in server buildsunsafe-eval needed)npm audit in CI)ng addOther measured skills in the registry, with their headline benchmark lift.