Skip to content

Commit 3b4a191

Browse files
authored
refactor(security)!: remove signing and billable Maps proxies (#788)
1 parent 0342a4e commit 3b4a191

48 files changed

Lines changed: 264 additions & 2195 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

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

Lines changed: 3 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -256,144 +256,15 @@ Platform-level rewrites bypass the privacy anonymization layer. The proxy handle
256256

257257
## Proxy Endpoint Security
258258

259-
Some proxy endpoints inject server-side API keys or forward arbitrary resource requests. This includes Google Static Maps, Geocode, Gravatar, and embed image proxies. Anyone can call an unprotected endpoint directly and consume your API quota.
260-
261-
### HMAC URL Signing
262-
263-
Optional HMAC signing accepts either an exact URL generated during SSR or prerender, or a request carrying a valid page token. Requests with neither credential receive a `403`. The [signing implementation](https://github.com/nuxt/scripts/blob/main/packages/script/src/runtime/server/utils/sign.ts) canonicalizes each URL before generating its HMAC-SHA256 signature.
264-
265-
#### Setup
266-
267-
Generate a signing secret with the CLI:
268-
269-
```bash
270-
npx @nuxt/scripts generate-secret
271-
```
272-
273-
Then set it as an environment variable:
274-
275-
```bash
276-
NUXT_SCRIPTS_PROXY_SECRET=<your-secret>
277-
```
278-
279-
Or configure it directly:
280-
281-
```ts [nuxt.config.ts]
282-
exportdefaultdefineNuxtConfig({
283-
scripts: {
284-
security: {
285-
secret: process.env.NUXT_SCRIPTS_PROXY_SECRET,
286-
}
287-
}
288-
})
289-
```
290-
291-
#### Verification Modes
292-
293-
The module uses two verification modes:
294-
295-
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.
296-
297-
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.
298-
299-
Page tokens are deliberately broader than URL signatures: anyone who can read a valid token can change the parameters and signed proxy path until it expires. Treat them as short-lived authorization for the proxy group, not proof that a request matches an exact server-generated URL.
300-
301-
#### Development
302-
303-
In development, the module generates a secret and writes it to `.env` on the first run.
304-
305-
#### Production
306-
307-
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.
308-
309-
::callout{type="warning"}
310-
Without a secret, proxy endpoints remain functional but unprotected. The module logs a warning at startup when it detects signed endpoints without a secret.
311-
::
312-
313-
#### Signed Endpoints
314-
315-
The following proxy endpoints require signing when you configure a secret:
316-
317-
| Script | Endpoints |
318-
|--------|-----------|
319-
|**Google Maps**|`/_scripts/proxy/google-static-maps`, `/_scripts/proxy/google-maps-geocode`|
320-
|**Gravatar**|`/_scripts/proxy/gravatar`|
321-
|**Bluesky**|`/_scripts/embed/bluesky`, `/_scripts/embed/bluesky-image`|
322-
|**Instagram**|`/_scripts/embed/instagram`, `/_scripts/embed/instagram-image`, `/_scripts/embed/instagram-asset`|
323-
|**X (Twitter)**|`/_scripts/embed/x`, `/_scripts/embed/x-image`|
324-
325-
The generic analytics proxy does not use signing. It accepts only upstream domains registered at build time and does not inject the protected API keys used by the signed endpoints above.
326-
327-
#### Configuration Reference
328-
329-
```ts [nuxt.config.ts]
330-
exportdefaultdefineNuxtConfig({
331-
scripts: {
332-
security: {
333-
// HMAC secret for signing proxy URLs.
334-
// Falls back to process.env.NUXT_SCRIPTS_PROXY_SECRET.
335-
secret: undefined,
336-
// Auto-generate and persist a secret to .env in dev mode.
337-
// Set to false to disable.
338-
autoGenerateSecret: true,
339-
// Page-token lifetime in seconds (default: 3600).
340-
pageTokenMaxAge: 3600,
341-
}
342-
}
343-
})
344-
```
345-
346-
To disable proxy security entirely, set `security` to `false`:
347-
348-
```ts [nuxt.config.ts]
349-
exportdefaultdefineNuxtConfig({
350-
scripts: {
351-
// No secret is resolved or auto-generated, no page token is added to the
352-
// SSR payload, and proxy endpoints pass requests through unverified.
353-
security: false,
354-
}
355-
})
356-
```
357-
358-
Disable security when you need a deterministic SSR payload, such as one used to compute a stable response `etag`. Without it, proxy endpoints still work but remain open to quota abuse and arbitrary requests to their allowlisted upstreams.
259+
Embed, avatar, and analytics proxy routes are public resources. They do not contain server-side API keys. Each route accepts only the upstream hosts and request shapes declared by its integration.
359260

360261
::callout{type="warning"}
361262
Runtime proxy fetches validate the initial upstream URL and every redirect target before requesting it. Direct local, private, link-local, and reserved targets are rejected on every runtime; Node deployments also validate and pin DNS results before opening the socket. Image routes reject active content types such as HTML and SVG. The Instagram embed route restricts post and stylesheet hosts, then sanitizes the returned fragment before client rendering.
362263
::
363264

364-
#### Troubleshooting
365-
366-
**Signed URLs return 403 after deploy**
367-
368-
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.
369-
370-
**403 errors across multiple replicas**
371-
372-
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.
373-
374-
**Unexpected `NUXT_SCRIPTS_PROXY_SECRET` in `.env`**
375-
376-
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`.
377-
378-
**Page tokens expire**
379-
380-
Page tokens are valid for 1 hour by default. If a user leaves a tab open longer than `security.pageTokenMaxAge`, client-side proxy requests will start returning 403. The page will recover on the next navigation or refresh.
265+
Nuxt Scripts does not proxy Google Maps requests. Static Maps loads from Google with the public browser key, while location lookup uses the Maps JavaScript Places service. Apply website and API restrictions to the key, then configure Google Cloud quotas to cap spend. See [Google Maps Platform security guidance](https://developers.google.com/maps/api-security-best-practices).
381266

382-
**Proxy token changes the response payload on every request**
383-
384-
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 you accept the wider request-authorization boundary described above.
385-
386-
#### Static Generation and SPA Mode
387-
388-
URL signing requires a server runtime to verify HMAC signatures. Two deployment modes cannot support signing:
389-
390-
**`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 such as GitHub Pages. If you need proxy endpoints alongside prerendered pages, deploy to a server target that supports runtime request handling; [Vercel supports both static and server-rendered Nuxt deployments](https://vercel.com/docs/frameworks/full-stack/nuxt).
391-
392-
**`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.
393-
394-
::callout{type="info"}
395-
The module skips signing setup and logs a build warning in both cases. In SPA mode with a deployed server, registered endpoints remain available without signature checks. A fully static host has no runtime endpoint to receive the request.
396-
::
267+
If public embed traffic needs request limits, configure them at your deployment edge or add application middleware. Nitro 2 route rules do not provide a portable rate limiter.
397268

398269
## Supported Scripts
399270

‎docs/content/docs/3.api/5.nuxt-config.md‎

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -107,22 +107,6 @@ export default defineNuxtConfig({
107107
})
108108
```
109109

110-
## `security`{lang="ts"}
111-
112-
- Type: `false | { secret?: string, autoGenerateSecret?: boolean, pageTokenMaxAge?: number }`{lang="ts"}
113-
- Default: `undefined` (the module configures signing after it registers a protected endpoint)
114-
115-
Configures HMAC protection for proxy endpoints that expose server-side API keys or forward arbitrary external resources. The secret falls back to `NUXT_SCRIPTS_PROXY_SECRET`. In development, the module generates and persists a secret when you enable a signed endpoint without providing one.
116-
117-
Production does not auto-generate a secret. If the module finds a protected endpoint without one, it warns and leaves that endpoint functional but unsigned. URL signing is also unavailable for `ssr: false` and static Nitro presets because they have no server runtime to verify signatures.
118-
119-
-`security.secret`{lang="ts"}: the HMAC secret
120-
-`security.autoGenerateSecret`{lang="ts"}: whether development may create the secret; defaults to `true`
121-
-`security.pageTokenMaxAge`{lang="ts"}: client page-token lifetime in seconds; defaults to `3600`
122-
-`security: false`{lang="ts"}: disables signing and lets registered endpoints accept unsigned requests
123-
124-
See [Proxy Endpoint Security](/docs/guides/first-party#proxy-endpoint-security) for setup and deployment constraints.
125-
126110
## Partytown (Web Worker) :badge[Experimental]{color="amber"}
127111

128112
Load individual scripts in a web worker using [Partytown](https://partytown.qwik.dev/). A registry `trigger` is still required to generate a global call, but it does not defer the Partytown tag: the current implementation writes that tag into the server-rendered HTML.
@@ -250,10 +234,3 @@ Cache duration for bundled scripts in milliseconds. Scripts older than this will
250234
Generates a Subresource Integrity (SRI) hash for each bundled script and adds `integrity` with `crossorigin="anonymous"`.
251235

252236
Browsers compare the downloaded script with its declared hash before executing it; see MDN's [Subresource Integrity](https://developer.mozilla.org/en-US/docs/Web/Security/Defenses/Subresource_Integrity) guide.
253-
254-
## `googleStaticMapsProxy`{lang="ts"}
255-
256-
- Type: `{ enabled?: boolean, cacheMaxAge?: number }`{lang="ts"}
257-
- Default: `{ enabled: false, cacheMaxAge: 3600 }`
258-
259-
Controls the legacy Google Static Maps proxy switch and its response cache duration in seconds. Registering `googleMaps` also enables the Static Maps and geocoding endpoints. See the [Google Maps Static Map API](/scripts/google-maps/api/static-map) for component usage.

‎docs/content/scripts/bluesky-embed.md‎

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,6 @@ links:
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
Enabling the integration registers `/_scripts/embed/bluesky` for post data and `/_scripts/embed/bluesky-image` for images.
2622

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

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

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,10 @@
11
---
22
title: <ScriptGoogleMapsStaticMap>
3-
description: Render a Google Maps Static API image directly or through the built-in server proxy.
3+
description: Render a Google Maps Static API image directly from Google.
44
---
55

66
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.
77

8-
::callout{type="info"}
9-
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.
10-
::
11-
12-
::callout{color="amber"}
13-
Google's [Maps Platform FAQ](https://developers.google.com/maps/faq#static_map) requires browser pages to load Static Maps images directly from Google rather than store and serve copies. Because the built-in proxy caches and serves the image, pass an explicit `api-key` to bypass it and review the current Google Maps terms for your use case.
14-
::
15-
168
::script-types{script-key="google-maps"filter="ScriptGoogleMapsStaticMap"}
179
::
1810

@@ -75,7 +67,7 @@ Use inside [`<ScriptGoogleMaps>`{lang="html"}](/scripts/google-maps/api/script-g
7567
|`language`|`string`|| Language code for map labels. |
7668
|`region`|`string`|| Region bias. |
7769
|`signature`|`string`||[Google Maps URL signature](https://developers.google.com/maps/digital-signature) for a direct request. Use it with an explicit `apiKey`; a signature does not replace the key. |
78-
|`apiKey`|`string`|| API key override. Takes priority over the proxy; the component falls back to the server-side key when omitted. |
70+
|`apiKey`|`string`|| API key override. Falls back to the public `googleMaps` registry key when omitted. |
7971
|`width`|`number \| string`|`640`| CSS width for the container. |
8072
|`height`|`number \| string`|`400`| CSS height for the container. |
8173
|`loading`|`'eager' \| 'lazy'`|`'lazy'`| Image loading strategy. |
@@ -92,14 +84,12 @@ When `size` is not provided, the component:
9284

9385
Set `size` explicitly to bypass auto-measurement.
9486

95-
## Proxy Support
96-
97-
Configuring `googleMaps` in `scripts.registry` enables a server-side proxy automatically. The component routes requests through it unless you provide an explicit `apiKey` prop. Passing the prop produces a direct `https://maps.googleapis.com/maps/api/staticmap` image URL.
87+
## API Key Security
9888

99-
The built-in proxy cannot use a client-provided `signature` because the server appends the API key after it receives the query. Pass both `apiKey` and `signature` to make a signed direct request.
89+
The component loads the image directly from `maps.googleapis.com`. Restrict the key to your website and the Maps Static API before production use. See [Google Maps Platform security guidance](https://developers.google.com/maps/api-security-best-practices).
10090

10191
::callout{color="amber"}
102-
The component currently builds its proxy URL with the default `/_scripts` prefix. If you configure a custom `scripts.prefix`, static-map proxy requests still use `/_scripts/proxy/google-static-maps` and will miss the relocated route. Pass `apiKey` to use Google's direct URL, or keep the default prefix until this is fixed.
92+
Google digital signatures cover the exact request URL. When passing `signature`, also pass an explicit `size` and keep every signed parameter stable. Generate the signature on a trusted server. Never expose the URL signing secret to the browser.
10393
::
10494

10595
## Slots

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

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
title: Google Maps
3-
description: Load interactive maps on demand and proxy Static Maps or geocoding requests.
3+
description: Load interactive maps, static maps, and location search on demand.
44
links:
55
- label: useScriptGoogleMaps
66
icon: i-simple-icons-github
@@ -47,23 +47,10 @@ export default defineNuxtConfig({
4747
NUXT_PUBLIC_SCRIPTS_GOOGLE_MAPS_API_KEY=<YOUR_API_KEY>
4848
```
4949

50-
Registering Google Maps also adds server proxy routes that keep the key out of static-map and geocoding request URLs:
51-
52-
-`/_scripts/proxy/google-static-maps` for placeholder images
53-
-`/_scripts/proxy/google-maps-geocode` for location search
54-
5550
Add `trigger: 'onNuxtReady'` to the registry entry only when you want the interactive Maps API to load globally. It bypasses the component's default interaction delay because the shared script instance is already loading.
5651

5752
::callout{color="amber"}
58-
The Maps JavaScript API still sends the key to the browser when the interactive map loads. Follow Google's [API security guidance](https://developers.google.com/maps/api-security-best-practices): restrict keys by application and API, and use separate keys for client-side and server-side services when possible. Passing `api-key` directly on `<ScriptGoogleMaps>`{lang="html"} also exposes it in the client bundle, whereas runtime config lets you vary the key by deployment.
59-
::
60-
61-
::callout{color="amber"}
62-
Google's [Maps Platform FAQ](https://developers.google.com/maps/faq#static_map) requires browser pages to load Static Maps images directly from Google. The current static-map proxy caches and serves those images, so pass an explicit `api-key` to `<ScriptGoogleMapsStaticMap>`{lang="html"} to bypass the proxy and review the Maps Platform terms before using that component.
63-
::
64-
65-
::callout{type="info"}
66-
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.
53+
The Maps JavaScript and Static Maps APIs send this key to the browser. Follow Google's [API security guidance](https://developers.google.com/maps/api-security-best-practices): apply a Websites application restriction, allow only the APIs this site uses, and configure quota limits. Runtime config keeps deployment values configurable; it does not make a `NUXT_PUBLIC_` key secret.
6754
::
6855

6956
See [Billing & Permissions](/scripts/google-maps/guides/billing) for API costs and required permissions.

‎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. The avatar image request is proxied through your server, so Gravatar does not receive the user's IP address from that request.

‎docs/content/scripts/instagram-embed.md‎

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,6 @@ links:
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 @@ links:
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/bin/cli.mjs‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,8 @@
11
#!/usr/bin/env node
2-
import('../dist/cli.mjs')
2+
importprocessfrom'node:process'
3+
import{runCli}from'../dist/cli.mjs'
4+
5+
process.exitCode=runCli(process.argv.slice(2),{
6+
writeStdout: value=>process.stdout.write(value),
7+
writeStderr: value=>process.stderr.write(value),
8+
})

0 commit comments

Comments
 (0)