Dynamically compiled lazy loaded dynamic routes in Angular causing 'unsafe-eval' error

1.9k Views Asked by At

In the index.html file of the angular application after applying the Content Security Policy, the application is giving 'unsafe-eval' console error as below -

core.js:4442 ERROR Error: Uncaught (in promise): EvalError: Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of script in the following Content Security Policy directive: "default-src 'self'".

EvalError: Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of script in the following Content Security Policy directive: "default-src 'self'".

    at new Function (<anonymous>)
    at JitEvaluator.evaluateCode (compiler.js:6740)
    at JitEvaluator.evaluateStatements (compiler.js:6714)
    at CompilerFacadeImpl.jitExpression (compiler.js:19300)
    at CompilerFacadeImpl.compileNgModule (compiler.js:19238)
    at Function.get (core.js:25864)
    at getNgModuleDef (core.js:1853)
    at new NgModuleFactory$1 (core.js:24270)
    at Compiler_compileModuleSync__POST_R3__ (core.js:27085)
    at Compiler_compileModuleAsync__POST_R3__ [as compileModuleAsync] (core.js:27090)
    at resolvePromise (zone-evergreen.js:798)
    at resolvePromise (zone-evergreen.js:750)
    at zone-evergreen.js:860
    at ZoneDelegate.invokeTask (zone-evergreen.js:399)
    at Object.onInvokeTask (core.js:27483)
    at ZoneDelegate.invokeTask (zone-evergreen.js:398)
    at Zone.runTask (zone-evergreen.js:167)
    at drainMicroTaskQueue (zone-evergreen.js:569)

This error is getting caused by using the compileModuleAsync() method from Compiler class as I am trying to build the module dynamically.

If I don't use the Content Security Policy, then the application works fine and it doesn't give such console error. Below is the policy details -

<meta http-equiv="Content-Security-Policy" content="default-src 'self';" />

As per the observation from callstack, the below function part of Angular Framework uses new Function() expression and leads to security issue -

 /**
     * Evaluate a piece of JIT generated code.
     * @param sourceUrl The URL of this generated code.
     * @param ctx A context object that contains an AST of the code to be evaluated.
     * @param vars A map containing the names and values of variables that the evaluated code might
     * reference.
     * @param createSourceMap If true then create a source-map for the generated code and include it
     * inline as a source-map comment.
     * @returns The result of evaluating the code.
     */
    evaluateCode(sourceUrl, ctx, vars, createSourceMap) {
        let fnBody = `"use strict";${ctx.toSource()}\n//# sourceURL=${sourceUrl}`;
        const fnArgNames = [];
        const fnArgValues = [];
        for (const argName in vars) {
            fnArgValues.push(vars[argName]);
            fnArgNames.push(argName);
        }
        if (createSourceMap) {
            // using `new Function(...)` generates a header, 1 line of no arguments, 2 lines otherwise
            // E.g. ```
            // function anonymous(a,b,c
            // /**/) { ... }```
            // We don't want to hard code this fact, so we auto detect it via an empty function first.
            const emptyFn = new Function(...fnArgNames.concat('return null;')).toString();
            const headerLines = emptyFn.slice(0, emptyFn.indexOf('return null;')).split('\n').length - 1;
            fnBody += `\n${ctx.toSourceMapGenerator(sourceUrl, headerLines).toJsComment()}`;
        }
        const fn = new Function(...fnArgNames.concat(fnBody));
        return this.executeFunction(fn, fnArgValues);
    }

This is the routes.json in which I am trying to build configuration written in the loadChildren -

{
      path: '',
      componentName: 'dummy',
      children: [
        {
          path: '',
          pathMatch: 'full',
          redirectTo: 'set-focus-action',
        },
        {
          path: 'set-focus-action',
          loadChildren: {
            routes: [
              {
                path: '',
                componentName: 'dynamicType1',
              },
            ],
          },
        },
      ],
    }

Below is the code to build the module -

private featureModule(loadChildren: string): Observable<Type<any>> {
    return this.getRoutes(loadChildren).pipe(
      switchMap((routesConfig) => {
        const module = NgModule(this.createFeatureModule(routesConfig))(
          class {}
        );
        return from(this.compiler.compileModuleAsync(module));
      }),
      map((m) => {
        return m.moduleType;
      })
    );
  }

Also, I am using JitCompilerFactory for this compiler -

{ provide: COMPILER_OPTIONS, useValue: {}, multi: true },
        {
          provide: CompilerFactory,
          useClass: JitCompilerFactory,
          deps: [COMPILER_OPTIONS],
        },
        {
          provide: Compiler,
          useFactory: createCompiler,
          deps: [CompilerFactory],
        }

Please let me know in-case any other details. Any suggestions would be really helpful.

Below is a link for stackblitz where this issue is getting reproducible https://stackblitz.com/github/HimanshuGoel/unsafe-eval-issue?file=src%2Findex.html

enter image description here

If I remove this CSP, it gets render correctly -

enter image description here

2

There are 2 best solutions below

0
On

There is unfortunately no direct way around it. The angular JIT compiler needs to use new Function, and to generate a dynamic module, you need the JIT compiler.

So you have two options, add unsafe-eval as content source:

<meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-eval';" />

Or re-evaluate your need for a dynamic module by heading back to the drawing board. In general it is advised to not use JIT at all, because of the size increase and speed reduction it brings. For instance the newest angular versions uses AOT by default, even in ng serve mode.

1
On

It seems the reason for this issue is a current Angular deficiency

This is a minimalistic reproduction of the issue. All we need is to add the CSP meta tag to the page head on the standard stackblitz app:

<meta http-equiv="Content-Security-Policy" content="default-src 'self';" />

Support for CSP would be provided by a Webpack configuration

webpack is capable of adding nonce to all scripts that it loads

however, this is not currently supported by angular:

Ahead-of-Time (AOT) compilation (aka ng build --prod) separates out all JavaScript code from the index.html file. Unfortunately, processing of the CSS is not as neat and styles remain inline in all the components (see this ticket for tracking). So, we have to put up with unpleasant style-src 'unsafe-inline'.

As for the scripts, 'unsafe-inline' is also required if we want plugins to work. There will be a way with angular/angular#26152 though: a combination of nonce-based CSP with strict-dynamic directive. Hence, if a script trusted with a nonce creates a new script at runtime, this new script will also be considered legitimate.

so as per team Angular's recommendation the only current way to use the CSP header is with 'unsafe-inline' and do some refactoring (i.e. not using lazy loaded modules??? the horror...)