Angular / web frontend security
Angular escapes interpolated values by output context by default, so the classic reflected XSS is closed out of the box. The vulnerabilities are where you leave that path, trust the client with something it should not hold, or reach a DOM sink Angular never saw. This is the client-side map; it pairs with the runtime security-guidance plugin (which reviews a live diff) and with dotnet-security (the server side). Treat every value that crossed a trust boundary - an API response, a route param, a deep link, a postMessage - as hostile until proven otherwise.
XSS and the sanitizer bypass
- Interpolation
{{ }} and property bindings auto-escape by context. The holes are the escape hatches: DomSanitizer.bypassSecurityTrustHtml / Script / Style / Url / ResourceUrl each disable Angular's protection for that value. Never call a bypassSecurityTrust* on anything that contains user input - a bypassSecurityTrustResourceUrl on a user-controlled iframe or <object> src is a full XSS, and bypassSecurityTrustHtml on user markup ships a script.
// VULNERABLE - user-controlled query param, sanitizer disabled for it: full XSS
readonly embedUrl = this.sanitizer.bypassSecurityTrustResourceUrl(
this.route.snapshot.queryParams['src']);
// SAFE - bypass only a value YOU constructed; user data enters it encoded
readonly embedUrl = this.sanitizer.bypassSecurityTrustResourceUrl(
`https://player.example.com/embed/${encodeURIComponent(this.videoId)}`);
[innerHTML]="value" is sanitized for the HTML context, but it is still a smell on user data - prefer structural rendering. Never assign ElementRef.nativeElement.innerHTML, call document.write, or use Renderer2 / raw DOM APIs to inject unsanitized markup; those skip Angular's sanitizer entirely.
- URL bindings (
[href], [src]) are sanitized against javascript: and other dangerous schemes - a bypassSecurityTrustUrl re-opens that hole.
- Reflected-from-the-API is still XSS: a value the API stored from another user and you render is untrusted; the escaping must hold end-to-end.
Content-Security-Policy
- Ship a strict CSP - a nonce-based script-src with no unsafe-inline and no unsafe-eval. Angular supports a build/runtime nonce (
CSP_NONCE) so its inline styles/scripts carry the nonce. A good CSP turns a landed XSS from code execution into a blocked console error.
- The style-src directive needs a nonce or hash for Angular component styles; do not fall back to unsafe-inline to make it work.
- Add Trusted Types where the browser supports it (the require-trusted-types-for 'script' CSP directive): it makes raw string-to-DOM-sink writes throw at the browser layer, so a stray
innerHTML or a slipped bypassSecurityTrust* becomes a hard error instead of a payload. Angular's sanitizer is Trusted Types-aware, so idiomatic bindings keep working.
CSRF
- For cookie-authenticated APIs, use Angular HttpClient's built-in XSRF support (
provideHttpClient(withXsrfConfiguration(...))): it reads the XSRF-TOKEN cookie and sends the X-XSRF-TOKEN header, which the server validates. A state-changing cookie-auth endpoint with no antiforgery is a CSRF finding.
- Pure
Authorization: Bearer APIs carry no ambient cookie, so CSRF does not apply - but then see token storage, because the token lives in JS.
Secrets never ship to the client
environment.ts is compiled into the bundle - anything there is world-readable. No API keys, connection strings, or signing secrets client-side. A value the browser can read is not a secret; if the front end needs one, it calls a backend-for-frontend that holds it.
- Distinguish a publishable key (a Stripe publishable key, a Maps browser key with referrer restrictions - legitimately client-side) from a real secret; only the former belongs in the bundle.
- Disable or restrict source maps in production, or you ship readable source and comments.
Auth-token storage
localStorage / sessionStorage are readable by any script on the origin - one XSS drains every token there. Prefer an httpOnly, Secure, SameSite cookie the JS cannot read (paired with the CSRF protection above). If a token must live in JS, minimize its lifetime and scope so a theft window is short.
SSR / hydration (Angular SSR / Universal)
TransferState embeds server-fetched data into the served HTML for the client to reuse - never place a secret, an internal field, or another user's data in it.
- SSR that reflects request input into the rendered HTML is server-side XSS - escape it on the server too.
- Server-only config and code must not leak into the client bundle; keep server providers off the browser build.
Navigation and redirects
- Open redirect: never
router.navigateByUrl / window.location = a URL taken from a query param without validating it against an allowlist of internal paths.
// VULNERABLE - ?returnUrl=https://evil.example walks the user off-site after login
this.router.navigateByUrl(this.route.snapshot.queryParams['returnUrl']);
// SAFE - internal paths only ('//' is protocol-relative, still an exit)
const returnUrl: string = this.route.snapshot.queryParams['returnUrl'] ?? '/';
this.router.navigateByUrl(
returnUrl.startsWith('/') && !returnUrl.startsWith('//') ? returnUrl : '/');
target="_blank" needs rel="noopener" (modern browsers mostly default it, but be explicit) or the opened page can drive window.opener.
- Validate route and query params before they reach a request or a DOM sink.
Dependencies and supply chain
npm audit and the lockfile: a known-CVE package version is a finding. Pin versions, review transitive pulls, and watch for typosquatted package names. A compromised build-time dependency runs with your build's privileges.
Sensitive-data hygiene
- No PII or tokens in
console.log shipped to production. No secrets rendered into the DOM. Strip debug panels and diagnostic routes from the production build.
Review output
Report findings as surface | risk | fix, ordered by risk - e.g. [innerHTML] fed through bypassSecurityTrustHtml | stored XSS runs in every viewer's session | bind the sanitized value and keep trust calls away from user-influenced input. Findings on the server side route to the skill covering ASP.NET / .NET hardening (the OWASP-mapped server mitigations), on the native shell to the skill covering the Ionic / Capacitor native attack surface (Keychain storage, deep links, WebView lockdown) - name the route by what it covers, do not restate its content here; when no installed skill matches, keep the finding in this report tagged with its surface and mark it UNVERIFIED for that stack.
Where the rest lives
Server-side authorization, injection, CORS, and error-envelope leakage are dotnet-security. The Ionic / Capacitor native shell - secure storage, deep-link validation, WebView hardening, native permissions - is ionic-security (an Ionic app inherits everything here plus that native surface).
1---2name: angular-security3description: Angular / web frontend security-hardening reference mapped to concrete Angular 17+ mitigations: XSS and the DomSanitizer bypassSecurityTrust* escape hatches, innerHTML injection, nonce-based CSP, CSRF via HttpClient's XSRF support, secrets that must never ship in the bundle (environment.ts, source maps), auth-token storage (httpOnly cookie over localStorage), SSR/TransferState leaks, open redirects, target=_blank window.opener, unsafe URL bindings, and vulnerable npm dependencies. Load when hardening or reviewing an Angular web feature, or when the security-auditor sweeps the web stack. Points at dotnet-security for the API side, ionic-security for the Capacitor native shell. Do NOT load for non-security work or the mobile native surface.4---56# Angular / web frontend security78Angular escapes interpolated values by output context by default, so the classic reflected XSS is closed out of the box. The vulnerabilities are where you leave that path, trust the client with something it should not hold, or reach a DOM sink Angular never saw. This is the client-side map; it pairs with the runtime security-guidance plugin (which reviews a live diff) and with `dotnet-security` (the server side). Treat every value that crossed a trust boundary - an API response, a route param, a deep link, a postMessage - as hostile until proven otherwise.910## XSS and the sanitizer bypass1112- Interpolation `{{ }}` and property bindings auto-escape by context. The holes are the escape hatches: `DomSanitizer.bypassSecurityTrustHtml / Script / Style / Url / ResourceUrl` each **disable** Angular's protection for that value. Never call a `bypassSecurityTrust*` on anything that contains user input - a `bypassSecurityTrustResourceUrl` on a user-controlled iframe or `<object>` src is a full XSS, and `bypassSecurityTrustHtml` on user markup ships a script.1314```ts15// VULNERABLE - user-controlled query param, sanitizer disabled for it: full XSS16readonly embedUrl = this.sanitizer.bypassSecurityTrustResourceUrl(17 this.route.snapshot.queryParams['src']);1819// SAFE - bypass only a value YOU constructed; user data enters it encoded20readonly embedUrl = this.sanitizer.bypassSecurityTrustResourceUrl(21 `https://player.example.com/embed/${encodeURIComponent(this.videoId)}`);22```23- `[innerHTML]="value"` is sanitized for the HTML context, but it is still a smell on user data - prefer structural rendering. Never assign `ElementRef.nativeElement.innerHTML`, call `document.write`, or use `Renderer2` / raw DOM APIs to inject unsanitized markup; those skip Angular's sanitizer entirely.24- URL bindings (`[href]`, `[src]`) are sanitized against `javascript:` and other dangerous schemes - a `bypassSecurityTrustUrl` re-opens that hole.25- Reflected-from-the-API is still XSS: a value the API stored from another user and you render is untrusted; the escaping must hold end-to-end.2627## Content-Security-Policy2829- Ship a strict CSP - a nonce-based script-src with no unsafe-inline and no unsafe-eval. Angular supports a build/runtime nonce (`CSP_NONCE`) so its inline styles/scripts carry the nonce. A good CSP turns a landed XSS from code execution into a blocked console error.30- The style-src directive needs a nonce or hash for Angular component styles; do not fall back to unsafe-inline to make it work.31- Add Trusted Types where the browser supports it (the require-trusted-types-for 'script' CSP directive): it makes raw string-to-DOM-sink writes throw at the browser layer, so a stray `innerHTML` or a slipped `bypassSecurityTrust*` becomes a hard error instead of a payload. Angular's sanitizer is Trusted Types-aware, so idiomatic bindings keep working.3233## CSRF3435- For **cookie-authenticated** APIs, use Angular HttpClient's built-in XSRF support (`provideHttpClient(withXsrfConfiguration(...))`): it reads the `XSRF-TOKEN` cookie and sends the `X-XSRF-TOKEN` header, which the server validates. A state-changing cookie-auth endpoint with no antiforgery is a CSRF finding.36- Pure `Authorization: Bearer` APIs carry no ambient cookie, so CSRF does not apply - but then see token storage, because the token lives in JS.3738## Secrets never ship to the client3940- `environment.ts` is compiled **into** the bundle - anything there is world-readable. No API keys, connection strings, or signing secrets client-side. A value the browser can read is not a secret; if the front end needs one, it calls a backend-for-frontend that holds it.41- Distinguish a **publishable** key (a Stripe publishable key, a Maps browser key with referrer restrictions - legitimately client-side) from a real secret; only the former belongs in the bundle.42- Disable or restrict source maps in production, or you ship readable source and comments.4344## Auth-token storage4546- `localStorage` / `sessionStorage` are readable by any script on the origin - one XSS drains every token there. Prefer an `httpOnly`, `Secure`, `SameSite` cookie the JS cannot read (paired with the CSRF protection above). If a token must live in JS, minimize its lifetime and scope so a theft window is short.4748## SSR / hydration (Angular SSR / Universal)4950- `TransferState` embeds server-fetched data into the served HTML for the client to reuse - never place a secret, an internal field, or another user's data in it.51- SSR that reflects request input into the rendered HTML is server-side XSS - escape it on the server too.52- Server-only config and code must not leak into the client bundle; keep server providers off the browser build.5354## Navigation and redirects5556- **Open redirect**: never `router.navigateByUrl` / `window.location =` a URL taken from a query param without validating it against an allowlist of internal paths.5758```ts59// VULNERABLE - ?returnUrl=https://evil.example walks the user off-site after login60this.router.navigateByUrl(this.route.snapshot.queryParams['returnUrl']);6162// SAFE - internal paths only ('//' is protocol-relative, still an exit)63const returnUrl: string = this.route.snapshot.queryParams['returnUrl'] ?? '/';64this.router.navigateByUrl(65 returnUrl.startsWith('/') && !returnUrl.startsWith('//') ? returnUrl : '/');66```67- `target="_blank"` needs `rel="noopener"` (modern browsers mostly default it, but be explicit) or the opened page can drive `window.opener`.68- Validate route and query params before they reach a request or a DOM sink.6970## Dependencies and supply chain7172- `npm audit` and the lockfile: a known-CVE package version is a finding. Pin versions, review transitive pulls, and watch for typosquatted package names. A compromised build-time dependency runs with your build's privileges.7374## Sensitive-data hygiene7576- No PII or tokens in `console.log` shipped to production. No secrets rendered into the DOM. Strip debug panels and diagnostic routes from the production build.7778## Review output7980Report findings as `surface | risk | fix`, ordered by risk - e.g. `[innerHTML] fed through bypassSecurityTrustHtml | stored XSS runs in every viewer's session | bind the sanitized value and keep trust calls away from user-influenced input`. Findings on the server side route to the skill covering ASP.NET / .NET hardening (the OWASP-mapped server mitigations), on the native shell to the skill covering the Ionic / Capacitor native attack surface (Keychain storage, deep links, WebView lockdown) - name the route by what it covers, do not restate its content here; when no installed skill matches, keep the finding in this report tagged with its surface and mark it UNVERIFIED for that stack.8182## Where the rest lives8384Server-side authorization, injection, CORS, and error-envelope leakage are `dotnet-security`. The Ionic / Capacitor native shell - secure storage, deep-link validation, WebView hardening, native permissions - is `ionic-security` (an Ionic app inherits everything here **plus** that native surface).