在Angular中动态编译的延迟加载动态路由,导致“不安全评估”错误

问题描述

应用内容安全策略后,在角度应用程序的index.html文件中,该应用程序显示“ unsafe-eval”控制台错误,如下所示-

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)

错误是由于在尝试动态构建模块时使用 Compiler 类中的 compileModuleAsync()方法引起的。

如果我不使用内容安全策略,则该应用程序可以正常工作,并且不会出现此类控制台错误。以下是政策详细信息-

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

根据调用堆栈的观察,Angular Framework的以下函数部分使用new Function()表达式并导致安全问题-

 /**
     * 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);
    }

这是routes.json,我正在其中尝试构建用loadChildren编写的配置-

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

下面是构建模块的代码-

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;
      })
    );
  }

此外,我正在为此编译器使用JitCompilerFactory-

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

请让我知道其他细节。任何建议都会很有帮助。

下面是stackblitz的链接,此问题在此可重现 https://stackblitz.com/github/HimanshuGoel/unsafe-eval-issue?file=src%2Findex.html

enter image description here

如果我删除此CSP,它将正确呈现-

enter image description here

解决方法

不幸的是,没有直接的解决方法。角JIT编译器需要使用new Function,并且要生成动态模块,需要JIT编译器。

因此,您有两个选择,将unsafe-eval添加为内容源:

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

或者返回绘图板,重新评估您对动态模块的需求。通常,建议不要使用JIT,因为它会增加大小并降低速度。例如,即使在ng serve模式下,最新的角度版本也默认使用AOT。

,

此问题的原因似乎是current Angular deficiency

这是问题的minimalistic reproduction。我们需要做的就是将CSP meta标签添加到标准stackblitz应用程序的页面标题上:

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

对CSP的支持将由Webpack configuration

提供

webpack能够向其加载的所有脚本中添加nonce

但是,这是not currently supported by angular

提前(AOT) 编译(aka ng build --prod)分离出所有JavaScript代码 从index.html文件。不幸的是,没有处理CSS 因为所有组件中的整洁和样式都保持内联(有关跟踪,请参见this ticket)。因此,我们不得不忍受不愉快的style-src “不安全内联”。

对于脚本,如果需要,还需要'unsafe-inline' 插件正常工作。 angular/angular#26152将会有一种方法 虽然:基于随机数的CSP与严格动态的结合 指示。因此,如果受随机数信任的脚本会创建一个新 脚本在运行时,该新脚本也将被视为合法脚本。

因此,按照Angular团队的建议,当前使用CSP标头的唯一方法是使用'unsafe-inline'并进行一些重构(即,不使用延迟加载的模块???恐怖...)