by André Jaenisch
5th February 2023
CC BY 4.0 International
André Jaenisch Web-Development & -Consulting
Freelancer
Mastodon: @RyunoKi@layer8.space
Experience with Angular, TypeScript and Webpack
Interest in security and performance
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
$ 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
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
A quick look into the files generated by Angular before we move on
index.html
This is the app shell. Containing minimal HTML5 to load CSS and reference the above JavaScript files.
styles.[hash].css
At this point in time it is empty. The hash is generated by Webpack
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
polyfills.[hash].js
Contains extensions to the browser runtime for things Angular expects like Zone, certain Promise features or fetch
main.[hash].js
Mainly your code + webpack boilerplate for RxJS, Angular template parser
// 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 { }
Partial interface
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());
$ ng generate component page-not-found
$ ng generate component speaker # To be protected
$ ng generate component slides
No changes on build (tree-shaking)
// 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 */
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) {
// 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 */
app.component.html
{{ title }} app is running
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();
}
// 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 */
$ 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 */
// 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 { }
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 |
Generate a new guard with the CLI.
$ ng g guard CanActivateSpeaker
Use proper Permissions implementation below
// 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 {}
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).
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
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
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!
Unless otherwise noted the presentation is licensed under Creative Commons Attribution 4.0 International
Thank you