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`, {