From 84ed3392331a71a3816e8e2e6f0341122a1f99cd Mon Sep 17 00:00:00 2001 From: Javi Velasco Date: Wed, 3 Nov 2021 12:57:34 +0100 Subject: [PATCH] Update eval checks Co-authored-by: Tobias Koppers --- .../webpack/plugins/middleware-plugin.ts | 142 +++++++++++++++--- packages/next/server/web/sandbox/sandbox.ts | 28 +++- 2 files changed, 150 insertions(+), 20 deletions(-) diff --git a/packages/next/build/webpack/plugins/middleware-plugin.ts b/packages/next/build/webpack/plugins/middleware-plugin.ts index 4db5e3179589..579cfb8a230d 100644 --- a/packages/next/build/webpack/plugins/middleware-plugin.ts +++ b/packages/next/build/webpack/plugins/middleware-plugin.ts @@ -1,4 +1,4 @@ -import { webpack, sources } from 'next/dist/compiled/webpack/webpack' +import { webpack, sources, webpack5 } from 'next/dist/compiled/webpack/webpack' import { getMiddlewareRegex } from '../../../shared/lib/router/utils' import { getSortedRoutes } from '../../../shared/lib/router/utils' import { @@ -39,7 +39,7 @@ export default class MiddlewarePlugin { } createAssets( - compilation: any, + compilation: webpack5.Compilation, assets: any, envPerRoute: Map ) { @@ -52,6 +52,7 @@ export default class MiddlewarePlugin { } for (const entrypoint of entrypoints.values()) { + if (!entrypoint.name) continue const result = MIDDLEWARE_FULL_ROUTE_REGEX.exec(entrypoint.name) const ssrEntryInfo = ssrEntries.get(entrypoint.name) @@ -111,19 +112,21 @@ export default class MiddlewarePlugin { ) } - apply(compiler: webpack.Compiler) { + apply(compiler: webpack5.Compiler) { + const { dev } = this + const wp = compiler.webpack compiler.hooks.compilation.tap( PLUGIN_NAME, (compilation, { normalModuleFactory }) => { const envPerRoute = new Map() - compilation.hooks.finishModules.tap(PLUGIN_NAME, () => { + compilation.hooks.afterOptimizeModules.tap(PLUGIN_NAME, () => { const { moduleGraph } = compilation as any envPerRoute.clear() for (const [name, info] of compilation.entries) { if (name.match(MIDDLEWARE_ROUTE)) { - const middlewareEntries = new Set() + const middlewareEntries = new Set() const env = new Set() const addEntriesFromDependency = (dep: any) => { @@ -133,19 +136,41 @@ export default class MiddlewarePlugin { } } + const runtime = wp.util.runtime.getEntryRuntime(compilation, name) + info.dependencies.forEach(addEntriesFromDependency) info.includeDependencies.forEach(addEntriesFromDependency) const queue = new Set(middlewareEntries) for (const module of queue) { - const { buildInfo } = module as any - if (buildInfo?.usingIndirectEval) { - // @ts-ignore TODO: Remove ignore when webpack 5 is stable - const error = new webpack.WebpackError( - `\`eval\` not allowed in Middleware ${name}` + const { buildInfo } = module + if ( + !dev && + buildInfo && + isUsedByExports({ + module, + moduleGraph, + runtime, + usedByExports: buildInfo.usingIndirectEval, + }) + ) { + if ( + /node_modules[\\/]regenerator-runtime[\\/]runtime\.js/.test( + module.identifier() + ) + ) + continue + const error = new wp.WebpackError( + `Dynamic Code Evaluation (e. g. 'eval', 'new Function') not allowed in Middleware ${name}${ + typeof buildInfo.usingIndirectEval !== 'boolean' + ? `\nUsed by ${Array.from( + buildInfo.usingIndirectEval + ).join(', ')}` + : '' + }` ) error.module = module - compilation.warnings.push(error) + compilation.errors.push(error) } if (buildInfo?.nextUsedEnvVars !== undefined) { @@ -167,19 +192,82 @@ export default class MiddlewarePlugin { } }) - const handler = (parser: any) => { - const flagModule = () => { - parser.state.module.buildInfo.usingIndirectEval = true + const handler = (parser: webpack5.javascript.JavascriptParser) => { + const wrapExpression = (expr: any) => { + if (dev) { + const dep1 = new wp.dependencies.ConstDependency( + '__next_eval__(function() { return ', + expr.range[0] + ) + dep1.loc = expr.loc + parser.state.module.addPresentationalDependency(dep1) + const dep2 = new wp.dependencies.ConstDependency( + '})', + expr.range[1] + ) + dep2.loc = expr.loc + parser.state.module.addPresentationalDependency(dep2) + } + expressionHandler() + return true + } + + const flagModule = ( + usedByExports: boolean | Set | undefined + ) => { + if (usedByExports === undefined) usedByExports = true + const old = parser.state.module.buildInfo.usingIndirectEval + if (old === true || usedByExports === false) return + if (!old || usedByExports === true) { + parser.state.module.buildInfo.usingIndirectEval = usedByExports + return + } + const set = new Set(old) + for (const item of usedByExports) { + set.add(item) + } + parser.state.module.buildInfo.usingIndirectEval = set + } + + const expressionHandler = () => { + wp.optimize.InnerGraph.onUsage(parser.state, flagModule) + } + + const ignore = () => { + return true } - parser.hooks.expression.for('eval').tap(PLUGIN_NAME, flagModule) - parser.hooks.expression.for('Function').tap(PLUGIN_NAME, flagModule) + // wrapping + parser.hooks.call.for('eval').tap(PLUGIN_NAME, wrapExpression) + parser.hooks.call.for('global.eval').tap(PLUGIN_NAME, wrapExpression) + parser.hooks.call.for('Function').tap(PLUGIN_NAME, wrapExpression) + parser.hooks.call + .for('global.Function') + .tap(PLUGIN_NAME, wrapExpression) + parser.hooks.new.for('Function').tap(PLUGIN_NAME, wrapExpression) + parser.hooks.new + .for('global.Function') + .tap(PLUGIN_NAME, wrapExpression) + + // fallbacks + parser.hooks.expression + .for('eval') + .tap(PLUGIN_NAME, expressionHandler) + parser.hooks.expression + .for('Function') + .tap(PLUGIN_NAME, expressionHandler) + parser.hooks.expression + .for('Function.prototype') + .tap(PLUGIN_NAME, ignore) parser.hooks.expression .for('global.eval') - .tap(PLUGIN_NAME, flagModule) + .tap(PLUGIN_NAME, expressionHandler) parser.hooks.expression .for('global.Function') - .tap(PLUGIN_NAME, flagModule) + .tap(PLUGIN_NAME, expressionHandler) + parser.hooks.expression + .for('global.Function.prototype') + .tap(PLUGIN_NAME, ignore) const memberChainHandler = (_expr: any, members: string[]) => { if ( @@ -237,3 +325,21 @@ export default class MiddlewarePlugin { ) } } + +function isUsedByExports(args: { + module: webpack5.Module + moduleGraph: webpack5.ModuleGraph + runtime: any + usedByExports: boolean | Set | undefined +}): boolean { + const { moduleGraph, runtime, module, usedByExports } = args + if (usedByExports === undefined) return false + if (typeof usedByExports === 'boolean') return usedByExports + const exportsInfo = moduleGraph.getExportsInfo(module) + const wp = webpack as unknown as typeof webpack5 + for (const exportName of usedByExports) { + if (exportsInfo.getUsed(exportName, runtime) !== wp.UsageState.Unused) + return true + } + return false +} diff --git a/packages/next/server/web/sandbox/sandbox.ts b/packages/next/server/web/sandbox/sandbox.ts index 9b2dd7d0e92f..931a8ad08eb2 100644 --- a/packages/next/server/web/sandbox/sandbox.ts +++ b/packages/next/server/web/sandbox/sandbox.ts @@ -13,6 +13,7 @@ let cache: paths: Map require: Map sandbox: vm.Context + warnedEvals: Set } | undefined @@ -39,6 +40,7 @@ export async function run(params: { }): Promise { if (cache === undefined) { const context: { [key: string]: any } = { + __next_eval__, _ENTRIES: {}, atob: polyfills.atob, Blob, @@ -88,11 +90,20 @@ export async function run(params: { cache = { context, + paths: new Map(), require: new Map([ [require.resolve('next/dist/compiled/cookie'), { exports: cookie }], ]), - paths: new Map(), - sandbox: vm.createContext(context), + sandbox: vm.createContext(context, { + codeGeneration: + process.env.NODE_ENV === 'production' + ? { + strings: false, + wasm: false, + } + : undefined, + }), + warnedEvals: new Set(), } loadDependencies(cache.sandbox, [ @@ -220,3 +231,16 @@ function getFetchURL(input: RequestInfo, headers: NodeHeaders = {}): string { function isRequestLike(obj: unknown): obj is Request { return Boolean(obj && typeof obj === 'object' && 'url' in obj) } + +function __next_eval__(fn: Function) { + const key = fn.toString() + if (!cache?.warnedEvals.has(key)) { + console.warn( + new Error( + `Dynamic Code Evaluation (e. g. 'eval', 'new Function') not allowed in Middleware` + ).stack + ) + cache?.warnedEvals.add(key) + } + return fn() +}