From 3ced9ca9b578f6c18bb15a07f7b8e95a7a144d92 Mon Sep 17 00:00:00 2001 From: Harlan Wilton Date: Thu, 21 May 2026 15:07:17 +1000 Subject: [PATCH 1/4] fix(security): allow opting out of proxy page token in SSR payload Resolves #783 --- docs/content/docs/1.guides/2.first-party.md | 9 ++++++++ packages/script/src/module.ts | 23 +++++++++++++++++---- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/docs/content/docs/1.guides/2.first-party.md b/docs/content/docs/1.guides/2.first-party.md index ba6a2f98c..c65c0f74f 100644 --- a/docs/content/docs/1.guides/2.first-party.md +++ b/docs/content/docs/1.guides/2.first-party.md @@ -280,6 +280,11 @@ export default defineNuxtConfig({ // Auto-generate and persist a secret to .env in dev mode. // Set to false to disable. autoGenerateSecret: true, + // Emit a per-request proxy page token into the SSR payload so + // client-driven proxy calls authenticate without pre-signed URLs. + // Set to false to keep the token out of the payload (e.g. for a + // stable response etag); client-side calls then need signed URLs. + pageToken: true, } } }) @@ -303,6 +308,10 @@ The module only writes this when running `nuxt dev` with a signed endpoint enabl Page tokens are valid for 1 hour. If a user leaves a tab open longer than that, client-side proxy requests will start returning 403. The page will recover on next navigation or refresh. +**Proxy token changes the response payload on every request** + +The per-request page token is injected into the SSR payload, so the response hash differs each request. If you compute a stable `etag`, set `security.pageToken: false` to keep the token out of the payload. Client-side proxy calls will then need explicitly signed URLs. + #### Static Generation and SPA Mode URL signing requires a server runtime to verify HMAC signatures. Two deployment modes cannot support signing: diff --git a/packages/script/src/module.ts b/packages/script/src/module.ts index f2f073fa0..a71cc940a 100644 --- a/packages/script/src/module.ts +++ b/packages/script/src/module.ts @@ -408,6 +408,17 @@ export interface ModuleOptions { * @default 3600 */ pageTokenMaxAge?: number + /** + * Emit a per-request proxy page token into the SSR payload so client-driven + * proxy calls authenticate without each URL being HMAC-signed up front. + * + * Set to `false` to keep the token out of the payload (e.g. when computing a + * stable response `etag`). Client-side proxy requests that rely on the token + * will then need explicitly signed URLs. + * + * @default true + */ + pageToken?: boolean } /** * Google Static Maps proxy configuration. @@ -1058,10 +1069,14 @@ export default defineNuxtModule({ // Emit a per-request page token during SSR so client-driven proxy // calls (reactive fetches, dynamic image helpers) authenticate via // `_pt` + `_ts` without needing each URL to be HMAC-signed up front. - addPlugin({ - src: await resolvePath('./runtime/plugins/proxy-token.server'), - mode: 'server', - }) + // Opt out via `security.pageToken: false` to keep the token out of the + // SSR payload (e.g. for a stable response etag). + if (config.security?.pageToken !== false) { + addPlugin({ + src: await resolvePath('./runtime/plugins/proxy-token.server'), + mode: 'server', + }) + } } else if (!nuxt.options.dev) { logger.warn( From f5cbad4bb4bb708065b3627035fcca93dc4cfffe Mon Sep 17 00:00:00 2001 From: Harlan Wilton Date: Thu, 21 May 2026 15:07:40 +1000 Subject: [PATCH 2/4] docs: active voice --- docs/content/docs/1.guides/2.first-party.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/docs/1.guides/2.first-party.md b/docs/content/docs/1.guides/2.first-party.md index c65c0f74f..7071d3e86 100644 --- a/docs/content/docs/1.guides/2.first-party.md +++ b/docs/content/docs/1.guides/2.first-party.md @@ -310,7 +310,7 @@ Page tokens are valid for 1 hour. If a user leaves a tab open longer than that, **Proxy token changes the response payload on every request** -The per-request page token is injected into the SSR payload, so the response hash differs each request. If you compute a stable `etag`, set `security.pageToken: false` to keep the token out of the payload. Client-side proxy calls will then need explicitly signed URLs. +The module injects a per-request page token into the SSR payload, so the response hash differs each request. If you compute a stable `etag`, set `security.pageToken: false` to keep the token out of the payload. Client-side proxy calls will then need explicitly signed URLs. #### Static Generation and SPA Mode From f5e19cb68589935d0eaf897ecf9876cd8b8ee459 Mon Sep 17 00:00:00 2001 From: Harlan Wilton Date: Thu, 21 May 2026 15:20:06 +1000 Subject: [PATCH 3/4] docs: clarify pageToken opt-out rejects client-side proxy calls --- docs/content/docs/1.guides/2.first-party.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/content/docs/1.guides/2.first-party.md b/docs/content/docs/1.guides/2.first-party.md index 7071d3e86..6b2f19f72 100644 --- a/docs/content/docs/1.guides/2.first-party.md +++ b/docs/content/docs/1.guides/2.first-party.md @@ -283,7 +283,8 @@ export default defineNuxtConfig({ // Emit a per-request proxy page token into the SSR payload so // client-driven proxy calls authenticate without pre-signed URLs. // Set to false to keep the token out of the payload (e.g. for a - // stable response etag); client-side calls then need signed URLs. + // stable response etag). SSR/prerendered signed URLs keep working, + // but client-side reactive proxy calls will be rejected (403). pageToken: true, } } @@ -310,7 +311,7 @@ Page tokens are valid for 1 hour. If a user leaves a tab open longer than that, **Proxy token changes the response payload on every request** -The module injects a per-request page token into the SSR payload, so the response hash differs each request. If you compute a stable `etag`, set `security.pageToken: false` to keep the token out of the payload. Client-side proxy calls will then need explicitly signed URLs. +The module injects a per-request page token into the SSR payload, so the response hash differs each request. If you compute a stable `etag`, set `security.pageToken: false` to keep the token out of the payload. SSR and prerendered URLs are signed at render time so they keep working, but client-side reactive proxy calls (e.g. dynamic image helpers, runtime geocoding) will be rejected with a 403. Only disable the token if your scripts rely solely on SSR-signed URLs. #### Static Generation and SPA Mode From e549e6168382b29054eee296859c4aa414c821a8 Mon Sep 17 00:00:00 2001 From: Harlan Wilton Date: Thu, 21 May 2026 15:23:29 +1000 Subject: [PATCH 4/4] fix(security): allow `security: false` to fully disable proxy signing --- docs/content/docs/1.guides/2.first-party.md | 22 ++++++++---- packages/script/src/module.ts | 38 +++++++++------------ 2 files changed, 32 insertions(+), 28 deletions(-) diff --git a/docs/content/docs/1.guides/2.first-party.md b/docs/content/docs/1.guides/2.first-party.md index 6b2f19f72..b97969f42 100644 --- a/docs/content/docs/1.guides/2.first-party.md +++ b/docs/content/docs/1.guides/2.first-party.md @@ -280,17 +280,25 @@ export default defineNuxtConfig({ // Auto-generate and persist a secret to .env in dev mode. // Set to false to disable. autoGenerateSecret: true, - // Emit a per-request proxy page token into the SSR payload so - // client-driven proxy calls authenticate without pre-signed URLs. - // Set to false to keep the token out of the payload (e.g. for a - // stable response etag). SSR/prerendered signed URLs keep working, - // but client-side reactive proxy calls will be rejected (403). - pageToken: true, } } }) ``` +To disable proxy security entirely, set `security` to `false`: + +```ts [nuxt.config.ts] +export default defineNuxtConfig({ + scripts: { + // No secret is resolved or auto-generated, no page token is added to the + // SSR payload, and proxy endpoints pass requests through unverified. + security: false, + } +}) +``` + +This is useful when you need a deterministic SSR payload (e.g. to compute a stable response `etag`), since the per-request page token otherwise changes the payload on every request. Proxy endpoints stay functional but unprotected against quota abuse. + #### Troubleshooting **Signed URLs return 403 after deploy** @@ -311,7 +319,7 @@ Page tokens are valid for 1 hour. If a user leaves a tab open longer than that, **Proxy token changes the response payload on every request** -The module injects a per-request page token into the SSR payload, so the response hash differs each request. If you compute a stable `etag`, set `security.pageToken: false` to keep the token out of the payload. SSR and prerendered URLs are signed at render time so they keep working, but client-side reactive proxy calls (e.g. dynamic image helpers, runtime geocoding) will be rejected with a 403. Only disable the token if your scripts rely solely on SSR-signed URLs. +The module injects a per-request page token into the SSR payload, so the response hash differs each request. If you compute a stable `etag`, set `security: false` to disable proxy security entirely. Proxy endpoints then pass requests through without signature verification, so only do this if quota abuse on those endpoints is not a concern. #### Static Generation and SPA Mode diff --git a/packages/script/src/module.ts b/packages/script/src/module.ts index a71cc940a..e3ae9fd18 100644 --- a/packages/script/src/module.ts +++ b/packages/script/src/module.ts @@ -373,8 +373,12 @@ export interface ModuleOptions { * * The secret must be deterministic across deployments so that prerendered URLs * remain valid. Set it via `NUXT_SCRIPTS_PROXY_SECRET` or `security.secret`. + * + * Set to `false` to disable proxy security entirely: no secret is resolved or + * auto-generated, no page token is injected into the SSR payload, and proxy + * endpoints pass requests through without signature verification. */ - security?: { + security?: false | { /** * HMAC secret used to sign proxy URLs. * @@ -408,17 +412,6 @@ export interface ModuleOptions { * @default 3600 */ pageTokenMaxAge?: number - /** - * Emit a per-request proxy page token into the SSR payload so client-driven - * proxy calls authenticate without each URL being HMAC-signed up front. - * - * Set to `false` to keep the token out of the payload (e.g. when computing a - * stable response `etag`). Client-side proxy requests that rely on the token - * will then need explicitly signed URLs. - * - * @default true - */ - pageToken?: boolean } /** * Google Static Maps proxy configuration. @@ -1040,7 +1033,14 @@ export default defineNuxtModule({ const isStaticTarget = staticPresets.includes(nitroPreset) const isSpa = nuxt.options.ssr === false - if (anyHandlerRequiresSigning && (isSpa || isStaticTarget)) { + // Proxy security explicitly disabled: skip secret resolution and the page + // token plugin. `withSigning` passes requests through unverified. + if (config.security === false) { + if (anyHandlerRequiresSigning && !nuxt.options.dev) { + logger.info('[security] Proxy security disabled via `security: false`. Proxy endpoints will pass requests through without signature verification.') + } + } + else if (anyHandlerRequiresSigning && (isSpa || isStaticTarget)) { logger.warn( `[security] URL signing requires a server runtime${isStaticTarget ? ` (detected preset: ${nitroPreset})` : ' (ssr: false)'}.\n` + ' Proxy endpoints will work without signature verification.\n' @@ -1069,14 +1069,10 @@ export default defineNuxtModule({ // Emit a per-request page token during SSR so client-driven proxy // calls (reactive fetches, dynamic image helpers) authenticate via // `_pt` + `_ts` without needing each URL to be HMAC-signed up front. - // Opt out via `security.pageToken: false` to keep the token out of the - // SSR payload (e.g. for a stable response etag). - if (config.security?.pageToken !== false) { - addPlugin({ - src: await resolvePath('./runtime/plugins/proxy-token.server'), - mode: 'server', - }) - } + addPlugin({ + src: await resolvePath('./runtime/plugins/proxy-token.server'), + mode: 'server', + }) } else if (!nuxt.options.dev) { logger.warn(