Angular Security
Angular has security built into its DNA, providing many security measures by default that require manual implementation in React/Next.js. These built-in security features are a major advantage in large-scale enterprise development.
1. XSS (Cross-Site Scripting) Prevention
Default Sanitization
Angular automatically escapes all values in templates. Interpolation expressions ({{ }}) are converted to HTML entities, preventing script injection.
// Component
@Component({
template: `<p>{{ userInput }}</p>`
})
export class SafeComponent {
userInput = '<script>alert("XSS")</script>';
// Rendered as: <script>alert("XSS")</script>
}
Unlike React's dangerouslySetInnerHTML, Angular requires explicit opt-in for potentially dangerous operations.
[innerHTML] Binding Sanitization
Even when using [innerHTML] binding, Angular automatically sanitizes the HTML. <script> tags and dangerous attributes like onerror are removed.
@Component({
template: `<div [innerHTML]="htmlContent"></div>`
})
export class HtmlComponent {
// <script> tags are automatically removed
htmlContent = '<b>Bold text</b><script>alert("XSS")</script>';
// Output: <b>Bold text</b>
}
DomSanitizer Service
Use DomSanitizer to explicitly insert trusted content. However, misuse can introduce XSS vulnerabilities, so handle it with care.
import { Component } from '@angular/core';
import { DomSanitizer, SafeHtml, SafeUrl } from '@angular/platform-browser';
@Component({
selector: 'app-trusted',
template: `
<div [innerHTML]="trustedHtml"></div>
<a [href]="trustedUrl">Link</a>
`
})
export class TrustedComponent {
trustedHtml: SafeHtml;
trustedUrl: SafeUrl;
constructor(private sanitizer: DomSanitizer) {
// ⚠️ Only use with content from trusted sources
this.trustedHtml = this.sanitizer.bypassSecurityTrustHtml(
'<b>Admin-approved content</b>'
);
// ⚠️ Never use with user input
this.trustedUrl = this.sanitizer.bypassSecurityTrustUrl(
'https://example.com/safe-page'
);
}
}
| Method | Use Case | Risk |
|---|---|---|
bypassSecurityTrustHtml | HTML insertion | Script injection |
bypassSecurityTrustUrl | href/src URLs | javascript: scheme |
bypassSecurityTrustResourceUrl | iframe/script URLs | Loading external code |
bypassSecurityTrustStyle | Inline styles | CSS injection |
Never use bypassSecurityTrust* with user input. Restrict its use to content that has been validated and sanitized on the server side.
2. CSRF Token (XSRF) Protection
Built-in XSRF Protection in HttpClient
Angular's HttpClient has built-in support for cookie-based XSRF protection. When the server sets a cookie named XSRF-TOKEN, HttpClient automatically appends it as an X-XSRF-TOKEN header on each request.
// app.config.ts
import { provideHttpClient, withXsrfConfiguration } from '@angular/common/http';
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(
withXsrfConfiguration({
cookieName: 'XSRF-TOKEN', // Cookie name set by server (default)
headerName: 'X-XSRF-TOKEN', // Header name added to requests (default)
})
)
]
};
Server-Side Configuration (ASP.NET Core)
// Program.cs
builder.Services.AddAntiforgery(options =>
{
options.Cookie.Name = "XSRF-TOKEN";
options.HeaderName = "X-XSRF-TOKEN";
options.Cookie.SameSite = SameSiteMode.Strict;
});
// Make XSRF-TOKEN cookie readable by Angular (HttpOnly=false)
app.Use(async (context, next) =>
{
var antiforgery = context.RequestServices.GetRequiredService<IAntiforgery>();
var tokens = antiforgery.GetAndStoreTokens(context);
context.Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken!,
new CookieOptions { HttpOnly = false });
await next();
});
React/Next.js requires manual CSRF protection implementation, while Angular's HttpClient provides this mechanism out of the box.
3. Content Security Policy (CSP)
Basic CSP Configuration
CSP (Content Security Policy) is an HTTP header that instructs browsers about allowable resource loading policies. It prevents secondary damage from XSS (i.e., script execution).
# Server-side HTTP header example
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{RANDOM_NONCE}';
style-src 'self' 'nonce-{RANDOM_NONCE}';
img-src 'self' data: https://trusted-cdn.example.com;
connect-src 'self' https://api.example.com;
frame-ancestors 'none';
Nonce-Based CSP (Angular Recommended)
Angular 16+ recommends nonce-based CSP. By passing a server-generated random nonce value to Angular, inline styles can be used in a CSP-compliant manner.
<!-- index.html: Angular reads the nonce attribute -->
<app-root ngCspNonce="{SERVER_GENERATED_NONCE}"></app-root>
// Angular side: CSP nonce configuration
import { CSP_NONCE } from '@angular/core';
export const appConfig: ApplicationConfig = {
providers: [
{
provide: CSP_NONCE,
useValue: globalThis.myRandomNonce, // Nonce value passed from server
}
]
};
ViewEncapsulation and style-src
Angular's default ViewEncapsulation (Emulated) injects inline styles, which may require style-src 'unsafe-inline'. Using nonces resolves this issue.
// Component-level configuration
@Component({
selector: 'app-root',
encapsulation: ViewEncapsulation.ShadowDom, // Use Shadow DOM to isolate styles
})
export class AppComponent {}
4. Trusted Types
Trusted Types API Overview
Trusted Types is a browser API that makes DOM operations type-safe to prevent XSS. It prohibits direct string assignment to dangerous sinks like innerHTML and eval, accepting only trusted objects.
Angular's Trusted Types Support
Angular natively supports Trusted Types, and all internal DOM operations are performed through Trusted Types policies.
# Enable Trusted Types via CSP header
Content-Security-Policy: require-trusted-types-for 'script'; trusted-types angular
// Angular's Trusted Types policy name is 'angular'
// When a custom policy is needed
const policy = trustedTypes.createPolicy('myAppPolicy', {
createHTML: (input: string) => {
// Custom sanitization logic
return DOMPurify.sanitize(input);
}
});
Trusted Types support varies by browser (major browsers are now supported). Consider using a polyfill for broader compatibility.
5. Authentication & Authorization Patterns
Authentication Token Management with HTTP Interceptors
// auth.interceptor.ts
import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const authService = inject(AuthService);
const token = authService.getToken();
if (token) {
const authReq = req.clone({
setHeaders: {
Authorization: `Bearer ${token}`
}
});
return next(authReq);
}
return next(req);
};
// Register in app.config.ts
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(withInterceptors([authInterceptor]))
]
};
Route-Level Authorization with Guards
// auth.guard.ts (functional guard - Angular 15+)
import { inject } from '@angular/core';
import { Router, CanActivateFn } from '@angular/router';
import { AuthService } from './auth.service';
export const authGuard: CanActivateFn = (route, state) => {
const authService = inject(AuthService);
const router = inject(Router);
if (authService.isAuthenticated()) {
return true;
}
// Redirect to login page when unauthenticated
return router.createUrlTree(['/login'], {
queryParams: { returnUrl: state.url }
});
};
// app.routes.ts
export const routes: Routes = [
{
path: 'dashboard',
component: DashboardComponent,
canActivate: [authGuard]
},
{
path: 'admin',
loadComponent: () => import('./admin/admin.component'),
canActivate: [authGuard, adminGuard] // Combining multiple guards
}
];
Element-Level Authorization with Custom Directives
// has-role.directive.ts
@Directive({
selector: '[appHasRole]',
standalone: true
})
export class HasRoleDirective implements OnInit {
@Input('appHasRole') requiredRole!: string | string[];
constructor(
private templateRef: TemplateRef<unknown>,
private viewContainer: ViewContainerRef,
private authService: AuthService
) {}
ngOnInit() {
const roles = Array.isArray(this.requiredRole)
? this.requiredRole
: [this.requiredRole];
if (this.authService.hasAnyRole(roles)) {
this.viewContainer.createEmbeddedView(this.templateRef);
} else {
this.viewContainer.clear();
}
}
}
// Usage
// <button *appHasRole="'admin'">Admin-only button</button>
// <div *appHasRole="['editor', 'admin']">Content for editors and admins</div>
JWT Token Refresh Pattern
// auth.interceptor.ts (with token refresh)
import { catchError, switchMap, throwError } from 'rxjs';
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const authService = inject(AuthService);
return next(addToken(req, authService.getToken())).pipe(
catchError(error => {
if (error.status === 401 && !req.url.includes('/auth/refresh')) {
// Access token expired → attempt refresh
return authService.refreshToken().pipe(
switchMap(newToken => {
return next(addToken(req, newToken));
}),
catchError(refreshError => {
authService.logout(); // Logout if refresh also fails
return throwError(() => refreshError);
})
);
}
return throwError(() => error);
})
);
};
function addToken(req: HttpRequest<unknown>, token: string | null) {
return token ? req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }) : req;
}
Compared to React: React requires implementing auth logic individually with custom hooks or context. Angular enables unified auth/authz management through interceptors (HTTP layer) + guards (route layer) + directives (UI layer).
6. Secure API Requests
withCredentials and HttpOnly Cookies
// withCredentials configuration in HttpClient
@Injectable({ providedIn: 'root' })
export class ApiService {
constructor(private http: HttpClient) {}
getData() {
return this.http.get('/api/data', {
withCredentials: true // Send HttpOnly cookies
});
}
}
// Using an interceptor for global configuration
export const credentialsInterceptor: HttpInterceptorFn = (req, next) => {
const credentialReq = req.clone({ withCredentials: true });
return next(credentialReq);
};
CORS and Preflight Requests
CORS (Cross-Origin Resource Sharing) is a server-side concern. When an Angular app makes requests to a different origin, the browser sends a preflight request (OPTIONS) first.
// Server-side CORS configuration (ASP.NET Core)
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowAngularApp", policy =>
{
policy.WithOrigins("https://yourapp.example.com") // Don't use wildcard (*)
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials(); // Required when using withCredentials
});
});
AllowAnyOrigin() and AllowCredentials() cannot be used together (prohibited for security reasons). Always specify concrete origins.
7. Security Auditing and Dependencies
Leveraging npm audit
# Check for vulnerabilities
npm audit
# Auto-fix (patch versions only)
npm audit fix
# Detailed report (JSON format)
npm audit --json
# CI/CD pipeline usage (fail build on high-severity vulnerabilities)
npm audit --audit-level=high
Automatic Patching with ng update
# Check for Angular and dependency updates
ng update
# Update Angular core
ng update @angular/core @angular/cli
# Apply all compatible updates
ng update --all
ng update automatically runs migration scripts during major version updates, significantly reducing manual fixes.
Dependency Vulnerability Management
// package.json: Strict version management
{
"dependencies": {
"@angular/core": "~18.0.0" // Tilde (~) auto-updates patch versions only
}
}
# Integration with third-party tools like Snyk
npx snyk test
npx snyk monitor
# Using GitHub Dependabot (.github/dependabot.yml)
# Automatically creates PRs for npm package updates on a schedule
Angular Security Configuration Checklist
| Item | Recommended Setting |
|---|---|
bypassSecurityTrust* usage | Never use with user input |
| XSRF protection | Enable with withXsrfConfiguration() |
| CSP headers | Configure with nonce-based policy |
withCredentials | Enable only when necessary |
| CORS | Specify concrete origins |
| Dependencies | Run npm audit + ng update regularly |
| Trusted Types | Enable via CSP header (recommended) |