From a87a5372c8863e8d50add6ac1eb427776f54c106 Mon Sep 17 00:00:00 2001 From: Seiya Nuta Date: Fri, 10 Jun 2022 13:26:34 +0900 Subject: [PATCH 1/2] [middleware] Warn if WASM code generation happened In Edge Middlewares, dynamic code execution is not available. Currently, we warn if eval / new Function is invoked in dev but don't warn another dynamic code exection in WebAssembly. This PR adds warnings for WebAssembly.compile and WebAssembly.instantiate with a buffer parameter invocations. Note that other methods that generate WASM dynamically such as WebAssembly.compileStreaming are not available in Edge Runtime so we don't cover them in this PR. --- errors/manifest.json | 4 ++ errors/middleware-dynamic-wasm-compilation.md | 27 ++++++++ .../webpack/plugins/middleware-plugin.ts | 62 +++++++++++++++++- packages/next/server/web/sandbox/context.ts | 43 ++++++++++++ .../middleware-dynamic-code/lib/square.wasm | Bin 0 -> 63 bytes .../middleware-dynamic-code/lib/wasm.js | 35 ++++++++++ .../middleware-dynamic-code/middleware.js | 27 ++++++++ .../middleware-dynamic-code/next.config.js | 6 ++ .../test/index.test.js | 59 +++++++++++++++-- .../index.test.ts | 58 ++++++++++++++-- .../middleware-with-dynamic-code/square.wasm | Bin 0 -> 63 bytes 11 files changed, 309 insertions(+), 12 deletions(-) create mode 100644 errors/middleware-dynamic-wasm-compilation.md create mode 100644 test/integration/middleware-dynamic-code/lib/square.wasm create mode 100644 test/integration/middleware-dynamic-code/lib/wasm.js create mode 100644 test/integration/middleware-dynamic-code/next.config.js create mode 100644 test/production/middleware-with-dynamic-code/square.wasm diff --git a/errors/manifest.json b/errors/manifest.json index 4205dc491e8f4..23129300371af 100644 --- a/errors/manifest.json +++ b/errors/manifest.json @@ -699,6 +699,10 @@ { "title": "get-initial-props-export", "path": "/errors/get-initial-props-export.md" + }, + { + "title": "middleware-dynamic-wasm-compilation", + "path": "/errors/middleware-dynamic-wasm-compilation.md" } ] } diff --git a/errors/middleware-dynamic-wasm-compilation.md b/errors/middleware-dynamic-wasm-compilation.md new file mode 100644 index 0000000000000..b5c5adfa8f07f --- /dev/null +++ b/errors/middleware-dynamic-wasm-compilation.md @@ -0,0 +1,27 @@ +# Dynamic WASM complication is not available in Middlewares + +#### Why This Error Occurred + +Compiling WASM binaries dynamically is not allowed in Middlewares. Specifically, +the following APIs are not supported: + +- `WebAssembly.compile` +- `WebAssembly.instantiate` with [a buffer parameter](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/instantiate#primary_overload_%E2%80%94_taking_wasm_binary_code) + +#### Possible Ways to Fix It + +Bundle your WASM binaries using `import`: + +```typescript +import { NextResponse } from 'next/server' +import squareWasm from './square.wasm?module' + +export default async function middleware() { + const m = await WebAssembly.instantiate(squareWasm) + const answer = m.exports.square(9) + + const response = NextResponse.next() + response.headers.set('x-square', answer.toString()) + return response +} +``` diff --git a/packages/next/build/webpack/plugins/middleware-plugin.ts b/packages/next/build/webpack/plugins/middleware-plugin.ts index bcf9ea94e061e..8fa4bbf2ab50d 100644 --- a/packages/next/build/webpack/plugins/middleware-plugin.ts +++ b/packages/next/build/webpack/plugins/middleware-plugin.ts @@ -138,6 +138,60 @@ function getCodeAnalizer(params: { return true } + /** + * This expression handler allows to wrap a WebAssembly.compile invocation with a + * function call where we can warn about WASM code generation not being allowed + * but actually execute the expression. + */ + const handleWrapWasmCompileExpression = (expr: any) => { + if (!isInMiddlewareLayer(parser)) { + return + } + + if (dev) { + const { ConstDependency } = wp.dependencies + const dep1 = new ConstDependency( + '__next_webassembly_compile__(function() { return ', + expr.range[0] + ) + dep1.loc = expr.loc + parser.state.module.addPresentationalDependency(dep1) + const dep2 = new ConstDependency('})', expr.range[1]) + dep2.loc = expr.loc + parser.state.module.addPresentationalDependency(dep2) + } + + handleExpression() + } + + /** + * This expression handler allows to wrap a WebAssembly.instatiate invocation with a + * function call where we can warn about WASM code generation not being allowed + * but actually execute the expression. + * + * Note that we don't update `usingIndirectEval`, i.e. we don't abort a production build + * since we can't determine statically if the first parameter is a module (legit use) or + * a buffer (dynamic code generation). + */ + const handleWrapWasmInstantiateExpression = (expr: any) => { + if (!isInMiddlewareLayer(parser)) { + return + } + + if (dev) { + const { ConstDependency } = wp.dependencies + const dep1 = new ConstDependency( + '__next_webassembly_instantiate__(function() { return ', + expr.range[0] + ) + dep1.loc = expr.loc + parser.state.module.addPresentationalDependency(dep1) + const dep2 = new ConstDependency('})', expr.range[1]) + dep2.loc = expr.loc + parser.state.module.addPresentationalDependency(dep2) + } + } + /** * For an expression this will check the graph to ensure it is being used * by exports. Then it will store in the module buildInfo a boolean to @@ -232,6 +286,12 @@ function getCodeAnalizer(params: { hooks.new.for(`${prefix}Function`).tap(NAME, handleWrapExpression) hooks.expression.for(`${prefix}eval`).tap(NAME, handleExpression) hooks.expression.for(`${prefix}Function`).tap(NAME, handleExpression) + hooks.call + .for(`${prefix}WebAssembly.compile`) + .tap(NAME, handleWrapWasmCompileExpression) + hooks.call + .for(`${prefix}WebAssembly.instantiate`) + .tap(NAME, handleWrapWasmInstantiateExpression) } hooks.new.for('Response').tap(NAME, handleNewResponseExpression) hooks.new.for('NextResponse').tap(NAME, handleNewResponseExpression) @@ -331,7 +391,7 @@ function getExtractMetadata(params: { } const error = new wp.WebpackError( - `Dynamic Code Evaluation (e. g. 'eval', 'new Function') not allowed in Middleware ${entryName}${ + `Dynamic Code Evaluation (e. g. 'eval', 'new Function', 'WebAssembly.compile') not allowed in Middleware ${entryName}${ typeof buildInfo.usingIndirectEval !== 'boolean' ? `\nUsed by ${Array.from(buildInfo.usingIndirectEval).join( ', ' diff --git a/packages/next/server/web/sandbox/context.ts b/packages/next/server/web/sandbox/context.ts index 2b4ea09ead45b..16424e477a22f 100644 --- a/packages/next/server/web/sandbox/context.ts +++ b/packages/next/server/web/sandbox/context.ts @@ -98,6 +98,7 @@ export async function getModuleContext(options: ModuleContextOptions) { */ async function createModuleContext(options: ModuleContextOptions) { const warnedEvals = new Set() + const warnedWasmCodegens = new Set() const wasm = await loadWasm(options.wasm) const runtime = new EdgeRuntime({ codeGeneration: @@ -121,6 +122,48 @@ async function createModuleContext(options: ModuleContextOptions) { return fn() } + context.__next_webassembly_compile__ = + function __next_webassembly_compile__(fn: Function) { + const key = fn.toString() + if (!warnedWasmCodegens.has(key)) { + const warning = new Error( + "Dynamic WASM code generation (e. g. 'WebAssembly.compile') not allowed in Middleware.\n" + + 'Learn More: https://nextjs.org/docs/messages/middleware-dynamic-wasm-compilation' + ) + warning.name = 'DynamicWasmCodeGenerationWarning' + Error.captureStackTrace(warning, __next_webassembly_compile__) + warnedWasmCodegens.add(key) + options.onWarning(warning) + } + return fn() + } + + context.__next_webassembly_instantiate__ = + async function __next_webassembly_instantiate__(fn: Function) { + const result = await fn() + + // If a buffer is given, WebAssembly.instantiate returns an object + // containing both a module and an instance while it returns only an + // instance if a WASM module is given. Utilize the fact to determine + // if the WASM code generation happens. + // + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/instantiate#primary_overload_%E2%80%94_taking_wasm_binary_code + const instantiatedFromBuffer = result.hasOwnProperty('module') + + const key = fn.toString() + if (instantiatedFromBuffer && !warnedWasmCodegens.has(key)) { + const warning = new Error( + "Dynamic WASM code generation ('WebAssembly.instantiate' with a buffer parameter) not allowed in Middleware.\n" + + 'Learn More: https://nextjs.org/docs/messages/middleware-dynamic-wasm-compilation' + ) + warning.name = 'DynamicWasmCodeGenerationWarning' + Error.captureStackTrace(warning, __next_webassembly_instantiate__) + warnedWasmCodegens.add(key) + options.onWarning(warning) + } + return result + } + const __fetch = context.fetch context.fetch = (input: RequestInfo, init: RequestInit = {}) => { init.headers = new Headers(init.headers ?? {}) diff --git a/test/integration/middleware-dynamic-code/lib/square.wasm b/test/integration/middleware-dynamic-code/lib/square.wasm new file mode 100644 index 0000000000000000000000000000000000000000..f836887dda8004a4f54c3897c1d349b676293119 GIT binary patch literal 63 zcmWN { const json = JSON.parse(res.headers.get('data')) await waitFor(500) expect(json.value).toEqual(100) - expect(output).toContain(DYNAMIC_CODE_ERROR) + expect(output).toContain(EVAL_ERROR) expect(output).toContain('DynamicCodeEvaluationWarning') expect(output).toContain('./middleware') // TODO check why that has a backslash on windows @@ -57,14 +60,62 @@ describe('Middleware usage of dynamic code evaluation', () => { const json = JSON.parse(res.headers.get('data')) await waitFor(500) expect(json.value).toEqual(100) - expect(output).not.toContain(DYNAMIC_CODE_ERROR) + expect(output).not.toContain('Dynamic Code Evaluation') }) it('does not has problems with eval in page or server code', async () => { const html = await renderViaHTTP(context.appPort, `/`) expect(html).toMatch(/>.*?100.*?and.*?100.*?<\//) await waitFor(500) - expect(output).not.toContain(DYNAMIC_CODE_ERROR) + expect(output).not.toContain('Dynamic Code Evaluation') + }) + + it('shows a warning when running WebAssembly.compile', async () => { + const res = await fetchViaHTTP( + context.appPort, + `/using-webassembly-compile` + ) + const json = JSON.parse(res.headers.get('data')) + await waitFor(500) + expect(json.value).toEqual(81) + expect(output).toContain(WASM_COMPILE_ERROR) + expect(output).toContain('DynamicWasmCodeGenerationWarning') + expect(output).toContain('./middleware') + expect(output).toMatch(/lib[\\/]wasm\.js/) + expect(output).toContain('usingWebAssemblyCompile') + expect(stripAnsi(output)).toContain( + 'await WebAssembly.compile(SQUARE_WASM_BUFFER)' + ) + }) + + it('shows a warning when running WebAssembly.instantiate w/ a buffer parameter', async () => { + const res = await fetchViaHTTP( + context.appPort, + `/using-webassembly-instantiate-with-buffer` + ) + const json = JSON.parse(res.headers.get('data')) + await waitFor(500) + expect(json.value).toEqual(81) + expect(output).toContain(WASM_INSTANTIATE_ERROR) + expect(output).toContain('DynamicWasmCodeGenerationWarning') + expect(output).toContain('./middleware') + expect(output).toMatch(/lib[\\/]wasm\.js/) + expect(output).toContain('usingWebAssemblyInstantiateWithBuffer') + expect(stripAnsi(output)).toContain( + 'await WebAssembly.instantiate(SQUARE_WASM_BUFFER, {})' + ) + }) + + it('does not show a warning when running WebAssembly.instantiate w/ a module parameter', async () => { + const res = await fetchViaHTTP( + context.appPort, + `/using-webassembly-instantiate` + ) + const json = JSON.parse(res.headers.get('data')) + await waitFor(500) + expect(json.value).toEqual(81) + expect(output).not.toContain(WASM_INSTANTIATE_ERROR) + expect(output).not.toContain('DynamicWasmCodeGenerationWarning') }) }) diff --git a/test/production/middleware-with-dynamic-code/index.test.ts b/test/production/middleware-with-dynamic-code/index.test.ts index 2002b3053037f..56e24b404ec62 100644 --- a/test/production/middleware-with-dynamic-code/index.test.ts +++ b/test/production/middleware-with-dynamic-code/index.test.ts @@ -1,6 +1,9 @@ -import { createNext } from 'e2e-utils' +import { createNext, FileRef } from 'e2e-utils' +import { join } from 'path' import { NextInstance } from 'test/lib/next-modes/base' +const DYNAMIC_CODE_EVAL_ERROR = `Dynamic Code Evaluation (e. g. 'eval', 'new Function', 'WebAssembly.compile') not allowed in Middleware middleware` + describe('Middleware with Dynamic code invokations', () => { let next: NextInstance @@ -8,6 +11,7 @@ describe('Middleware with Dynamic code invokations', () => { next = await createNext({ files: { 'lib/utils.js': '', + 'lib/square.wasm': new FileRef(join(__dirname, 'square.wasm')), 'pages/index.js': ` export default function () { return
Hello, world!
} `, @@ -32,6 +36,7 @@ describe('Middleware with Dynamic code invokations', () => { }) afterAll(() => next.destroy()) + beforeEach(() => next.stop()) it('detects dynamic code nested in @apollo/react-hooks', async () => { await next.patchFile( @@ -57,7 +62,7 @@ describe('Middleware with Dynamic code invokations', () => { await expect(next.start()).rejects.toThrow() expect(next.cliOutput).toContain(` ./node_modules/ts-invariant/lib/invariant.esm.js -Dynamic Code Evaluation (e. g. 'eval', 'new Function') not allowed in Middleware middleware`) +${DYNAMIC_CODE_EVAL_ERROR}`) }) it('detects dynamic code nested in has', async () => { @@ -71,10 +76,10 @@ Dynamic Code Evaluation (e. g. 'eval', 'new Function') not allowed in Middleware await expect(next.start()).rejects.toThrow() expect(next.cliOutput).toContain(` ./node_modules/function-bind/implementation.js -Dynamic Code Evaluation (e. g. 'eval', 'new Function') not allowed in Middleware middleware`) +${DYNAMIC_CODE_EVAL_ERROR}`) expect(next.cliOutput).toContain(` ./node_modules/has/src/index.js -Dynamic Code Evaluation (e. g. 'eval', 'new Function') not allowed in Middleware middleware`) +${DYNAMIC_CODE_EVAL_ERROR}`) }) it('detects dynamic code nested in qs', async () => { @@ -88,7 +93,7 @@ Dynamic Code Evaluation (e. g. 'eval', 'new Function') not allowed in Middleware await expect(next.start()).rejects.toThrow() expect(next.cliOutput).toContain(` ./node_modules/get-intrinsic/index.js -Dynamic Code Evaluation (e. g. 'eval', 'new Function') not allowed in Middleware middleware`) +${DYNAMIC_CODE_EVAL_ERROR}`) }) it('does not detects dynamic code nested in @aws-sdk/client-s3 (legit Function.bind)', async () => { @@ -106,8 +111,47 @@ Dynamic Code Evaluation (e. g. 'eval', 'new Function') not allowed in Middleware expect(next.cliOutput).not.toContain( `./node_modules/@aws-sdk/smithy-client/dist-es/lazy-json.js` ) - expect(next.cliOutput).not.toContain( - `Dynamic Code Evaluation (e. g. 'eval', 'new Function') not allowed in Middleware middleware` + expect(next.cliOutput).not.toContain(DYNAMIC_CODE_EVAL_ERROR) + }) + + it('does not determine WebAssembly.instantiate with a module parameter as dynamic code execution (legit)', async () => { + await next.patchFile( + 'lib/utils.js', + ` + import wasm from './square.wasm?module' + const instance = WebAssembly.instantiate(wasm) + ` ) + await next.start() + + expect(next.cliOutput).not.toContain(DYNAMIC_CODE_EVAL_ERROR) + }) + + // Actually this causes a dynamic code evaluation however, we can't determine the type of + // first parameter of WebAssembly.instanntiate statically. + it('does not determine WebAssembly.instantiate with a buffer parameter as dynamic code execution', async () => { + await next.patchFile( + 'lib/utils.js', + ` + const instance = WebAssembly.instantiate(new Uint8Array([0, 1, 2, 3])) + ` + ) + await next.start() + + expect(next.cliOutput).not.toContain(DYNAMIC_CODE_EVAL_ERROR) + }) + + it('detects use of WebAssembly.compile', async () => { + await next.patchFile( + 'lib/utils.js', + ` + const module = WebAssembly.compile(new Uint8Array([0, 1, 2, 3])) + ` + ) + + await expect(next.start()).rejects.toThrow() + expect(next.cliOutput).toContain(` +./lib/utils.js +${DYNAMIC_CODE_EVAL_ERROR}`) }) }) diff --git a/test/production/middleware-with-dynamic-code/square.wasm b/test/production/middleware-with-dynamic-code/square.wasm new file mode 100644 index 0000000000000000000000000000000000000000..f836887dda8004a4f54c3897c1d349b676293119 GIT binary patch literal 63 zcmWN Date: Thu, 16 Jun 2022 09:26:40 -0500 Subject: [PATCH 2/2] Apply suggestions from code review --- errors/middleware-dynamic-wasm-compilation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/errors/middleware-dynamic-wasm-compilation.md b/errors/middleware-dynamic-wasm-compilation.md index b5c5adfa8f07f..7b5272a41d505 100644 --- a/errors/middleware-dynamic-wasm-compilation.md +++ b/errors/middleware-dynamic-wasm-compilation.md @@ -1,4 +1,4 @@ -# Dynamic WASM complication is not available in Middlewares +# Dynamic WASM compilation is not available in Middlewares #### Why This Error Occurred