From 7f91336c1d40d00a4b0041d70759855ea7858a10 Mon Sep 17 00:00:00 2001 From: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com> Date: Fri, 10 Jul 2026 07:07:27 -0400 Subject: [PATCH 1/4] fix: don't disable CSRF protection in builds with a non-production NODE_ENV --- .changeset/tasty-dingos-breathe.md | 5 ++ .../runtime/app/server/remote/prerender.js | 3 +- packages/kit/src/runtime/server/respond.js | 2 +- .../csrf-non-production-node-env/package.json | 17 +++++++ .../csrf-non-production-node-env/src/app.html | 11 +++++ .../src/routes/+page.server.js | 3 ++ .../src/routes/+page.svelte | 3 ++ .../svelte.config.js | 10 ++++ .../vite.config.js | 6 +++ .../kit/test/build-errors/security.spec.js | 46 +++++++++++++++++++ pnpm-lock.yaml | 18 ++++++++ 11 files changed, 121 insertions(+), 3 deletions(-) create mode 100644 .changeset/tasty-dingos-breathe.md create mode 100644 packages/kit/test/build-errors/apps/csrf-non-production-node-env/package.json create mode 100644 packages/kit/test/build-errors/apps/csrf-non-production-node-env/src/app.html create mode 100644 packages/kit/test/build-errors/apps/csrf-non-production-node-env/src/routes/+page.server.js create mode 100644 packages/kit/test/build-errors/apps/csrf-non-production-node-env/src/routes/+page.svelte create mode 100644 packages/kit/test/build-errors/apps/csrf-non-production-node-env/svelte.config.js create mode 100644 packages/kit/test/build-errors/apps/csrf-non-production-node-env/vite.config.js create mode 100644 packages/kit/test/build-errors/security.spec.js diff --git a/.changeset/tasty-dingos-breathe.md b/.changeset/tasty-dingos-breathe.md new file mode 100644 index 000000000000..ebee724ddaaf --- /dev/null +++ b/.changeset/tasty-dingos-breathe.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: don't disable CSRF protection in builds with a non-production NODE_ENV diff --git a/packages/kit/src/runtime/app/server/remote/prerender.js b/packages/kit/src/runtime/app/server/remote/prerender.js index 0391c084da55..d775f3ab5de6 100644 --- a/packages/kit/src/runtime/app/server/remote/prerender.js +++ b/packages/kit/src/runtime/app/server/remote/prerender.js @@ -2,7 +2,6 @@ /** @import { RemotePrerenderInputsGenerator, RemotePrerenderInternals, MaybePromise } from 'types' */ /** @import { StandardSchemaV1 } from '@standard-schema/spec' */ import { json, error } from '@sveltejs/kit'; -import { DEV } from 'esm-env'; import { get_request_store } from '@sveltejs/kit/internal/server'; import { stringify, stringify_remote_arg } from '../../../shared.js'; import { noop } from '../../../../utils/functions.js'; @@ -99,7 +98,7 @@ export function prerender(validate_or_fn, fn_or_options, maybe_options) { const id = __.id; const url = `${base}/${app_dir}/remote/${id}${payload ? `/${payload}` : ''}`; - if (!state.prerendering && !DEV && !event.isRemoteRequest) { + if (!state.prerendering && !__SVELTEKIT_DEV__ && !event.isRemoteRequest) { try { // TODO adapters can provide prerendered data more efficiently than // fetching from the public internet diff --git a/packages/kit/src/runtime/server/respond.js b/packages/kit/src/runtime/server/respond.js index f73a8a156473..b323f560fce0 100644 --- a/packages/kit/src/runtime/server/respond.js +++ b/packages/kit/src/runtime/server/respond.js @@ -70,7 +70,7 @@ export async function internal_respond(request, options, manifest, state) { const is_data_request = has_data_suffix(url.pathname); const remote_id = get_remote_id(url); - if (!DEV) { + if (!__SVELTEKIT_DEV__) { const request_origin = request.headers.get('origin'); if (remote_id) { diff --git a/packages/kit/test/build-errors/apps/csrf-non-production-node-env/package.json b/packages/kit/test/build-errors/apps/csrf-non-production-node-env/package.json new file mode 100644 index 000000000000..35c562387b49 --- /dev/null +++ b/packages/kit/test/build-errors/apps/csrf-non-production-node-env/package.json @@ -0,0 +1,17 @@ +{ + "name": "csrf-non-production-node-env", + "private": true, + "version": "0.0.1", + "scripts": { + "build": "vite build", + "prepare": "svelte-kit sync" + }, + "devDependencies": { + "@sveltejs/adapter-auto": "workspace:^", + "@sveltejs/kit": "workspace:^", + "@sveltejs/vite-plugin-svelte": "catalog:", + "svelte": "catalog:", + "vite": "catalog:" + }, + "type": "module" +} diff --git a/packages/kit/test/build-errors/apps/csrf-non-production-node-env/src/app.html b/packages/kit/test/build-errors/apps/csrf-non-production-node-env/src/app.html new file mode 100644 index 000000000000..d533c5e31716 --- /dev/null +++ b/packages/kit/test/build-errors/apps/csrf-non-production-node-env/src/app.html @@ -0,0 +1,11 @@ + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/packages/kit/test/build-errors/apps/csrf-non-production-node-env/src/routes/+page.server.js b/packages/kit/test/build-errors/apps/csrf-non-production-node-env/src/routes/+page.server.js new file mode 100644 index 000000000000..20532646ab9b --- /dev/null +++ b/packages/kit/test/build-errors/apps/csrf-non-production-node-env/src/routes/+page.server.js @@ -0,0 +1,3 @@ +export const actions = { + default: () => ({ success: true }) +}; diff --git a/packages/kit/test/build-errors/apps/csrf-non-production-node-env/src/routes/+page.svelte b/packages/kit/test/build-errors/apps/csrf-non-production-node-env/src/routes/+page.svelte new file mode 100644 index 000000000000..e1425de82c6a --- /dev/null +++ b/packages/kit/test/build-errors/apps/csrf-non-production-node-env/src/routes/+page.svelte @@ -0,0 +1,3 @@ +
+ +
diff --git a/packages/kit/test/build-errors/apps/csrf-non-production-node-env/svelte.config.js b/packages/kit/test/build-errors/apps/csrf-non-production-node-env/svelte.config.js new file mode 100644 index 000000000000..6e60dbbbdd9c --- /dev/null +++ b/packages/kit/test/build-errors/apps/csrf-non-production-node-env/svelte.config.js @@ -0,0 +1,10 @@ +import adapter from '../../../../../adapter-auto/index.js'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + kit: { + adapter: adapter() + } +}; + +export default config; diff --git a/packages/kit/test/build-errors/apps/csrf-non-production-node-env/vite.config.js b/packages/kit/test/build-errors/apps/csrf-non-production-node-env/vite.config.js new file mode 100644 index 000000000000..bbf8c7da43f0 --- /dev/null +++ b/packages/kit/test/build-errors/apps/csrf-non-production-node-env/vite.config.js @@ -0,0 +1,6 @@ +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [sveltekit()] +}); diff --git a/packages/kit/test/build-errors/security.spec.js b/packages/kit/test/build-errors/security.spec.js new file mode 100644 index 000000000000..8764c6127712 --- /dev/null +++ b/packages/kit/test/build-errors/security.spec.js @@ -0,0 +1,46 @@ +import { execSync } from 'node:child_process'; +import path from 'node:path'; +import process from 'node:process'; +import { pathToFileURL } from 'node:url'; +import { assert, test } from 'vitest'; + +const timeout = 60_000; + +test('CSRF protection is enabled in non-production builds', { timeout }, async () => { + const cwd = path.join(process.cwd(), 'apps/csrf-non-production-node-env'); + + execSync('pnpm build', { + cwd, + stdio: 'pipe', + timeout, + env: { ...process.env, NODE_ENV: 'staging' } + }); + + const server_root = path.join(cwd, '.svelte-kit/output/server'); + const [{ Server }, { manifest }] = await Promise.all([ + import(pathToFileURL(path.join(server_root, 'index.js')).href), + import(pathToFileURL(path.join(server_root, 'manifest.js')).href) + ]); + + const server = new Server(manifest); + await server.init({ env: {} }); + + const options = { getClientAddress: () => '127.0.0.1' }; + const response = await server.respond(new Request('http://localhost/'), options); + assert.equal(response.status, 200); + + const csrf_response = await server.respond( + new Request('http://localhost/', { + method: 'POST', + headers: { + 'content-type': 'application/x-www-form-urlencoded', + origin: 'https://evil.example' + }, + body: 'foo=bar' + }), + options + ); + + assert.equal(csrf_response.status, 403); + assert.equal(await csrf_response.text(), 'Cross-site POST form submissions are forbidden'); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index afae32924842..5bce43c2ccae 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -957,6 +957,24 @@ importers: specifier: 'catalog:' version: 4.1.9(@opentelemetry/api@1.9.0)(@types/node@18.19.130)(@vitest/browser-playwright@4.1.9)(jsdom@26.1.0)(vite@6.4.3(@types/node@18.19.130)(jiti@2.4.2)(lightningcss@1.30.1)(yaml@2.9.0)) + packages/kit/test/build-errors/apps/csrf-non-production-node-env: + devDependencies: + '@sveltejs/adapter-auto': + specifier: workspace:^ + version: link:../../../../../adapter-auto + '@sveltejs/kit': + specifier: workspace:^ + version: link:../../../.. + '@sveltejs/vite-plugin-svelte': + specifier: 'catalog:' + version: 6.2.4(svelte@5.56.3(@typescript-eslint/types@8.61.1))(vite@6.4.3(@types/node@18.19.130)(jiti@2.4.2)(lightningcss@1.30.1)(yaml@2.9.0)) + svelte: + specifier: 'catalog:' + version: 5.56.3(@typescript-eslint/types@8.61.1) + vite: + specifier: 'catalog:' + version: 6.4.3(@types/node@18.19.130)(jiti@2.4.2)(lightningcss@1.30.1)(yaml@2.9.0) + packages/kit/test/build-errors/apps/prerender-entry-generator-mismatch: devDependencies: '@sveltejs/adapter-auto': From aff0c0d100f2a14e520a364925e41d4070d4e877 Mon Sep 17 00:00:00 2001 From: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com> Date: Fri, 10 Jul 2026 14:58:47 -0400 Subject: [PATCH 2/4] test: move CSRF test into options-2 instead of a new test app --- packages/kit/test/apps/options-2/test/test.js | 19 ++++++++ .../csrf-non-production-node-env/package.json | 17 ------- .../csrf-non-production-node-env/src/app.html | 11 ----- .../src/routes/+page.server.js | 3 -- .../src/routes/+page.svelte | 3 -- .../svelte.config.js | 10 ---- .../vite.config.js | 6 --- .../kit/test/build-errors/security.spec.js | 46 ------------------- pnpm-lock.yaml | 18 -------- 9 files changed, 19 insertions(+), 114 deletions(-) delete mode 100644 packages/kit/test/build-errors/apps/csrf-non-production-node-env/package.json delete mode 100644 packages/kit/test/build-errors/apps/csrf-non-production-node-env/src/app.html delete mode 100644 packages/kit/test/build-errors/apps/csrf-non-production-node-env/src/routes/+page.server.js delete mode 100644 packages/kit/test/build-errors/apps/csrf-non-production-node-env/src/routes/+page.svelte delete mode 100644 packages/kit/test/build-errors/apps/csrf-non-production-node-env/svelte.config.js delete mode 100644 packages/kit/test/build-errors/apps/csrf-non-production-node-env/vite.config.js delete mode 100644 packages/kit/test/build-errors/security.spec.js diff --git a/packages/kit/test/apps/options-2/test/test.js b/packages/kit/test/apps/options-2/test/test.js index 3ea959f035d3..7e9e7f1455f7 100644 --- a/packages/kit/test/apps/options-2/test/test.js +++ b/packages/kit/test/apps/options-2/test/test.js @@ -165,6 +165,25 @@ test.describe("bundleStrategy: 'single'", () => { }); }); +test.describe('CSRF', () => { + // this app is built with NODE_ENV=custom, which must not disable CSRF protection + test('blocks cross-origin form submissions', async ({ baseURL }) => { + test.skip(!!process.env.DEV); + + const res = await fetch(`${baseURL}/basepath`, { + method: 'POST', + headers: { + 'content-type': 'application/x-www-form-urlencoded', + origin: 'https://evil.example' + }, + body: 'foo=bar' + }); + + expect(res.status).toBe(403); + expect(await res.text()).toBe('Cross-site POST form submissions are forbidden'); + }); +}); + test.describe('Vite', () => { // regression test for https://github.com/sveltejs/kit/issues/13249: // user `define`s referenced at the top level of hooks.client.js must be diff --git a/packages/kit/test/build-errors/apps/csrf-non-production-node-env/package.json b/packages/kit/test/build-errors/apps/csrf-non-production-node-env/package.json deleted file mode 100644 index 35c562387b49..000000000000 --- a/packages/kit/test/build-errors/apps/csrf-non-production-node-env/package.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "csrf-non-production-node-env", - "private": true, - "version": "0.0.1", - "scripts": { - "build": "vite build", - "prepare": "svelte-kit sync" - }, - "devDependencies": { - "@sveltejs/adapter-auto": "workspace:^", - "@sveltejs/kit": "workspace:^", - "@sveltejs/vite-plugin-svelte": "catalog:", - "svelte": "catalog:", - "vite": "catalog:" - }, - "type": "module" -} diff --git a/packages/kit/test/build-errors/apps/csrf-non-production-node-env/src/app.html b/packages/kit/test/build-errors/apps/csrf-non-production-node-env/src/app.html deleted file mode 100644 index d533c5e31716..000000000000 --- a/packages/kit/test/build-errors/apps/csrf-non-production-node-env/src/app.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - %sveltekit.head% - - -
%sveltekit.body%
- - diff --git a/packages/kit/test/build-errors/apps/csrf-non-production-node-env/src/routes/+page.server.js b/packages/kit/test/build-errors/apps/csrf-non-production-node-env/src/routes/+page.server.js deleted file mode 100644 index 20532646ab9b..000000000000 --- a/packages/kit/test/build-errors/apps/csrf-non-production-node-env/src/routes/+page.server.js +++ /dev/null @@ -1,3 +0,0 @@ -export const actions = { - default: () => ({ success: true }) -}; diff --git a/packages/kit/test/build-errors/apps/csrf-non-production-node-env/src/routes/+page.svelte b/packages/kit/test/build-errors/apps/csrf-non-production-node-env/src/routes/+page.svelte deleted file mode 100644 index e1425de82c6a..000000000000 --- a/packages/kit/test/build-errors/apps/csrf-non-production-node-env/src/routes/+page.svelte +++ /dev/null @@ -1,3 +0,0 @@ -
- -
diff --git a/packages/kit/test/build-errors/apps/csrf-non-production-node-env/svelte.config.js b/packages/kit/test/build-errors/apps/csrf-non-production-node-env/svelte.config.js deleted file mode 100644 index 6e60dbbbdd9c..000000000000 --- a/packages/kit/test/build-errors/apps/csrf-non-production-node-env/svelte.config.js +++ /dev/null @@ -1,10 +0,0 @@ -import adapter from '../../../../../adapter-auto/index.js'; - -/** @type {import('@sveltejs/kit').Config} */ -const config = { - kit: { - adapter: adapter() - } -}; - -export default config; diff --git a/packages/kit/test/build-errors/apps/csrf-non-production-node-env/vite.config.js b/packages/kit/test/build-errors/apps/csrf-non-production-node-env/vite.config.js deleted file mode 100644 index bbf8c7da43f0..000000000000 --- a/packages/kit/test/build-errors/apps/csrf-non-production-node-env/vite.config.js +++ /dev/null @@ -1,6 +0,0 @@ -import { sveltekit } from '@sveltejs/kit/vite'; -import { defineConfig } from 'vite'; - -export default defineConfig({ - plugins: [sveltekit()] -}); diff --git a/packages/kit/test/build-errors/security.spec.js b/packages/kit/test/build-errors/security.spec.js deleted file mode 100644 index 8764c6127712..000000000000 --- a/packages/kit/test/build-errors/security.spec.js +++ /dev/null @@ -1,46 +0,0 @@ -import { execSync } from 'node:child_process'; -import path from 'node:path'; -import process from 'node:process'; -import { pathToFileURL } from 'node:url'; -import { assert, test } from 'vitest'; - -const timeout = 60_000; - -test('CSRF protection is enabled in non-production builds', { timeout }, async () => { - const cwd = path.join(process.cwd(), 'apps/csrf-non-production-node-env'); - - execSync('pnpm build', { - cwd, - stdio: 'pipe', - timeout, - env: { ...process.env, NODE_ENV: 'staging' } - }); - - const server_root = path.join(cwd, '.svelte-kit/output/server'); - const [{ Server }, { manifest }] = await Promise.all([ - import(pathToFileURL(path.join(server_root, 'index.js')).href), - import(pathToFileURL(path.join(server_root, 'manifest.js')).href) - ]); - - const server = new Server(manifest); - await server.init({ env: {} }); - - const options = { getClientAddress: () => '127.0.0.1' }; - const response = await server.respond(new Request('http://localhost/'), options); - assert.equal(response.status, 200); - - const csrf_response = await server.respond( - new Request('http://localhost/', { - method: 'POST', - headers: { - 'content-type': 'application/x-www-form-urlencoded', - origin: 'https://evil.example' - }, - body: 'foo=bar' - }), - options - ); - - assert.equal(csrf_response.status, 403); - assert.equal(await csrf_response.text(), 'Cross-site POST form submissions are forbidden'); -}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5bce43c2ccae..afae32924842 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -957,24 +957,6 @@ importers: specifier: 'catalog:' version: 4.1.9(@opentelemetry/api@1.9.0)(@types/node@18.19.130)(@vitest/browser-playwright@4.1.9)(jsdom@26.1.0)(vite@6.4.3(@types/node@18.19.130)(jiti@2.4.2)(lightningcss@1.30.1)(yaml@2.9.0)) - packages/kit/test/build-errors/apps/csrf-non-production-node-env: - devDependencies: - '@sveltejs/adapter-auto': - specifier: workspace:^ - version: link:../../../../../adapter-auto - '@sveltejs/kit': - specifier: workspace:^ - version: link:../../../.. - '@sveltejs/vite-plugin-svelte': - specifier: 'catalog:' - version: 6.2.4(svelte@5.56.3(@typescript-eslint/types@8.61.1))(vite@6.4.3(@types/node@18.19.130)(jiti@2.4.2)(lightningcss@1.30.1)(yaml@2.9.0)) - svelte: - specifier: 'catalog:' - version: 5.56.3(@typescript-eslint/types@8.61.1) - vite: - specifier: 'catalog:' - version: 6.4.3(@types/node@18.19.130)(jiti@2.4.2)(lightningcss@1.30.1)(yaml@2.9.0) - packages/kit/test/build-errors/apps/prerender-entry-generator-mismatch: devDependencies: '@sveltejs/adapter-auto': From aa436118ee6e4a6fc7ed54c3185c98daa71f346b Mon Sep 17 00:00:00 2001 From: Nic Polumeyv Date: Fri, 10 Jul 2026 15:06:56 -0400 Subject: [PATCH 3/4] Apply suggestion from @teemingc Co-authored-by: Tee Ming --- .changeset/tasty-dingos-breathe.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/tasty-dingos-breathe.md b/.changeset/tasty-dingos-breathe.md index ebee724ddaaf..24982895b213 100644 --- a/.changeset/tasty-dingos-breathe.md +++ b/.changeset/tasty-dingos-breathe.md @@ -2,4 +2,4 @@ '@sveltejs/kit': patch --- -fix: don't disable CSRF protection in builds with a non-production NODE_ENV +fix: enable CSRF protection in builds with a non-production `NODE_ENV` value From 67459fedcb0f782779b3c84514e84f612fd1a7f2 Mon Sep 17 00:00:00 2001 From: Tee Ming Date: Tue, 14 Jul 2026 16:50:11 +0800 Subject: [PATCH 4/4] Apply suggestion from @teemingc --- packages/kit/test/apps/options-2/test/test.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/kit/test/apps/options-2/test/test.js b/packages/kit/test/apps/options-2/test/test.js index 7e9e7f1455f7..f627e9adb682 100644 --- a/packages/kit/test/apps/options-2/test/test.js +++ b/packages/kit/test/apps/options-2/test/test.js @@ -166,8 +166,7 @@ test.describe("bundleStrategy: 'single'", () => { }); test.describe('CSRF', () => { - // this app is built with NODE_ENV=custom, which must not disable CSRF protection - test('blocks cross-origin form submissions', async ({ baseURL }) => { + test('blocks cross-origin form submissions when custom NODE_ENV is set', async ({ baseURL }) => { test.skip(!!process.env.DEV); const res = await fetch(`${baseURL}/basepath`, {