mirror of
https://github.com/hanzoai/next.js.git
synced 2026-09-24 17:00:51 +00:00
https://github.com/vercel/next.js/security/advisories/GHSA-955p-x3mx-jcvp Co-authored-by: Josh Story <[email protected]>
261 lines
9.0 KiB
TypeScript
261 lines
9.0 KiB
TypeScript
import { nextTestSetup } from 'e2e-utils'
|
|
import { createRequestTracker } from 'e2e-utils/request-tracker'
|
|
import { retry } from 'next-test-utils'
|
|
import { outdent } from 'outdent'
|
|
|
|
describe('unrecognized server actions', () => {
|
|
const unrecognizedActionId = '0'.repeat(42)
|
|
|
|
const { next, isNextDeploy, isNextDev } = nextTestSetup({
|
|
files: __dirname,
|
|
})
|
|
|
|
let cliOutputPosition: number = 0
|
|
beforeEach(() => {
|
|
cliOutputPosition = next.cliOutput.length
|
|
})
|
|
const getLogs = () => {
|
|
return next.cliOutput.slice(cliOutputPosition)
|
|
}
|
|
|
|
// This is disabled when deployed because the 404 page will be served as a static route
|
|
// which will not support POST requests, and will return a 405 instead.
|
|
if (!isNextDeploy) {
|
|
it('should 404 when POSTing a non-server-action request to a nonexistent page', async () => {
|
|
const res = await next.fetch('/non-existent-route', {
|
|
method: 'POST',
|
|
headers: {
|
|
'content-type': 'application/x-www-form-urlencoded',
|
|
},
|
|
body: 'foo=bar',
|
|
})
|
|
|
|
const cliOutput = getLogs()
|
|
expect(cliOutput).not.toContain('TypeError')
|
|
expect(cliOutput).not.toContain(
|
|
'Missing `origin` header from a forwarded Server Actions request'
|
|
)
|
|
expect(res.status).toBe(404)
|
|
})
|
|
|
|
describe.each([
|
|
{
|
|
idType: 'malformed',
|
|
actionId: '123',
|
|
expectedError: outdent`
|
|
The Server Reference ID did not match the expected format. Received "123".
|
|
Read more: https://nextjs.org/docs/messages/failed-to-find-server-action
|
|
`,
|
|
},
|
|
{
|
|
idType: 'plausible but missing',
|
|
actionId: unrecognizedActionId,
|
|
expectedError: outdent`
|
|
Failed to find Server Action "${unrecognizedActionId}". This request might be from an older or newer deployment.
|
|
Read more: https://nextjs.org/docs/messages/failed-to-find-server-action
|
|
`,
|
|
},
|
|
{
|
|
// A well-known property name is excluded from server reference
|
|
// validation in the module map (so framework reflection probes don't
|
|
// throw), which means the lookup returns undefined rather than throwing.
|
|
// We should still surface a diagnosable error instead of a TypeError.
|
|
idType: 'well-known property name',
|
|
actionId: 'toString',
|
|
expectedError: outdent`
|
|
The Server Reference ID did not match the expected format. Received "toString".
|
|
Read more: https://nextjs.org/docs/messages/failed-to-find-server-action
|
|
`,
|
|
},
|
|
])('with a $idType id', ({ actionId, expectedError }) => {
|
|
it.each([
|
|
{
|
|
// encodeReply encodes simple args as plaintext.
|
|
name: 'plaintext',
|
|
request: {
|
|
contentType: 'text/plain;charset=UTF-8',
|
|
body: '{}',
|
|
},
|
|
},
|
|
{
|
|
// encodeReply encodes complex args as FormData.
|
|
// this body is empty and wouldn't match how react encodes an action, but it should be rejected
|
|
// before we even get to parsing the FormData, so it doesn't really matter.
|
|
name: 'form-data/multipart',
|
|
request: {
|
|
body: new FormData(),
|
|
},
|
|
},
|
|
])(
|
|
'should 404 when POSTing a server action to a nonexistent page: $name',
|
|
async ({ request: { contentType, body } }) => {
|
|
const res = await next.fetch('/non-existent-route', {
|
|
method: 'POST',
|
|
headers: {
|
|
'next-action': actionId,
|
|
...(contentType ? { 'content-type': contentType } : undefined),
|
|
},
|
|
// @ts-expect-error: node-fetch types don't seem to like FormData
|
|
body,
|
|
})
|
|
|
|
expect(res.status).toBe(404)
|
|
|
|
const cliOutput = getLogs()
|
|
expect(cliOutput).not.toContain('TypeError')
|
|
expect(cliOutput).not.toContain(
|
|
'Missing `origin` header from a forwarded Server Actions request'
|
|
)
|
|
expect(cliOutput).toInclude(expectedError)
|
|
}
|
|
)
|
|
})
|
|
}
|
|
|
|
it('should error when POSTing a urlencoded action to a nonexistent page', async () => {
|
|
const res = await next.fetch('/non-existent-route', {
|
|
method: 'POST',
|
|
headers: {
|
|
'next-action': '123',
|
|
'content-type': 'application/x-www-form-urlencoded',
|
|
},
|
|
body: 'foo=bar',
|
|
})
|
|
|
|
// On deploy, this would hit the 404 route which is a static page, and returns a 405 instead.
|
|
expect(res.status).toBeOneOf([405, 404])
|
|
})
|
|
|
|
describe.each(['nodejs', 'edge'])(
|
|
'should error and log a warning when submitting a server action with an unrecognized ID - %s',
|
|
(runtime) => {
|
|
const testUnrecognizedActionSubmission = async ({
|
|
formId,
|
|
disableJavaScript,
|
|
}: {
|
|
formId: string
|
|
disableJavaScript: boolean
|
|
}) => {
|
|
const browser = await next.browser(`/${runtime}/unrecognized-action`, {
|
|
disableJavaScript,
|
|
})
|
|
const requestTracker = createRequestTracker(browser)
|
|
|
|
const [_, response] = await requestTracker.captureResponse(
|
|
async () =>
|
|
await browser
|
|
.elementByCss(`form#${formId} button[type="submit"]`)
|
|
.click(),
|
|
{
|
|
request: {
|
|
method: 'POST',
|
|
pathname: `/${runtime}/unrecognized-action`,
|
|
},
|
|
}
|
|
)
|
|
|
|
if (!disableJavaScript) {
|
|
// A fetch action, sent via the router.
|
|
expect(response.status()).toBe(404)
|
|
// NOTE: we cannot validate the response text, because playwright hangs on `response.text()` for some reason.
|
|
expect(response.headers()['content-type']).toStartWith('text/plain')
|
|
|
|
// The submission should throw and trigger our error boundary.
|
|
expect(await browser.elementByCss(`#error-boundary`).text()).toMatch(
|
|
/Error boundary: Server Action ".+?" was not found on the server\./
|
|
)
|
|
|
|
// We responded with a 404, but we shouldn't trigger a not-found (either a custom or a default one)
|
|
expect(await browser.elementByCss('body').text()).not.toContain(
|
|
'Not found'
|
|
)
|
|
expect(await browser.elementByCss('body').text()).not.toContain(
|
|
'my-not-found'
|
|
)
|
|
|
|
if (!isNextDeploy) {
|
|
await retry(async () =>
|
|
expect(getLogs()).toInclude(outdent`
|
|
Failed to find Server Action "${unrecognizedActionId}". This request might be from an older or newer deployment.
|
|
Read more: https://nextjs.org/docs/messages/failed-to-find-server-action
|
|
`)
|
|
)
|
|
}
|
|
} else {
|
|
// An MPA action, sent without JS.
|
|
|
|
// FIXME: When deployed, the request is logged as a 500, but returns a 405.
|
|
// We also don't seem to display the error page correctly
|
|
if (!isNextDeploy) {
|
|
// FIXME: Currently, an unrecognized id in an MPA action results in a 500.
|
|
// This is not ideal, and ignores all nested `error.js` files, only showing the topmost one.
|
|
expect(response.status()).toBe(500)
|
|
if (isNextDev) {
|
|
expect(response.headers()['content-type']).toStartWith(
|
|
'text/html'
|
|
)
|
|
} else {
|
|
const responseText = await response.text()
|
|
expect(responseText).toBe('Internal Server Error')
|
|
expect(response.headers()['content-type']).toStartWith(
|
|
'text/plain'
|
|
)
|
|
}
|
|
|
|
// In dev, the 500 page doesn't have any SSR'd html, so it won't show anything without JS.
|
|
if (!isNextDev) {
|
|
expect(await browser.elementByCss('body').text()).toContain(
|
|
'Internal Server Error'
|
|
)
|
|
}
|
|
|
|
if (!isNextDeploy) {
|
|
await retry(async () =>
|
|
expect(getLogs()).toInclude(
|
|
`Error: Failed to find Server Action "${unrecognizedActionId}". This request might be from an older or newer deployment`
|
|
)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
it.each([
|
|
{
|
|
description: 'js enabled',
|
|
disableJavaScript: false,
|
|
},
|
|
{
|
|
description: 'js disabled',
|
|
disableJavaScript: true,
|
|
},
|
|
])(
|
|
'server action invoked via form - $description',
|
|
async ({ disableJavaScript }) => {
|
|
await testUnrecognizedActionSubmission({
|
|
formId: 'form-direct',
|
|
disableJavaScript,
|
|
})
|
|
}
|
|
)
|
|
|
|
// these forms rely on client-side JS, so we can't test them with JS disabled
|
|
it.each([
|
|
{
|
|
description: 'with simple argument',
|
|
formId: 'form-simple-argument',
|
|
},
|
|
{
|
|
description: 'with complex argument',
|
|
formId: 'form-complex-argument',
|
|
},
|
|
])('server action invoked from JS - $description', async ({ formId }) => {
|
|
await testUnrecognizedActionSubmission({
|
|
formId,
|
|
disableJavaScript: false,
|
|
})
|
|
})
|
|
}
|
|
)
|
|
})
|