March 4, 2023

Angular for Pentesters 1

Note: I originally published this under the web page of one of my previous employers. Since then, the company was acquired and their page was taken down. I decided to re-post this here as it is one of my most referenced posts, and the content is still applicable.

The web is a playground of ideas (both good and bad), technologies, bugs, and well, a LOT of fancy JavaScript frameworks. One of the most popular web frameworks today is Angular, a platform that simplifies the development of front-end web applications through a set of prescriptive patterns. As an application security professional, learning the fundamentals of this framework can give you an advantage during the initial phases of an application security assessment.

This is the first of two blog posts that aim to help penetration testers and application security researchers in conducting a more in-depth analysis of Angular based applications. The first part will introduce you to the building blocks of AngularJS and Angular. We will highlight how each concept can help guide your security assessment. Then, in the second part, we will show some ways in which we can dynamically debug Angular code from the browser console. With that knowledge in hand, we can start conducting a more in-depth analysis of Angular based applications.

Note that this is not a blog about “How to Exploit XSS or XYZ Vulnerability in Angular.” There are plenty of articles dedicated to Angular specific vulnerabilities and quick ways to find them. Instead, the goal of this blog post is to understand how Angular applications work, common patterns, and ways to dynamically debug Angular from an application security perspective.

Before we continue, note that we will use Typescript for Angular 2+ in our sample code, as most Angular applications are written in that language. However, when the code is rendered in the browser, you will see the transpiled JavaScript. When applications are transpiled from Typescript to JavaScript, the resulting code is not the most readable or prettiest, so we will stick to Typescript for clarity and readability.

WHAT IS ANGULAR ANYWAYS?

Angular is one of those modern JavaScript frameworks that allow developers to create snappy and responsive websites with lots of fancy features. Angular also enables developers to separate front and back-end logic. This separation provides the ability to write front-end applications that use reusable APIs and microservices to pull data from. At the core of Angular is the ability to reflect changes in the application state in UI elements that are displayed to users “automagically.”

For instance, let’s say an application is supposed to pull Bitcoin prices from a server every 60 seconds. If the application were using jQuery, the front-end code would have to manipulate the DOM every time the price changed to reflect the price update on the page. In contrast, applications that use Angular can update the variable used for displaying the current price, say ``, and the new price would be shown in the page without having to manually set it by doing something like $('#price-field').text(newPrice);.

ANGULAR VERSIONS GALORE

When you start looking into Angular (either as developer, pentester, or application security researcher), one of the first things you may notice is the many versions of Angular that have been released in a relatively short period. However, we could say that there are only 2 major versions of Angular. Angular 1.x (or AngularJS) and Angular 2+ (or Angular). Applications using AngularJS rely on ECMAScript 5 (ES5) for the most part; so in a way, it is limited and often is not great at taking advantage of modern JavaScript features since ES6, such as classes, arrow functions, etc.

Officially, Angular versions 1.x are referenced as AngularJS. Versions 2 and above are referenced as Angular. We will try to specify when a concept or sample applies to only one version, but know that when we say “Angular,” we mean both Angular 1.X (AngularJS) and Angular 2+ (Angular).

Angular 1.X applications are built on 4 different constructs: Modules, Controllers, Services and Directives.

  • Modules can be thought of as namespaces.
  • Controllers are functions attached to DOM objects, and they control the behavior UI elements through functions that update variables scoped to those elements.
  • Services are in a way the business layer of Angular applications, as they hold functions that work directly with data.
  • Directives can be used to create custom HTML elements and attributes that can be added to any view with specific functionality attached to them. For instance, a directive may create the element <price-console /> to display the price of Bitcoin on the page.

At a high level, the relation between these concepts may look something like this:

High-level overview of Angular 1

When Angular 2 came out, patterns significantly changed to take advantage of the object-oriented features of ES6. To an extent, controllers and directives fused into what Angular calls components.

  • Like controllers, components control the behavior of DOM elements on a page; however, components often use templates to create custom reusable HTML elements that have their own scope. Additionally, whereas AngularJS directives and controllers are defined as functions with variables and closures available to the view, components are built as JavaScript classes.

At a high level, an Angular 2+ application looks something like this:

High-level overview of Angular 1

But what about version 4, 5 and 6? And what happened to version 3? We could say that those versions are, for the most part, improvements on Angular 2, but the patterns are mostly the same. Interestingly enough, Angular 3 never existed.

The official Angular guide contains an excellent resource that explains the main differences between Angular and AngularJS here.

Now let’s take a more detailed look at some Angular core concepts that will allow us to get a better understanding of how Angular applications are structured at a high level. As we go through each concept, we will highlight how each approach can help us along our assessment work.

DEEPER INTO ANGULAR CONCEPTS

The scope, man, it’s all in the scope

At the core of Angular 1.x is the concept of the scope. Every application is built as a series of controllers, and each component communicates changes on the page via its scope. Each controller can add, remove, and update variables available to views through their own $scope variables. Going back to the Bitcoin price example, a controller may update the price variable like this:

$scope.vm.btcPrice = data.btcPrice;

