Skip to content

Commit

Permalink
[middleware] Warn dynamic WASM compilation (vercel#37681)
Browse files Browse the repository at this point in the history
In Middlewares, dynamic code execution is not allowed. Currently, we warn if eval / new Function are invoked in dev but don't warn another dynamic code execution in WebAssembly.

This PR adds warnings for `WebAssembly.compile` and `WebAssembly.instantiate` with a buffer parameter (note that `WebAssembly.instantiate` with a **module** parameter is legit) invocations. Note that other methods that compile WASM dynamically such as `WebAssembly.compileStreaming` are not exposed to users so we don't need to cover them.



## Bug

- [ ] Related issues linked using `fixes #number`
- [ ] Integration tests added
- [ ] Errors have helpful link attached, see `contributing.md`

## Feature

- [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR.
- [ ] Related issues linked using `fixes #number`
- [x] Integration tests added
- [x] Documentation added
- [ ] Telemetry added. In case of a feature if it's used or not.
- [x] Errors have helpful link attached, see `contributing.md`

## Documentation / Examples

- [ ] Make sure the linting passes by running `pnpm lint`
- [ ] The examples guidelines are followed from [our contributing doc](https://github.com/vercel/next.js/blob/canary/contributing.md#adding-examples)


Co-authored-by: JJ Kasper <22380829+ijjk@users.noreply.github.com>
  • Loading branch information
2 people authored and aboqasem committed Jun 18, 2022
1 parent 5cdc750 commit c91e7d0
Show file tree
Hide file tree
Showing 11 changed files with 309 additions and 12 deletions.
4 changes: 4 additions & 0 deletions errors/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
]
}
Expand Down
27 changes: 27 additions & 0 deletions errors/middleware-dynamic-wasm-compilation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Dynamic WASM compilation 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
}
```
62 changes: 61 additions & 1 deletion packages/next/build/webpack/plugins/middleware-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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(
', '
Expand Down
43 changes: 43 additions & 0 deletions packages/next/server/web/sandbox/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ export async function getModuleContext(options: ModuleContextOptions) {
*/
async function createModuleContext(options: ModuleContextOptions) {
const warnedEvals = new Set<string>()
const warnedWasmCodegens = new Set<string>()
const wasm = await loadWasm(options.wasm)
const runtime = new EdgeRuntime({
codeGeneration:
Expand All @@ -122,6 +123,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 ?? {})
Expand Down
Binary file not shown.
35 changes: 35 additions & 0 deletions test/integration/middleware-dynamic-code/lib/wasm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// (module
// (type (;0;) (func (param i32) (result i32)))
// (func (;0;) (type 0) (param i32) (result i32)
// local.get 0
// local.get 0
// i32.mul)
// (table (;0;) 0 funcref)
// (memory (;0;) 1)
// (export "memory" (memory 0))
// (export "square" (func 0)))
const SQUARE_WASM_BUFFER = new Uint8Array([
0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x06, 0x01, 0x60, 0x01,
0x7f, 0x01, 0x7f, 0x03, 0x02, 0x01, 0x00, 0x04, 0x04, 0x01, 0x70, 0x00, 0x00,
0x05, 0x03, 0x01, 0x00, 0x01, 0x07, 0x13, 0x02, 0x06, 0x6d, 0x65, 0x6d, 0x6f,
0x72, 0x79, 0x02, 0x00, 0x06, 0x73, 0x71, 0x75, 0x61, 0x72, 0x65, 0x00, 0x00,
0x0a, 0x09, 0x01, 0x07, 0x00, 0x20, 0x00, 0x20, 0x00, 0x6c, 0x0b,
])

import squareWasmModule from './square.wasm?module'

export async function usingWebAssemblyCompile(x) {
const module = await WebAssembly.compile(SQUARE_WASM_BUFFER)
const instance = await WebAssembly.instantiate(module, {})
return { value: instance.exports.square(x) }
}

export async function usingWebAssemblyInstantiateWithBuffer(x) {
const { instance } = await WebAssembly.instantiate(SQUARE_WASM_BUFFER, {})
return { value: instance.exports.square(x) }
}

export async function usingWebAssemblyInstantiate(x) {
const instance = await WebAssembly.instantiate(squareWasmModule)
return { value: instance.exports.square(x) }
}
27 changes: 27 additions & 0 deletions test/integration/middleware-dynamic-code/middleware.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { notUsingEval, usingEval } from './lib/utils'
import {
usingWebAssemblyCompile,
usingWebAssemblyInstantiate,
usingWebAssemblyInstantiateWithBuffer,
} from './lib/wasm'

export async function middleware(request) {
if (request.nextUrl.pathname === '/using-eval') {
Expand All @@ -12,4 +17,26 @@ export async function middleware(request) {
headers: { data: JSON.stringify(await notUsingEval()) },
})
}

if (request.nextUrl.pathname === '/using-webassembly-compile') {
return new Response(null, {
headers: { data: JSON.stringify(await usingWebAssemblyCompile(9)) },
})
}

if (request.nextUrl.pathname === '/using-webassembly-instantiate') {
return new Response(null, {
headers: { data: JSON.stringify(await usingWebAssemblyInstantiate(9)) },
})
}

if (
request.nextUrl.pathname === '/using-webassembly-instantiate-with-buffer'
) {
return new Response(null, {
headers: {
data: JSON.stringify(await usingWebAssemblyInstantiateWithBuffer(9)),
},
})
}
}
6 changes: 6 additions & 0 deletions test/integration/middleware-dynamic-code/next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = {
webpack(config) {
config.experiments = { ...config.experiments, asyncWebAssembly: true }
return config
},
}
59 changes: 55 additions & 4 deletions test/integration/middleware-dynamic-code/test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ import {
} from 'next-test-utils'

const context = {}
const DYNAMIC_CODE_ERROR = `Dynamic Code Evaluation (e. g. 'eval', 'new Function') not allowed in Middleware`
const EVAL_ERROR = `Dynamic Code Evaluation (e. g. 'eval', 'new Function') not allowed in Middleware`
const DYNAMIC_CODE_ERROR = `Dynamic Code Evaluation (e. g. 'eval', 'new Function', 'WebAssembly.compile') not allowed in Middleware`
const WASM_COMPILE_ERROR = `Dynamic WASM code generation (e. g. 'WebAssembly.compile') not allowed in Middleware`
const WASM_INSTANTIATE_ERROR = `Dynamic WASM code generation ('WebAssembly.instantiate' with a buffer parameter) not allowed in Middleware`

jest.setTimeout(1000 * 60 * 2)
context.appDir = join(__dirname, '../')
Expand Down Expand Up @@ -43,7 +46,7 @@ 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).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
Expand All @@ -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')
})
})

Expand Down
Loading

0 comments on commit c91e7d0

Please sign in to comment.