Secure by Accident

by André Jaenisch

5th February 2023

CC BY 4.0 International

00:15 (00:15 total)

It's a me!

Photo of me

André Jaenisch Web-Development & -Consulting

Freelancer

Mastodon: @RyunoKi@layer8.space

00:10 (00:25 total) Since 2023 I switched into freelancing career.

Whom this talk is for?

Angular logo

Experience with Angular, TypeScript and Webpack

Interest in security and performance

00:20 (00:45 total)

Point of view of a security person

What you will learn today

  1. Steps to reproduce
  2. Interpret webpack build
  3. Enumerating child routes
  4. Protecting routes with guards
  5. Code splitting by route in Angular
00:15 (01:00 total)

Background story

Profile picture of percidae

Idea sponsored by percidae

Chat in fourth quarter of 2022 about the structure of Angular builds

Learned about what information should better not be included without prior authentication and authorisation

00:30 (01:30 total)

Before we begin

Ace Attorney sprite
Angular is used as an example here. The following applies to other frameworks as well. It cannot be handled on a framework level. The responsibility lies with the app developer. That's YOU.
00:30 (02:00 total)

Dependencies

  • Angular v15.0.0
  • Prettier v2.8.3
00:15 (02:15 total)
$ npx ng new fosdem
$ cd fosdem
# Because we are talking about Angular here,
# let's fix the build
$ npm install @types/node
$ npm install merge-descriptors
$ npm install license-webpack-plugin
$ npm run build
          
00:30 (02:45 total)

Right now, we don't change anything on any file

$ tree dist
dist/
└── fosdem
    ├── 3rdpartylicenses.txt
    ├── favicon.ico
    ├── index.html
    ├── main.91ffdd2e12df072d.js
    ├── polyfills.451f8e5f75f526a0.js
    ├── runtime.2ad8f73bb7b39640.js
    └── styles.ef46db3751d8e999.css

1 directory, 7 files
00:30 (03:15 total)

Anatomy of a webpack build

A quick look into the files generated by Angular before we move on

00:15 (03:30 total)

index.html

This is the app shell. Containing minimal HTML5 to load CSS and reference the above JavaScript files.

00:15 (03:45 total)

styles.[hash].css

At this point in time it is empty. The hash is generated by Webpack

00:15 (04:00 total)

runtime.[hash].js

Contains the Angular runtime that parses Angular templates and manages all the dependency injection and other magic of the framework for you

00:15 (04:15 total)

polyfills.[hash].js

Contains extensions to the browser runtime for things Angular expects like Zone, certain Promise features or fetch

00:15 (04:30 total)

main.[hash].js

Mainly your code + webpack boilerplate for RxJS, Angular template parser

00:15 (04:45 total)

The Case

00:15 (05:00 total)

Router

// src/app/app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';

const routes: Routes = [];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }
          
00:30 (05:30 total)

Route definition

Partial interface

03:00 (08:30 total)

Before any changes

Large chunk of boilerplate bloat before starting with implementation