Note that Angular 2+ uses this instead of $scope for the same purposes, as the components are built as classes (not functions). The above example may look like the following in an Angular 2+ application:

this.price = data.price;

Angular monitors variables in the scope, listening for scope changes and triggering functions in response to those changes. For instance, elements in the page may be shown only when a variable such as $scope.vm.isAdmin is set to true. As an application security researcher, it may be a good idea to change isAdmin to true to see if you get access to any page elements that may be of interest. You might, for instance, be able to see an admin-only menu and features on the page.

Angular uses the HTML attributes ng-if (Angular 1.X) and ngIf (Angular 2+) for deciding what to show or not show in the page (this attribute shows or removes elements on the page, rather than hiding them or unhiding them). Changing isAdmin to true may allow you to see elements on the page that would otherwise be removed by Angular when rendering the page. At that point, you can start clicking around to see if any admin functionality is triggered that was not adequately protected in the server.

As a security researcher, you can examine the state of different variables by inspecting the scope. Moreover, a change in a scope variable could cascade a series of other changes and fire various functions. This can be done in a few different ways. To examine and manipulate the scope of a UI element, you can use an Angular browser inspector application such as Augary or ng-inspector. It is also possible to manipulate scope variables using the browser console, as we will see in the second part of this post.

Angular routes

The concept of routes applies to all versions of Angular. Angular decides what view and partial views to render on the page using router modules. For instance, when you navigate to _www.example.com/#/users_, Angular knows to render /views/users.html, which is controlled by a controller called UsersControllers, or a component such as UserComponent in Angular 2+.

Routes are typically declared in the same file that sets up the application module, often called app.ts or app.js. Finding the Angular routes would give you a list of directories and pages that you can and can’t access depending on your user rights. Moreover, the routes code can provide insight as to what roles can access which pages and directories.

In Angular 2+, the routes are often declared like this:

RouterModule.forRoot([
  { path: '', component: LoginComponent, pathMatch: 'full' },
  { path: '', component: HomeComponent, canActivate: [AuthGuard] },
  { path: 'login', component: LoginComponent, pathMatch: 'full' },
  { path: 'events', component: EventsComponent },
]),

The same would look like this in Angular 1.x:

$routeProvider.
   when("/dashboard", {
       templateUrl: 'views/home.html',
       controller: 'DashboardController',
       controllerAs: 'vm'
   }).
   when("/login", {
       templateUrl: 'views/login.html',
       controller: 'LoginController',
       controllerAs: 'vm'
   }).
   when('/admin', {
       templateUrl: 'views/adminpanel.html',
       controller: 'AdminPanelCtrl',
       css: 'styles/css/adminpanel.css',
       resolve: {
           "currentAuth": [
               "Auth", "AdminAuth", function (Auth, AdminAuth) {
                   var token = Auth.$getAuth().token;
                   return AdminAuth.isAdmin(token);
               }
           ]
       }
   }).
   otherwise({ redirectTo: '/' });

In this last instance, we are declaring the controller for each page and an alias for referencing that controller from the view. The above code also tells us that the admin/ panel route is resolved based on whether the AdminAuth.isAdmin(token); returns true or false. So, what can we do with that information? Our next step could be looking for and modifying the AdminAuth.isAdmin(token); function to always return true, in case we could get access to view elements meant to be seen only by admin users.

Services

Services often do most of the heavy lifting outside of the UI and DOM logic. Services are injected as singletons into components and controllers, so they usually serve as a way to share data between controllers. You will see most of the REST logic of an application handled by Angular services, so they can serve as a form of documentation for whatever API the application uses.

Not only do services hold references to interesting URLs that you may be unable to find through other methods (i.e., spidering or automated directory and file enumeration), they will also show you how to format API requests correctly. Indeed, one of the most powerful pieces of an in-depth assessment of Angular applications is conducting a review of services, as they can allow you to easily test for missing server-side access control vulnerabilities without needing the server-side source code (or swagger docs or some other sensitive/non-public set of info).

Let’s take a look at an Angular 2+ service:

export class UserService extends BaseService {
  constructor(private http: HttpClient, private router: Router) {
    super();
  }
  
  addUser(firstName: string, lastName: string, email: string) {   
    return this.http.post<any>('api/admin/adduser', {   
        first: firstName,   
        last: lastName,   
        email: email  
        })   
        .pipe(map((res: any) => {   
            // login successful if there's a jwt token in the response   
            if (res && res.code == 200) {   
                Notifications.show("User account created successfully.")   
            }   
        }));   
    }   
}

From the above we know that to create a new user we need to send a POST request to /api/admin/adduser with a body that is formatted like this: { "first":"George", "last":"Orwell", "email":"1984@example.com"}.

In Angular 1 a service looks slightly different. Let’s take a look at a service that connects to a Firebase database to obtain a list of users:

