Angular security hardening: XSS template-binding hygiene, DomSanitizer bypass policy, nonce-based CSP, Trusted Types (v17+), CSRF handling with the .NET backend, and permission-ONLY client-side access control — route guards and UI gating check permissions, never roles; client checks are UX only, real enforcement is dotnet-authorization. References OWASP A01/A03.
Scanned 9/6/2026
Install to Claude Code
npx -y skills add AgenticPawan/FullStack-Pilot --skill angular-security --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Angular Security?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/agenticpawan-angular-security)More formats (shields.io, HTML) on the badges page.
---
name: angular-security
description: "Angular security hardening: XSS template-binding hygiene, DomSanitizer bypass policy, nonce-based CSP, Trusted Types (v17+), CSRF handling with the .NET backend, and permission-ONLY client-side access control — route guards and UI gating check permissions, never roles; client checks are UX only, real enforcement is dotnet-authorization. References OWASP A01/A03."
when_to_use: XSS, innerHTML, DomSanitizer, CSP, Content Security Policy, Trusted Types, CSRF, cross-site scripting, sanitization, security audit, nonce, unsafe-inline, XSRF token, bypassSecurityTrust, security review, route guard, CanActivate, CanMatch, permission guard, role guard, hasPermission directive, hasRole directive, access control
applies_to: angular
---
<!-- Version index:
DomSanitizer all Angular versions
CSP_NONCE token Angular 16+
autoCsp option Angular 17+
Trusted Types policies Angular 17+ (5 built-in policies)
Trusted Types stable Angular 17+
withXsrfConfiguration() Angular 15+ (provideHttpClient functional API)
-->
## Rule reference
| ID | Standard | Severity |
|----|----------|----------|
| angular-no-innerhtml | OWASP A03 | block |
| angular-no-bypass-without-comment | OWASP A03 | block |
| angular-csp-nonce | InternalPolicy | warn |
| angular-trusted-types | InternalPolicy | warn |
| angular-csrf-dotnet | InternalPolicy | warn |
| angular-permission-based-authz | OWASP A01 | block |
---
## XSS — template binding hygiene
Angular escapes `{{ interpolation }}` and `[textContent]` bindings automatically. The danger surface is:
- `[innerHTML]` — Angular sanitizes but `bypassSecurityTrustHtml` disables sanitization entirely
- Dynamic `<script>` injection via `Renderer2.createElement`
- `[style]` / `[src]` / `[href]` with user-controlled values
### BAD — innerHTML with untrusted content
```html
<!-- OWASP A03: Angular sanitizes here, but the pattern invites bypass misuse -->
<div [innerHTML]="user.bio"></div>
<div [innerHTML]="product.description"></div>
```
```typescript
// CRITICAL: disables ALL sanitization for this value
this.content = this.sanitizer.bypassSecurityTrustHtml(apiResponse.html);
```
### GOOD — text binding (always safe); HTML only with justification
```html
<!-- Option 1: text binding — zero XSS risk -->
<p>{{ user.bio }}</p>
<!-- Option 2: HTML required — sanitized value with justification comment -->
<div [innerHTML]="sanitizedBio"></div>
```
```typescript
// Acceptable ONLY when source is server-validated rich text (e.g. CMS output).
// Source: CMS markdown-to-HTML pipeline — no user-supplied HTML is accepted.
this.sanitizedBio = this.sanitizer.bypassSecurityTrustHtml(cms.html);
```
**Rule:** every call to any `bypassSecurityTrust*` method MUST have a comment on the
preceding line explaining the trusted source. PR reviewers MUST reject uncommented bypasses.
---
## Content Security Policy — nonce-based (Angular 16+)
Do not use `unsafe-inline`. Use per-request nonces generated server-side.
### BAD — unsafe-inline in CSP header
```
# .NET backend response header — DO NOT USE
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline';
```
### GOOD — nonce-based CSP end-to-end
**Step 1 — .NET backend generates a nonce per request:**
```csharp
// Program.cs / middleware
app.Use(async (ctx, next) =>
{
var nonce = Convert.ToBase64String(RandomNumberGenerator.GetBytes(16));
ctx.Items["csp-nonce"] = nonce;
ctx.Response.Headers.Append(
"Content-Security-Policy",
$"default-src 'self'; script-src 'self' 'nonce-{nonce}'; style-src 'self' 'nonce-{nonce}';"
);
await next();
});
```
**Step 2 — Inject nonce into the Angular root element:**
```html
<!-- index.html — nonce written by the .NET view/Razor page -->
<app-root ngCspNonce="@ViewData["csp-nonce"]"></app-root>
```
**Step 3 — Angular reads the nonce automatically** via the `CSP_NONCE` token (Angular 16+).
No further configuration is required — Angular applies the nonce to all inline styles it generates.
**Optional (Angular 17+): enable autoCsp in angular.json:**
```json
{
"architect": {
"build": {
"options": {
"security": { "autoCsp": true }
}
}
}
}
```
---
## Trusted Types (Angular 17+)
Angular ships five built-in Trusted Types policies:
| Policy | Used by |
|--------|---------|
| `angular` | Core framework DOM operations |
| `angular#bundler` | Lazy-loaded chunk injection |
| `angular#unsafe-bypass` | `DomSanitizer.bypassSecurityTrust*` calls |
| `angular#unsafe-jit` | JIT compiler (dev only — disable in production) |
| `angular#unsafe-upgrade` | `@angular/upgrade` hybrid apps |
**Enable Trusted Types in CSP:**
```
Content-Security-Policy:
require-trusted-types-for 'script';
trusted-types angular angular#bundler;
```
Remove `angular#unsafe-bypass` from the policy to enforce that no bypass calls reach the DOM.
Remove `angular#unsafe-jit` in production (JIT should not run in production builds).
### BAD — Trusted Types policy omitted
```
# Allows any string as DOM sink — Trusted Types enforcement inactive
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-...';
```
### GOOD — full Trusted Types enforcement
```
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{nonce}';
style-src 'self' 'nonce-{nonce}';
require-trusted-types-for 'script';
trusted-types angular angular#bundler;
```
---
## CSRF — .NET backend integration
`HttpClient` sends the XSRF-TOKEN cookie value as the `X-XSRF-TOKEN` request header on
all state-changing methods (POST, PUT, PATCH, DELETE) automatically.
### BAD — custom token header names that break CSRF protection
```typescript
// withNoXsrfProtection() disables CSRF completely — requires explicit justification
provideHttpClient(withNoXsrfProtection())
```
### GOOD — coordinate cookie/header names with .NET antiforgery options
```typescript
// app.config.ts — Angular side
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(
withXsrfConfiguration({
cookieName: 'XSRF-TOKEN', // must match .NET AntiforgeryOptions.Cookie.Name
headerName: 'X-XSRF-TOKEN' // must match .NET AntiforgeryOptions.HeaderName
})
)
]
};
```
```csharp
// Program.cs — .NET side
builder.Services.AddAntiforgery(opts =>
{
opts.Cookie.Name = "XSRF-TOKEN";
opts.HeaderName = "X-XSRF-TOKEN";
opts.Cookie.SameSite = SameSiteMode.Strict;
opts.Cookie.SecurePolicy = CookieSecurePolicy.Always;
});
```
---
## Access control — permission-based ONLY (no role checks, ever)
Client-side access control is UX, not enforcement — the .NET API is the real authorization
boundary (see `dotnet-authorization` AZ-001) and must reject any request a hidden/disabled
UI element would have blocked. But the client-side gating itself must follow the same
permissions-only rule as the backend: route guards and UI-element visibility must check a
discrete **permission**, never a **role** name. A role check on the client has the same
problems as on the server — it can't be revoked or granted per-capability, and a new
feature either overloads an existing role or forces shipping a new one.
### BAD — route guard and structural directive keyed on role
```typescript
// orders.routes.ts
export const routes: Routes = [
{
path: 'orders/:id/approve',
canActivate: [() => inject(AuthService).hasRole('Manager')], // role, not permission
loadComponent: () => import('./approve-order.component'),
},
];
```
```html
<!-- order-list.component.html -->
<button *appHasRole="'Manager'" (click)="approve(order)">Approve</button>
```
### GOOD — permission-based guard and structural directive
```typescript
// orders.routes.ts
export const routes: Routes = [
{
path: 'orders/:id/approve',
canActivate: [() => inject(PermissionService).hasPermission('orders.approve')],
loadComponent: () => import('./approve-order.component'),
},
];
// permission.service.ts — permissions resolved from the server per-session/token refresh,
// never decoded from JWT claims directly (the JWT should not carry them — dotnet-authorization AZ-006)
@Injectable({ providedIn: 'root' })
export class PermissionService {
private readonly permissions = signal<ReadonlySet<string>>(new Set());
hasPermission(permission: string): boolean {
return this.permissions().has(permission);
}
}
```
```html
<!-- order-list.component.html -->
<button *appHasPermission="'orders.approve'" (click)="approve(order)">Approve</button>
```
```typescript
// has-permission.directive.ts
@Directive({ selector: '[appHasPermission]', standalone: true })
export class HasPermissionDirective {
private readonly permission = inject(PermissionService);
private readonly templateRef = inject(TemplateRef);
private readonly viewContainer = inject(ViewContainerRef);
@Input() set appHasPermission(required: string) {
this.viewContainer.clear();
if (this.permission.hasPermission(required)) {
this.viewContainer.createEmbeddedView(this.templateRef);
}
}
}
```
**Detection rule:** flag any `hasRole(...)`, `*appHasRole`, `roles.includes(...)`, or route
`data: { roles: [...] }` array used to drive a `canActivate`/`canMatch` guard or hide/show a
template element. The equivalent permission-based guard/directive is always the fix — there
is no acceptable "coarse" exception, matching the backend rule.
---
## Angular security checklist
- [ ] No `bypassSecurityTrust*` calls without a preceding source-justification comment
- [ ] No `unsafe-inline` in CSP headers — use nonces (Angular 16+)
- [ ] `ngCspNonce` set on root element from server-generated per-request nonce
- [ ] `require-trusted-types-for 'script'` in CSP (Angular 17+)
- [ ] `angular#unsafe-jit` excluded from `trusted-types` in production builds
- [ ] `withXsrfConfiguration()` cookie/header names match .NET `AntiforgeryOptions`
- [ ] `withNoXsrfProtection()` is not used unless the endpoint is a public read-only API
- [ ] Dynamic route parameters are never interpolated into `[innerHTML]`
- [ ] `[src]` and `[href]` bindings with user content go through `bypassSecurityTrustUrl` with justification
- [ ] No `canActivate`/`canMatch` guard or structural directive checks a role — permission-based only
- [ ] No route `data: { roles: [...] }` array driving a guard
- [ ] `PermissionService` sources permissions from the server per-session, not decoded from JWT claims
---
## References
- Angular security guide: https://angular.dev/best-practices/security
- OWASP A01:2021 Broken Access Control: https://owasp.org/Top10/A01_2021-Broken_Access_Control/
- OWASP A03:2021 Injection: https://owasp.org/Top10/A03_2021-Injection/
- W3C Trusted Types: https://w3c.github.io/trusted-types/dist/spec/
- .NET Antiforgery: https://learn.microsoft.com/en-us/aspnet/core/security/anti-request-forgery
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!