Skip to content

Commit c8fe15b

Browse files
committed
refactor(security): remove proxy URL signing and the page token
The proxy page token was embedded per-request in the SSR payload, which made the payload non-deterministic (breaking response etag hashing, issue #783), broke under response caching and SSG, and was weak protection anyway since it is scrapeable from the payload. Signing also could not protect the one endpoint that mattered: the Google Maps proxy URL is emitted by a client component, so it cannot be signed without either shipping node:crypto to the browser or exposing a signing oracle. Remove the proxy URL signing subsystem entirely: - Delete the page-token plugin/composable, sign.ts, withSigning, sign-constants, and the proxy secret resolution + .env auto-generation. - Drop the `security` config option and the generate-secret CLI command. - Proxy/embed endpoints rely on their existing upstream-domain allowlist; the Google Maps proxy keeps its API key server-side and is cache-shielded. - useScriptProxyUrl / buildProxyUrl emit plain URLs; the SSR payload is now deterministic. Resolves #783
1 parent 0829ee0 commit c8fe15b

40 files changed

Lines changed: 148 additions & 1602 deletions

docs/content/docs/1.guides/2.first-party.md

Lines changed: 3 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -202,118 +202,11 @@ Platform-level rewrites bypass the privacy anonymisation layer. The proxy handle
202202

203203
## Proxy Endpoint Security
204204

205-
Several proxy endpoints (Google Static Maps, Geocode, Gravatar, embed image proxies) inject server-side API keys or forward requests to third-party services. Without protection, anyone who discovers these endpoints could call them directly and consume your API quota.
205+
Proxy and embed endpoints (Google Static Maps, Geocode, Gravatar, embed image proxies) forward requests to third-party services. Each endpoint is restricted to an allowlist of upstream domains, so it cannot be used as an open proxy for arbitrary URLs.
206206

207-
### HMAC URL Signing
207+
The Google Maps endpoints inject your server-side API key. They are cache-shielded (static maps for 7 days, geocode for 30 days), so repeated requests for the same map do not bill again. The endpoints are still reachable by anyone who discovers them; if quota abuse is a concern, add rate limiting at your platform edge or via Nitro `routeRules` for the `/_scripts/proxy/**` paths.
208208

209-
The module provides optional HMAC signing to lock down proxy endpoints. When enabled, only URLs generated server-side (during SSR or prerender) or accompanied by a valid page token are accepted. Unsigned requests receive a `403`.
210-
211-
#### Setup
212-
213-
Generate a signing secret:
214-
215-
```bash
216-
npx @nuxt/scripts generate-secret
217-
```
218-
219-
Then set it as an environment variable:
220-
221-
```bash
222-
NUXT_SCRIPTS_PROXY_SECRET=<your-secret>
223-
```
224-
225-
Or configure it directly:
226-
227-
```ts [nuxt.config.ts]
228-
export default defineNuxtConfig({
229-
scripts: {
230-
security: {
231-
secret: process.env.NUXT_SCRIPTS_PROXY_SECRET,
232-
}
233-
}
234-
})
235-
```
236-
237-
#### How It Works
238-
239-
The module uses two verification modes:
240-
241-
1. **URL signatures** for server-rendered content. During SSR/prerender, proxy URLs include a `sig` parameter: an HMAC of the path and query params. The proxy endpoint verifies the signature before forwarding.
242-
243-
2. **Page tokens** for client-side reactive updates. Some components recompute their proxy URL after mount (e.g. measuring element dimensions). The server embeds a short-lived token (`_pt` + `_ts` params) in the SSR payload. The token is valid for any params on any proxy path and expires after 1 hour.
244-
245-
#### Development
246-
247-
In development, the module auto-generates a secret and writes it to your `.env` file on first run. You don't need to configure anything for local dev.
248-
249-
#### Production
250-
251-
Set `NUXT_SCRIPTS_PROXY_SECRET` in your deployment environment. The secret must be the same across all replicas and across build/runtime so that URLs signed at prerender time remain valid.
252-
253-
::callout{type="warning"}
254-
Without a secret, proxy endpoints remain functional but unprotected. The module logs a warning at startup when it detects signed endpoints without a secret.
255-
::
256-
257-
#### Signed Endpoints
258-
259-
The following proxy endpoints require signing when you configure a secret:
260-
261-
| Script | Endpoints |
262-
|--------|-----------|
263-
| **Google Maps** | `/_scripts/proxy/google-static-maps`, `/_scripts/proxy/google-maps-geocode` |
264-
| **Gravatar** | `/_scripts/proxy/gravatar` |
265-
| **Bluesky** | `/_scripts/embed/bluesky`, `/_scripts/embed/bluesky-image` |
266-
| **Instagram** | `/_scripts/embed/instagram`, `/_scripts/embed/instagram-image`, `/_scripts/embed/instagram-asset` |
267-
| **X (Twitter)** | `/_scripts/embed/x`, `/_scripts/embed/x-image` |
268-
269-
Analytics proxy endpoints (Google Analytics, Plausible, etc.) do not use signing because they only forward collection payloads and never expose API keys.
270-
271-
#### Configuration Reference
272-
273-
```ts [nuxt.config.ts]
274-
export default defineNuxtConfig({
275-
scripts: {
276-
security: {
277-
// HMAC secret for signing proxy URLs.
278-
// Falls back to process.env.NUXT_SCRIPTS_PROXY_SECRET.
279-
secret: undefined,
280-
// Auto-generate and persist a secret to .env in dev mode.
281-
// Set to false to disable.
282-
autoGenerateSecret: true,
283-
}
284-
}
285-
})
286-
```
287-
288-
#### Troubleshooting
289-
290-
**Signed URLs return 403 after deploy**
291-
292-
The secret must be identical at build time (when URLs are signed during prerender) and at runtime (when the server verifies them). If you prerender pages, ensure `NUXT_SCRIPTS_PROXY_SECRET` is available in both your build environment and your deployment environment.
293-
294-
**403 errors across multiple replicas**
295-
296-
All server instances must share the same secret. If each replica generates its own secret, a URL signed by one instance will fail verification on another. Set `NUXT_SCRIPTS_PROXY_SECRET` as a shared environment variable across all replicas.
297-
298-
**Unexpected `NUXT_SCRIPTS_PROXY_SECRET` in `.env`**
299-
300-
The module only writes this when running `nuxt dev` with a signed endpoint enabled and no secret configured. If you only use client-side scripts (analytics, tracking), the module does not generate a secret. To prevent auto-generation entirely, set `autoGenerateSecret: false`.
301-
302-
**Page tokens expire**
303-
304-
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.
305-
306-
#### Static Generation and SPA Mode
307-
308-
URL signing requires a server runtime to verify HMAC signatures. Two deployment modes cannot support signing:
309-
310-
**`nuxt generate` (SSG) with static hosting**: Prerendered pages contain proxy URLs, but no Nitro server exists at runtime to verify signatures or forward requests. Proxy endpoints will not work on static hosts (GitHub Pages, Cloudflare Pages static, etc.). If you need proxy endpoints with prerendering, deploy to a server target that supports both prerendering and runtime request handling (e.g. Node, Cloudflare Workers, [Vercel](https://vercel.com)).
311-
312-
**`ssr: false` (SPA mode)**: No server-side rendering means no opportunity to sign URLs or embed page tokens. The signing secret lives in server-only runtime config and cannot be accessed from the client. Proxy endpoints still function if deployed with a server, but requests will be unsigned.
313-
314-
::callout{type="info"}
315-
In both cases, the module automatically detects the limitation and skips signing setup. Proxy endpoints remain functional but unprotected. The module logs a warning at build time.
316-
::
209+
Analytics proxy endpoints (Google Analytics, Plausible, etc.) only forward collection payloads and never expose API keys.
317210

318211
## Supported Scripts
319212

docs/content/scripts/bluesky-embed.md

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,6 @@ Nuxt Scripts provides a [`<ScriptBlueskyEmbed>`{lang="html"}](/scripts/bluesky-e
1818
::script-docs{embed}
1919
::
2020

21-
::callout{type="info"}
22-
This script's proxy endpoints use [HMAC URL signing](/docs/guides/first-party#proxy-endpoint-security) when you configure a `NUXT_SCRIPTS_PROXY_SECRET`. See the [security guide](/docs/guides/first-party#proxy-endpoint-security) for setup instructions.
23-
::
24-
2521
This registers the required server API routes (`/_scripts/embed/bluesky` and `/_scripts/embed/bluesky-image`) that handle fetching post data and proxying images.
2622

2723
## [`<ScriptBlueskyEmbed>`{lang="html"}](/scripts/bluesky-embed){lang="html"}

docs/content/scripts/google-maps/2.api/1b.static-map.md

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,6 @@ title: <ScriptGoogleMapsStaticMap>
44

55
Renders a [Google Maps Static API](https://developers.google.com/maps/documentation/maps-static) image. Use standalone for static map previews, or drop into the `#placeholder` slot of [`<ScriptGoogleMaps>`{lang="html"}](/scripts/google-maps/api/script-google-maps) for a loading placeholder.
66

7-
::callout{type="info"}
8-
This script's proxy endpoints use [HMAC URL signing](/docs/guides/first-party#proxy-endpoint-security) when you configure a `NUXT_SCRIPTS_PROXY_SECRET`. See the [security guide](/docs/guides/first-party#proxy-endpoint-security) for setup instructions.
9-
::
10-
117
::script-types{script-key="google-maps" filter="ScriptGoogleMapsStaticMap"}
128
::
139

docs/content/scripts/google-maps/index.md

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,6 @@ You must add this. It registers server proxy routes that keep your API key serve
6363
You can pass `api-key` directly on the `<ScriptGoogleMaps>`{lang="html"} component, but this approach is not recommended, as it exposes your key in client-side requests.
6464
::
6565

66-
::callout{type="info"}
67-
This script's proxy endpoints use [HMAC URL signing](/docs/guides/first-party#proxy-endpoint-security) when you configure a `NUXT_SCRIPTS_PROXY_SECRET`. See the [security guide](/docs/guides/first-party#proxy-endpoint-security) for setup instructions.
68-
::
69-
7066
See [Billing & Permissions](/scripts/google-maps/guides/billing) for API costs and required permissions.
7167

7268
## Quick Start

docs/content/scripts/gravatar.md

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,6 @@ links:
2020
::script-docs
2121
::
2222

23-
::callout{type="info"}
24-
This script's proxy endpoints use [HMAC URL signing](/docs/guides/first-party#proxy-endpoint-security) when you configure a `NUXT_SCRIPTS_PROXY_SECRET`. See the [security guide](/docs/guides/first-party#proxy-endpoint-security) for setup instructions.
25-
::
26-
2723
## [`<ScriptGravatar>`{lang="html"}](/scripts/gravatar){lang="html"}
2824

2925
The [`<ScriptGravatar>`{lang="html"}](/scripts/gravatar){lang="html"} component renders a Gravatar avatar for a given email address. All requests are proxied through your server - Gravatar never sees your user's IP address or headers.

docs/content/scripts/instagram-embed.md

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,6 @@ Nuxt Scripts provides a [`<ScriptInstagramEmbed>`{lang="html"}](/scripts/instagr
1818
::script-docs{embed}
1919
::
2020

21-
::callout{type="info"}
22-
This script's proxy endpoints use [HMAC URL signing](/docs/guides/first-party#proxy-endpoint-security) when you configure a `NUXT_SCRIPTS_PROXY_SECRET`. See the [security guide](/docs/guides/first-party#proxy-endpoint-security) for setup instructions.
23-
::
24-
2521
This registers the required server API routes (`/_scripts/embed/instagram`, `/_scripts/embed/instagram-image`, and `/_scripts/embed/instagram-asset`) that handle fetching embed HTML and proxying images/assets.
2622

2723
## [`<ScriptInstagramEmbed>`{lang="html"}](/scripts/instagram-embed){lang="html"}

docs/content/scripts/x-embed.md

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,6 @@ Nuxt Scripts provides a [`<ScriptXEmbed>`{lang="html"}](/scripts/x-embed){lang="
1818
::script-docs{embed}
1919
::
2020

21-
::callout{type="info"}
22-
This script's proxy endpoints use [HMAC URL signing](/docs/guides/first-party#proxy-endpoint-security) when you configure a `NUXT_SCRIPTS_PROXY_SECRET`. See the [security guide](/docs/guides/first-party#proxy-endpoint-security) for setup instructions.
23-
::
24-
2521
This registers the required server API routes (`/_scripts/embed/x` and `/_scripts/embed/x-image`) that handle fetching tweet data and proxying images.
2622

2723
## [`<ScriptXEmbed>`{lang="html"}](/scripts/x-embed){lang="html"}

packages/script/src/cli.ts

Lines changed: 1 addition & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,12 @@
11
/**
22
* @nuxt/scripts CLI.
33
*
4-
* Currently hosts a single command, `generate-secret`, which produces a
5-
* cryptographically random HMAC secret for `NUXT_SCRIPTS_PROXY_SECRET`. This
6-
* is an alternative to letting the module auto-write a secret into `.env`,
7-
* for users who want explicit control (e.g. teams that commit secrets to a
8-
* vault rather than `.env`).
9-
*
104
* Keep this file zero-dependency: it runs standalone via `npx @nuxt/scripts`
115
* and should boot instantly.
126
*/
137

14-
import { randomBytes } from 'node:crypto'
158
import process from 'node:process'
169

17-
function generateSecret(): void {
18-
const secret = randomBytes(32).toString('hex')
19-
process.stdout.write(
20-
[
21-
'',
22-
' @nuxt/scripts: proxy signing secret',
23-
'',
24-
` Secret: ${secret}`,
25-
'',
26-
' Add this to your environment:',
27-
` NUXT_SCRIPTS_PROXY_SECRET=${secret}`,
28-
'',
29-
' The secret is automatically picked up by the module via runtime config.',
30-
' It must be the same across all deployments and prerender builds so that',
31-
' signed URLs remain valid.',
32-
'',
33-
'',
34-
].join('\n'),
35-
)
36-
}
37-
3810
function showHelp(): void {
3911
process.stdout.write(
4012
[
@@ -44,8 +16,7 @@ function showHelp(): void {
4416
' Usage: npx @nuxt/scripts <command>',
4517
'',
4618
' Commands:',
47-
' generate-secret Generate a signing secret for proxy URL tamper protection',
48-
' help Show this help',
19+
' help Show this help',
4920
'',
5021
'',
5122
].join('\n'),
@@ -57,9 +28,6 @@ const command = process.argv[2]
5728
if (!command || command === 'help' || command === '--help' || command === '-h') {
5829
showHelp()
5930
}
60-
else if (command === 'generate-secret') {
61-
generateSecret()
62-
}
6331
else {
6432
process.stderr.write(`Unknown command: ${command}\n`)
6533
showHelp()

0 commit comments

Comments
 (0)