Angular Security
Categories- Angular
Typescript
Security
HTTP
Security gets more and more important as applications and their user base grow, especially in the current age of more and more attacks. As soon as you have a public accessible domain, you can observe botnets e.g. searching for a /wp-admin/setup-config.php or other malicious practices (from what I observed ...).
A good resource to learn more about security is the Open Worldwide Application Security Project (OWASP) and the current OWASP Cheat Sheet Series. Here we will concentrate on client side security mainly, but also will look into server side rendering security on the surface.
Possible attack vectors
First we want to understand, how attackers can exploit our applications. Client side attacks are mainly focused on cross site scripting (XSS) and cross site request forgery (CSRF). These attacks exploit vulnerabilities in the browser and can be used to steal data from the user. This data data can be everything from a session token, a password, cookies or privacy critical information like IP addresses or bank credentials.
Outside of your code, also third party code can be malicious, so keeping all dependencies up to date and altering with third party implementations as little as possible is a good practice. For more information about dependencies look into Software Bill of Materials (SBOM) tools like CycloneDX from the OWASP project.
Cross Site Scripting
XSS tries to execute malicious code in a victim's browser. This can be done in many ways, mostly by e.g. saving some data in a cookie or by injecting a script tag into a page. This is done by finding e.g. a saved form data or other user input, which is saved and later injected and executed in a victims browser.
This can be dangerous, because it allows an attacker to impersonate the victim and steal sensitive data or perform other malicious actions on the users behalf. The attacker can also inject malicious code into the victim's browser, which can lead to other attacks like Clickjacking.
Reflected / Stored / DOM based XSS
In general we can divide XSS attacks into three categories: Reflected, Stored and DOM based. All XSS attacks are executed in the browser, but where the attack stores the data makes the differnce:
- Stored XSS: The script is permanently saved on the server (e.g., in a database).
sequenceDiagram actor Attacker1 as Attacker participant Server1 as Server participant Database1 as Database participant VictimBrowser1 as Victim's Browser actor Victim1 as Victim Attacker1->>Server1: POST request with malicious script Server1->>Database1: Saves script Database1-->>Server1: Confirmation Server1->>Attacker1: Success Response Note over Server1, VictimBrowser1: Time passes... Victim1->>VictimBrowser1: Navigates to the vulnerable page VictimBrowser1->>Server1: GET request for the page Server1->>Database1: Retrieves page content, including stored malicious script Database1-->>Server1: Page content with malicious script Server1->>VictimBrowser1: Sends page content including malicious script in HTML VictimBrowser1->>Victim1: Executes malicious script - Reflected XSS: The script is sent to the server (e.g., in a URL) and immediately reflected back to the user without being stored.
sequenceDiagram actor Attacker2 as Attacker participant AttackerMail2 as Attacker's Email/Chat actor Victim2 as Victim participant VictimBrowser2 as Victim's Browser participant Server2 as Server Attacker2->>AttackerMail2: Crafts malicious link with script payload AttackerMail2->>Victim2: Sends phishing email/message with malicious link Victim2->>VictimBrowser2: Clicks the malicious link VictimBrowser2->>Server2: GET request with malicious script in URL parameter Server2->>VictimBrowser2: Reflects malicious script directly in HTML response VictimBrowser2->>Victim2: Executes malicious script - DOM-based XSS: The script is never sent to the server. It's executed entirely in the client's browser (the DOM) as a result of insecure client-side code.
sequenceDiagram actor Attacker3 as Attacker participant AttackerMail3 as Attacker's Email/Chat actor Victim3 as Victim participant VictimBrowser3 as Victim's Browser participant Server3 as Server participant ClientSideJS Attacker3->>AttackerMail3: Crafts malicious link with script payload in URL fragment AttackerMail3->>Victim3: Sends phishing email/message with malicious link Victim3->>VictimBrowser3: Clicks the malicious link VictimBrowser3->>Server3: GET request for the vulnerable page (URL fragment is NOT sent to server) Server3->>VictimBrowser3: Sends legitimate HTML and vulnerable Client-Side JavaScript VictimBrowser3->>ClientSideJS: Page loads, Client-Side JavaScript starts execution ClientSideJS->>VictimBrowser3: Reads malicious payload from URL fragment (window.location.hash) ClientSideJS->>VictimBrowser3: Insecurely writes payload into the DOM VictimBrowser3->>Victim3: Executes malicious script inserted into DOM
How to prevent XSS attacks in general
The most common way to defend against it is to escape/ encode all user input before saving it to a database or other storage and sanitize all user input before displaying it to other users. Most common frontend framweworks like Angular or React already provide a built-in XSS protection.
sequenceDiagram
actor Attacker
participant WebServer
participant Database
actor Victim
participant VictimBrowser
autonumber
%% --- Part 1: Input & Sanitization (Write Phase) ---
Attacker->>WebServer: POST request with malicious payload (e.g., O'Malley<script>alert(1)</script>)
activate WebServer
Note over WebServer: == 1. Input Sanitization (On Write) ==
WebServer->>WebServer: Sanitizer Library filters input. <br/> Removes dangerous tags like <script>.
Note over WebServer: Sanitized Data: "O'Malleyalert(1)"
WebServer->>Database: Stores **sanitized** data
Database-->>WebServer: Confirmation
deactivate WebServer
Note over Attacker, VictimBrowser: ... Time passes ...
Victim->>VictimBrowser: Navigates to page
VictimBrowser->>WebServer: GET request for page content
activate WebServer
WebServer->>Database: Retrieve data
Database-->>WebServer: Returns **sanitized** data ("O'Malleyalert(1)")
Note over WebServer: == 2. Output Encoding (On Read) ==
WebServer->>WebServer: Encoder Library escapes data for HTML context. <br/> Converts ' to '
Note over WebServer: Encoded Data: "O'Malleyalert(1)"
WebServer->>VictimBrowser: Sends HTML with **encoded & sanitized** data
deactivate WebServer
VictimBrowser->>Victim: Renders "O'Malleyalert(1)" as plain text. <br/> (Script does not run!)
VictimBrowser--x Attacker: XSS Attack Prevented
Sanitizing and Encoding / Escaping
Sometimes the difference between sanitizing and encoding / escaping is not quite clear - so here a short summary:
- Sanitizing: Removes dangerous tags and attributes from user input. E.g. removes
<script>tags. You can think of it like a bouncer at a club - it decides whichtagsandattributesare allowed to be used in the HTML and filters all others out. - Escaping is a specific form of encoding that converts characters to their HTML/XML equivalents. E.g.
<becomes<. This is necessary to prevent XSS attacks, because<is a valid character in HTML and can be used to inject scripts.
Of course you normally should try not to interpret user data at all in html, but sometime you might need to do so. E.g. if you want to display a user's email address in a <a> tag. In this case you need to encode the email address to prevent XSS attacks.
Another common case for wanting some html be rendered from user input are rich text editors. In this case you need to sanitize the input before rendering it to only allow certain tags and attributes important for the rich text editor, but nothing else. This can be especially tricky with external loaded sources like images or other binaries. Even a style attribute can load background images, so we have to mitigate that, while allowing certain interactivity.
XSS prevention in Angular
Angulars default way is to trat values as untrusted, but you still have to look out for some pitfalls. You can mark values as trusted, but this should only happen if you are sure that the value is safe (already sanitized/ escaped). Also when manipulating the DOM e.g. with the Renderer2 or document directly you have to be aware of the security implications. The DomSanitizer is the inbuilt Angular service to sanitize and escape values client side.
To systematically block XSS bugs, Angular treats all values as untrusted by default. When a value is inserted into the DOM from a template binding, or interpolation, Angular sanitizes and escapes untrusted values. If a value was already sanitized outside of Angular and is considered safe, communicate this to Angular by marking the value as trusted.From Angular Docs
Angulars default model prevents xss, so something like this wouldn't work:
import { Component } from '@angular/core';
@Component({
selector: 'app-default-safe',
template: `
<h3>1. Angular's Default (Safe)</h3>
<p>The attack is automatically removed:</p>
<div [innerHTML]="htmlContent"></div>
`
})
export class DefaultSafeComponent {
// This is the dangerous string we want to display
htmlContent = `This is <b>bold</b> text. <img src=x onerror="alert('XSS Attack!')">`;
// The 'onerror' will be stripped. Only 'This is <b>bold</b> text. <img src=x>' will render.
}
But this can be circumvented by using the DomSanitizer service:
import { Component } from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
@Component({
selector: 'app-unsafe',
template: `
<h3>1. Angular's Bypassed (Unsafe)</h3>
<p>The attack is executed:</p>
<div [innerHTML]="htmlContent"></div>
`
})
export class DefaultSafeComponent {
private readonly _sanitizer = inject(DomSanitizer);
// This is the dangerous string we want to trust
htmlContent: TrustedHtml = this._sanitizer.bypassSecurityTrustHtml(
`This is <b>bold</b> text. <img src=x onerror="alert('XSS Attack!')">`
);
}
Normally we don't want to use the bypassSecurityTrustHtml function without making sure the content is sanitized - so in the example of rich text, where we need sanitize rules, to circumvent some angular default sanitization, we rely on libraries like DomPurify:
import { Component } from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import * as DOMPurify from 'dompurify'; // Import the library
@Component({
selector: 'app-unsafe',
template: `
<h3>1. Angular's Bypassed (Unsafe)</h3>
<p>The attack is executed:</p>
<div [innerHTML]="htmlContent"></div>
`
})
export class DefaultSafeComponent {
private readonly _sanitizer = inject(DomSanitizer);
// This is the dangerous string we want to trust, after sanitized by DOMPurify
htmlContent: TrustedHtml = this._sanitizer.bypassSecurityTrustHtml(
DOMPurify.sanitize(
`This is <b>bold</b> text. <img src=x onerror="alert('XSS Attack!')">`
)
);
}
In other cases you need to build simple DOM structures, this can be done XSS safe when using the Renderer2. It allows you to define elements, add attributes and text and the append the nodes to the DOM. Make sure to put user content only inside of the createText function, or sanitize the content beforehand.
// component.ts
import { Component, Renderer2, ElementRef, viewChild, effect } from '@angular/core';
@Component({
selector: 'app-renderer-safe',
template: `
<h3>3. The Programmatic Way (Very Safe)</h3>
<p>HTML tags are rendered as text, or elements are built safely:</p>
<div #target></div>
`
})
export class RendererSafeComponent {
private readonly _renderer = inject(Renderer2);
readonly target = viewChild('target');
constructor() {
effect(() => {
const target = this.target().nativeElement;
if (target) {
const p = this.renderer.createElement('p');
const text1 = this.renderer.createText('This is ');
const b = this.renderer.createElement('b');
const boldText = this.renderer.createText('safe');
const text2 = this.renderer.createText(' text. No XSS possible.');
// Build the structure: <p>This is <b>safe</b> text...</p>
this.renderer.appendChild(b, boldText);
this.renderer.appendChild(p, text1);
this.renderer.appendChild(p, b);
this.renderer.appendChild(p, text2);
// Add it to the DOM
this.renderer.appendChild(target, p);
}
});
}
}
Cross Site Request Forgery
A Cross-Site Request Forgery (CSRF) attack occurs when a malicious web site, email, blog, instant message, or program tricks an authenticated user's web browser into performing an unwanted action on a trusted site. If a target user is authenticated to the site, unprotected target sites cannot distinguish between legitimate authorized requests and forged authenticated requests.From OWASP Cheat Sheet Series
The main mitigation strategies for CSRF are Same Origin Policy, CSRF Tokens and a secure authentication mechanism.
Authentication
State of the art authentication requires some kind of Single Sign On (SSO) method to be used. This prevents the application from accessing authentication critical information from the browser at any step - only the authentication server should be able to verify the user's identity.
sequenceDiagram
autonumber
participant U as User
participant B as Browser (User Agent)
participant SP as Client App (Backend)
participant IdP as Identity Provider (Auth Server)
Note over U, IdP: Phase 1: Initiation & Redirection
U->>B: Visits Protected Page (/dashboard)
B->>SP: GET /dashboard
Note right of B: No Session Cookie found
SP-->>B: 302 Redirect to IdP
Note right of B: Params: client_id, redirect_uri,
response_type=code, state=XYZ,
code_challenge (PKCE)
B->>IdP: GET /authorize?...params...
Note over U, IdP: Phase 2: Authentication
IdP->>U: Prompt for Credentials
U->>IdP: Enters Username/Password
IdP->>IdP: Validates Credentials
IdP-->>B: 302 Redirect to App Callback
Note right of B: Params: code=AUTH_CODE, state=XYZ
Note over U, IdP: Phase 3: Token Exchange (Back Channel)
B->>SP: GET /callback?code=AUTH_CODE&state=XYZ
SP->>SP: Verify 'state' matches (Anti-CSRF)
SP->>IdP: POST /token
Note right of SP: Payload: code, client_secret, code_verifier (PKCE)
IdP->>IdP: Validate code & verifier
IdP-->>SP: 200 OK (Access Token, ID Token)
Note over U, IdP: Phase 4: Secure Session Establishment
SP->>SP: Validate ID Token Signature
SP->>SP: Create Local Session
SP-->>B: 302 Redirect to /dashboard
Note right of B: Set-Cookie: session_id=abc, HttpOnly, Secure, SameSite=Lax
Note over U, IdP: Phase 5: Authenticated Access
B->>SP: GET /dashboard
Note right of B: Cookie: session_id=abc
SP->>SP: Lookup Session -> Retrieve Access Token
SP-->>B: Return Protected Content
The anti-CSRF mechanism is to validate the state parameter in the redirect from the IdP. Also setting a SameSite=Lax, HttpOnly, Secure on the session cookie prevents CSRF attacks from cross-site subrequests. Aim is to not save the token or refresh token anywhere in the browser, where an attacker might be able to access it and prevent interfering with the sign in process though cryptographic mechanisms.
Cookies
I already mentioned the Cookie attributes SameSite, HttpOnly and Secure in the authentication section. Here I want to explain the settings and their impact on the CSRF protection:
SameSite- Defined weather the cookie should be sent with cross-site requests.Lax- Allows cookies to be sent with cross-site subrequests, but only for same-site requests or cross site requests under following conditions:- The request is a top level navigation, this excludes requests made e.g. with
fetch()orXMLHttpRequestor requests from sub resources like images, stylesheets or scripts. Included would be requests made by interactions of the user, e.g. clicking a link or submitting a form. - The request uses a safe method, e.g.
GETorHEAD.
- The request is a top level navigation, this excludes requests made e.g. with
Strict- Does not allow the cookie to be sent with cross-site subrequests.None- Disables the SameSite restriction.
HttpOnly- Prevents client-side JavaScript from accessing the cookie, e.g. via thedocument.cookieAPI. A Cookie created with this attribute will still be sent with requests made with JavaScript initiated requests, e.g.fetch()orXMLHttpRequest.send()Secure- Indicates that the cookie is sent to the server only when a request is made with thehttps:scheme (except on localhost), and therefore, is more resistant to man-in-the-middle attacks.Max-Age- Indicates the number of seconds until the cookie expires. A zero or negative number will expire the cookie immediately. If bothExpiresandMax-Ageare set,Max-Agehas precedence.
Headers
Http headers are one of the most important security mechanisms, because they allow the server to set boundaries for their clients, in which they have to operate. This is especially important for CSRF attacks, but alos helps against man-in-the-middle attacks, cross-site scripting (XSS) and many others.
| Header | Description |
|---|---|
| Security | |
Content-Security-Policy (CSP) | Controls resources the user agent is allowed to load for a given page based on their URI. You can control different sources. |
Content-Security-Policy-Report-Only | Allows web developers to experiment with policies by monitoring, but not enforcing, their effects. These violation reports consist of JSON documents sent via an HTTP POST request to the specified URI. |
Cross-Origin-Embedder-Policy (COEP) | Allows a server to declare an embedder policy for a given document. |
Cross-Origin-Opener-Policy (COOP) | Allows a website to control whether a new top-level document, opened using Window.open() or by navigating to a new page, is opened in the same browsing context group (BCG) or in a new browsing context group.When opened in a new BCG, any references between the new document and its opener are severed, and the new document may be process-isolated from its opener. This ensures that potential attackers can't open your documents with Window.open() and then use the returned value to access its global object, and thereby prevents a set of cross-origin attacks referred to as XS-Leaks. |
Cross-Origin-Resource-Policy (CORP) | Prevents other domains from reading the response of the resources to which this header is applied. See also CORP explainer article. |
Permissions-Policy | Provides a mechanism to allow and deny the use of browser features in a website's own frame, and in <iframe>s that it embeds.<directive>=<allowList> |
Strict-Transport-Security (HSTS) | Force communication using HTTPS instead of HTTP (redirect) |
Upgrade-Insecure-Requests | Sends a signal to the server expressing the client's preference for an encrypted and authenticated response, and that it can successfully handle the upgrade-insecure-requests directive.Example: Upgrade-Insecure-Requests: 1 |
X-Content-Type-Options | Disables MIME sniffing and forces browser to use the type given in Content-Type. The value can be no-sniff |
X-Frame-Options (XFO) | Indicates whether a browser should be allowed to render a page in a <frame>, <iframe>, <embed> or <object>. |
X-Permitted-Cross-Domain-Policies | A cross-domain policy file may grant clients, such as Adobe Acrobat or Apache Flex (among others), permission to handle data across domains that would otherwise be restricted due to the Same-Origin Policy. The X-Permitted-Cross-Domain-Policies header overrides such policy files so that clients still block unwanted requests. |
X-Powered-By | May be set by hosting environments or other frameworks and contains information about them while not providing any usefulness to the application or its visitors. Unset this header to avoid exposing potential vulnerabilities. |
X-XSS-Protection | Enables cross-site scripting filtering. |
Angular Specifics
In Angular we, for strict csp settings, have to differentiate between build targets (static / server) and if we want to render on the client or on the server.
Hash based solution
In static build applications (all pages pre-rendered) we have to option for auto csp. This is done by setting angular.json build.options.security
"architect": {
"build": {
"builder": "@angular/build:application",
"options": {
"outputMode": "static",
"security": {
"autoCsp": true
}
},
}
}
The autoCsp option generates sha checksums for every resource while building (instead of a nonce based solution). This requires all resource loaded over a directive be known at build time. For some pages this is possible, for most usecases this may be hard to realize. For this cases you can use the nonce based solution.
Nonce based solution
In server and client side rendered applications (not static / prerendered), we can if we don't know all resources at build time, use a nonce based solution. In this case we have to supply a nonce per request (to an index.html). The nonce must be unique for every request.
In SSR / SSG applications we have to set the nonce manually. This can be done by using the ngCspNonce directive or providing the CSP_NONCE.
What of both is possible depends on the application. The nonce has to change for every request, so in case of dynamically rendered content we have to normally replace a nonce-placeholder with the actual nonce while rendering. This can be done by a angular SSR node instance or e.g. an NGinx proxy.