function FR(e, t) {
1 & e && (C(0, "pre"), Q(1, "ng generate component xyz"), I());
          
00:30 (09:00 total)

Generating components

$ ng generate component page-not-found
$ ng generate component speaker # To be protected
$ ng generate component slides

No changes on build (tree-shaking)

00:15 (09:15 total)

Declaring routes

// src/app/app-routing.module.ts
/* Imports from above plus additionally */
import { SlidesComponent } from './slides/slides.component';
import { SpeakerComponent } from './speaker/speaker.component';

const routes: Routes = [
  { path: 'slides', component: SlidesComponent },
  { path: 'speaker', component: SpeakerComponent },
];

/* Continue as above */
00:30 (09:45 total)

Declaring routes (continued)

return new (t || ui)();
      }),
        (ui.ɵcmp = Xn({
          type: ui,
          selectors: [["app-slides"]],
          decls: 2,
          vars: 0,
          template: function (t, n) {
            1 & t && (C(0, "p"), Q(1, "slides works!"), I());
          },
        }));
      class li {}
      (li.ɵfac = function (t) {
        return new (t || li)();
      }),
        (li.ɵcmp = Xn({
          type: li,
          selectors: [["app-speaker"]],
          decls: 2,
          vars: 0,
          template: function (t, n) {
            1 & t && (C(0, "p"), Q(1, "speaker works!"), I())
          },
        }));
      const kR = [
        { path: "slides", component: ui },
        { path: "speaker", component: li },
      ];
      class Wr {}
      function LR(e, t) {
          
00:30 (10:15 total)

Adding catch all routes

// src/app/app-routing.module.ts
/* Imports from above plus additionally */
import { PageNotFoundComponent } from './page-not-found/page-not-found.component';

const routes: Routes = [
  { path: 'slides', component: SlidesComponent },
  { path: 'speaker', component: SpeakerComponent },
  { path: '', redirectTo: '/slides', pathMatch: 'full' },
  { path: '**', component: PageNotFoundComponent },
];

/* Continue as above */
00:30 (10:45 total)

Replace app.component.html


{{ title }} app is running

  • Slides
  • Speaker
00:30 (11:15 total)

Child routes without guard

Add FormsModule to the imports in the AppModule

Usually go with Reactive forms for more advanced behaviour

// src/app/speaker/speaker.component.ts
class Auth {
  public password = '';
}

/* @Component decorator here */
export class SpeakerComponent {
  public model = new Auth();
}
00:30 (11:15 total)

Child routes without guard (Continued)

00:30 (11:45 total)

Child routes without guard (Continued)

// src/app/app-routing.module.ts
/* Same as before */
const routes: Routes = [
  { path: 'slides', component: SlidesComponent },
  {
    path: 'speaker',
    component: SpeakerComponent,
    children: [{ path: 'slides', component: SlidesComponent }]
  },
  { path: '', redirectTo: '/slides', pathMatch: 'full' },
  { path: '**', component: PageNotFoundComponent },
];
/* Keep as before */
            
00:30 (12:15 total)

Lazy-loading child routes

$ ng generate module speaker --route speaker --module app.module
// src/app/app-routing.module.ts
/* Remove SpeakerComponent import */
const routes: Routes = [
  {
    path: 'speaker',
    loadChildren: () => import('./speaker/speaker.module').then(m => m.SpeakerModule)
  }
];
/* Continue as above */
          
00:30 (12:45 total)

Lazy-loading child routes (Continued)

// src/app/speaker/speaker-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';

import { SlidesComponent } from '../slides/slides.component';

import { SpeakerComponent } from './speaker.component';

const routes: Routes = [
  {
    path: '',
    component: SpeakerComponent,
    children: [{ path: 'slides', component: SlidesComponent }]
  }
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule]
})
export class SpeakerRoutingModule { }
00:30 (13:15 total)

Lazy-loading child routes (Continued)

Remove SpeakerComponent from src/app/app.module.ts

Observe new build artifacts being generated


Lazy Chunk Files | Names                  |  Raw Size
[hash].[hash].js | speaker-speaker-module |   5.83 kB |
            
00:30 (13:45 total)

Writing a guard

Generate a new guard with the CLI.

$ ng g guard CanActivateSpeaker

Use proper Permissions implementation below

00:30 (14:15 total)

Child route with guard

// src/app/app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { CanActivateSpeakerGuard, Permissions, UserToken } from './can-activate-speaker.guard';

const routes: Routes = [
  {
    path: 'speaker',
    canActivate: [CanActivateSpeakerGuard],
    loadChildren: () => import('./speaker/speaker.module').then(m => m.SpeakerModule)
  },
  /* Same as before */
];

/* Added provider! */
@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule],
  providers: [CanActivateSpeakerGuard, Permissions, UserToken]
})
export class AppRoutingModule {}
          
00:30 (14:45 total)

The Remedy

Embarassed Phoenix Wright

This is the part where I would like to use Webpack's named chunks.

But Angular does not support them.

But you can turn on Named Chunks in angular.json (thereby giving away your directory structure).

00:15 (15:00 total)

The Remedy (Continued)

The idea being to protect that specific chunk with HTTP headers.

Speaking of, the Security guide on Content Security Policy declares that at the very minimum Angular requires

default-src 'self'; style-src 'self' 'unsafe-inline';

You can still apply SHA hashes or nonces with some effort for protection

00:15 (15:15 total)

The Remedy (Continued)

If you validate the password, don't list forbidden passwords in Angular. Otherwise these entries will be excluded from Credential stuffing attacks

In a similar vein load password classes (length, special characters) asynchronously to make criminals' life harder

Best to check passwords on the server and display validation errors from the response

00:30 (15:45 total)

The Remedy (Continued)

At least guarding paths protect them from being listed but not from being cURLed. Therefore they can still be enumerated for attacks.

Load more data after a successful login. Check the Authorization header on the server!

00:30 (16:15 total)

What have you learned today?

  • Understanding webpack bundles
  • Named chunks in Angular builds
  • Content Security Policy options
  • More options to secure static files
00:30 (16:45 total)

Image credits

Unless otherwise noted the presentation is licensed under Creative Commons Attribution 4.0 International

  • Profile picture of percidae by percidae (with friendly permission, all rights reserved)
  • Angular logo by Angular under CC BY 4.0
  • Ace Attorney Courthouse by Moydow licensed under CC BY-SA 3.0
  • Ace Attorney sprite and Ace Attorney sprite by Rojal licensed under CC BY-NC 4.0
00:30 (17:15 total)

Thank you

00:15 (17:30 total)