angular
  .module('myApp.admin.services')
  .factory('AdminService', ['$firebaseArray', '$firebaseObject', '$http', '$q',
    function ($firebaseArray, $firebaseObject, $http, $q) {
      var AdminService = {
          allUsers: {},
          getAllUsers: getAllUsers,
      }

// Get a reference to the Firebase  
var userProfilesRef = new Firebase("https://example.firebaseio.com/userprofiles/");

function getAllUsers() {   
    var deferred = $q.defer();   
    AdminService.allUsers = $firebaseObject(userProfilesRef);   
    Admin.allUsers.$loaded().then(function () {   
        deferred.resolve(AdminService.allUsers);   
    }, function (error) {   
        deferred.reject(error);   
    });   
    return deferred.promise;   
}   

The above service logic tells us that there is a Firebase endpoint at example.firebaseio.com/userprofiles/ that we may be able to call to get a list of users for the application.

Services can also hold interesting logic for communicating with serverless services, such as Firebase. In some instances, you may find that the entire data layer logic is in client-side code, in which case you may be able to enumerate a NoSQL database by manipulating service functions from the browser console. We will see how to do this in the second part of this post.

Controllers

As previously discussed, controllers are an Angular 1.x concept. Controllers are used by AngularJS applications to manipulate DOM elements and to set up listeners and variables available through the Angular $scope. For instance, a controller can hold variables that are used by the UI elements in the page, such as form values and dynamic URLs, and they can use services to submit those forms to some server running back-end code. Additionally, controllers can hold scope variables that, when changed, alter the behavior of an application. If you need to figure out what triggers specific changes on the page, look for controller logic.

Components

Angular dropped the use of controllers in favor of components since the release of Angular 2. A component controls a view or part of a view. Instead of building controllers as functions with closures, components are built as JavaScript classes that can hold markup templates for custom web elements, CSS styles restricted to a component, variables used by that component, and logic that controls the behavior of that component. Like AngularJS controllers, you’d want to examine components when trying to determine what variables are available to specific sections of a page, and what changes could occur when those variables change.

Authentication patterns

Most of the logic for authentication is often located in services dedicated to those purposes. For instance, the below is a common practice for Angular 2+ authentication services:

//...
export class AuthenticationService extends BaseService {
  constructor(private http: HttpClient, private router: Router) {
    super();
  }

//...  
    login(username: string, password: string) {  
        return this.http.post<any>('/api/auth/login', 
        { 
            username: username, 
            password: password, 
            email: email 
        }).pipe(map((res: any) => {  
            // successful if there's a jwt token in the response  
            if (res && res.auth_token) {  
            // store username and jwt token in local storage to keep user logged in between page refreshes  
                localStorage.setItem('currentUser', JSON.stringify({ username, auth_token: res.auth_token }));  
            }  
        }));  
    }

    logout() {   // remove user from local storage to log user out   
        localStorage.removeItem('currentUser');   this.router.navigateByUrl('/login');   
    }   
//...

The above tells us that on successful authentication attempts, a currentUser object is saved in localStorage. The logout function simply removes that object, therefore forcing the client to send another request to the API for a new token. This also tells us that sessions are likely not invalidated in the server upon logout.

Notice how the logout function also forces a redirection to the login page. What would happen if we manually added a currentUser in localStorage? Would the application allow you to see authenticated routes? These are just some things you could poke at after examining authentication services such as the one above.

A similar pattern exists in Angular 1.X, where authentication is checked on route changes.

run(["$rootScope", "$location", "$templateCache", '$http',  function ($rootScope, $location, $templateCache,$http) {
    $.material.init();
    $rootScope.$on("$routeChangeError", function (event, next, previous, error) {
        if (error === "AUTH_REQUIRED" || error.status === 401) {
            $location.path("/login");
        }
    });
}]);

Another common pattern is to use Angular interceptors for authorization. An HTTP interceptor is a function that runs every time a request is submitted and/or a server response is received. Two common interceptors are JWT interceptors and error interceptors. An error interceptor can be used to detect responses with code 401 (Unauthorized) to inform the client that the user must be logged out and redirected to the login page:

export class ErrorInterceptor implements HttpInterceptor {
  constructor(private authenticationService: AuthenticationService) { }

    intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {  
        return next.handle(request).pipe(catchError(err => {  
            if (err.status === 401) {  
                // if any response has a code of 401, logout the user and reload the page, forcing a redirect to the login page  
                this.authenticationService.logout();  
                location.reload(true);  
        }
        const error = err.error.message || err.statusText;   return throwError(error);   
        }))   
    }   
}

While the above interceptor is concerned with HTTP responses the job of the JWT interceptor is to include a JWT header token in all HTTP requests:

export class JwtInterceptor implements HttpInterceptor {
  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    // add authorization header with jwt token if available
    let currentUser = JSON.parse(localStorage.getItem('currentUser'));
    if (currentUser && currentUser.token) {
      request = request.clone({
        setHeaders: {
          Authorization: `Bearer ${currentUser.token}`
        }
      });
    }

        return next.handle(request);   
    }   
}   

Again, you can use this information to learn about how the application handles authentication and, if possible, try to change functions to see if you can notice something that may be of value to your assessment.

WRAP UP

Hopefully, this information gives you insight that can aid you in future security assessments of Angular applications. Understanding Angular can increase your intuition as you search for bugs in web applications. In the second part of this post, we will explore some ways in which we can use debugging features provided by Angular to test applications from the browser console dynamically.

© hex0punk 2023