From 4e3962fcb5f4a1a093dd31b6dd123cdfae65c80b Mon Sep 17 00:00:00 2001 From: "Anthony Fu (via agent)" Date: Wed, 29 Jul 2026 08:55:35 +0000 Subject: [PATCH] fix(devtools): continue hardening the RPC surface Follow-up to the dev auth token work, covering the rest of the RPC surface reachable over the HMR WebSocket: - structured-clone-es: require >=2.0.1 (catalog + tree-wide override) so the deserializer allowlists constructors, and add onGeneralError on the server channel so a frame it now rejects is dropped instead of crashing dev. - enableTimeline, customTabAction, getServerRuntimeConfig: require the dev auth token (config write + restart, custom-tab action handler, and runtimeConfig/env exposure respectively). - getServerConfig: strip private runtimeConfig from this unauthenticated, eagerly-read payload (keeps public/app). - requestForAuth: always derive the origin server-side rather than trusting a client-supplied one. - RPC channel: only accept same-origin WebSocket connections, leaving the HMR socket itself untouched. --- package.json | 1 + packages/devtools-kit/src/_types/rpc.ts | 8 ++-- .../devtools/client/composables/dev-auth.ts | 2 +- packages/devtools/client/composables/state.ts | 3 +- .../client/pages/modules/custom-[name].vue | 4 +- .../client/pages/modules/timeline.vue | 5 ++- .../devtools/src/server-rpc/custom-tabs.ts | 5 ++- packages/devtools/src/server-rpc/general.ts | 27 +++++++++++-- packages/devtools/src/server-rpc/index.ts | 24 +++++++++++- packages/devtools/src/server-rpc/timeline.ts | 5 ++- packages/devtools/src/utils/rpc-origin.ts | 36 ++++++++++++++++++ .../test/rpc-deserialize-safety.test.ts | 38 +++++++++++++++++++ packages/devtools/test/rpc-origin.test.ts | 30 +++++++++++++++ pnpm-lock.yaml | 18 ++++----- pnpm-workspace.yaml | 2 +- 15 files changed, 178 insertions(+), 30 deletions(-) create mode 100644 packages/devtools/src/utils/rpc-origin.ts create mode 100644 packages/devtools/test/rpc-deserialize-safety.test.ts create mode 100644 packages/devtools/test/rpc-origin.test.ts diff --git a/package.json b/package.json index 18046c2a66..0545b3937f 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "esbuild": "catalog:buildtools", "rollup": "catalog:buildtools", "semver": "catalog:prod", + "structured-clone-es": "catalog:frontend", "typescript": "catalog:cli", "unimport": "catalog:types", "vite": "catalog:buildtools", diff --git a/packages/devtools-kit/src/_types/rpc.ts b/packages/devtools-kit/src/_types/rpc.ts index 5bae976553..988aab3217 100644 --- a/packages/devtools-kit/src/_types/rpc.ts +++ b/packages/devtools-kit/src/_types/rpc.ts @@ -15,7 +15,7 @@ export interface ServerFunctions { getServerConfig: () => NuxtOptions getServerDebugContext: () => Promise getServerData: (token: string) => Promise - getServerRuntimeConfig: () => Record + getServerRuntimeConfig: (token: string) => Promise> getModuleOptions: () => ModuleOptions getComponents: () => Component[] getComponentsRelationships: () => Promise @@ -66,16 +66,16 @@ export interface ServerFunctions { // Actions telemetryEvent: (payload: object, immediate?: boolean) => void - customTabAction: (name: string, action: number) => Promise + customTabAction: (token: string, name: string, action: number) => Promise runWizard: (token: string, name: T, ...args: GetWizardArgs) => Promise openInEditor: (token: string, filepath: string) => Promise restartNuxt: (token: string, hard?: boolean) => Promise installNuxtModule: (token: string, name: string, dry?: boolean) => Promise uninstallNuxtModule: (token: string, name: string, dry?: boolean) => Promise - enableTimeline: (dry: boolean) => Promise<[string, string]> + enableTimeline: (token: string, dry: boolean) => Promise<[string, string]> // Dev Token - requestForAuth: (info?: string, origin?: string) => Promise + requestForAuth: (info?: string) => Promise verifyAuthToken: (token: string) => Promise } diff --git a/packages/devtools/client/composables/dev-auth.ts b/packages/devtools/client/composables/dev-auth.ts index 33bc6e1836..6bd2e6c93a 100644 --- a/packages/devtools/client/composables/dev-auth.ts +++ b/packages/devtools/client/composables/dev-auth.ts @@ -64,7 +64,7 @@ export async function requestForAuth() { userAgentInfo.os.version, userAgentInfo.device.type, ].filter(i => i).join(' ') - return await rpc.requestForAuth(desc, window.location.origin) + return await rpc.requestForAuth(desc) } async function authConfirmAction() { diff --git a/packages/devtools/client/composables/state.ts b/packages/devtools/client/composables/state.ts index 8a5ce5c589..f44c4781ab 100644 --- a/packages/devtools/client/composables/state.ts +++ b/packages/devtools/client/composables/state.ts @@ -4,6 +4,7 @@ import { objectPick } from '@antfu/utils' import { computed } from 'vue' import { useFetch } from '#app/composables/fetch' import { useClientRouter } from './client' +import { ensureDevAuthToken } from './dev-auth' import { rpc } from './rpc' import { useAsyncState } from './utils' @@ -44,7 +45,7 @@ export function useServerDebugContext() { } export function useServerRuntimeConfig() { - return useAsyncState('getServerRuntimeConfig', () => rpc.getServerRuntimeConfig()) + return useAsyncState('getServerRuntimeConfig', async () => rpc.getServerRuntimeConfig(await ensureDevAuthToken())) } export function useModuleOptions() { diff --git a/packages/devtools/client/pages/modules/custom-[name].vue b/packages/devtools/client/pages/modules/custom-[name].vue index b6254b4008..3e29f09f64 100644 --- a/packages/devtools/client/pages/modules/custom-[name].vue +++ b/packages/devtools/client/pages/modules/custom-[name].vue @@ -3,7 +3,7 @@ import type { ModuleCustomTab } from '~/../src/types' import { computed, onMounted } from 'vue' import { useRoute, useRouter } from '#app/composables/router' import { definePageMeta } from '#imports' -import { isDevAuthed, requestForAuth } from '~/composables/dev-auth' +import { ensureDevAuthToken, isDevAuthed, requestForAuth } from '~/composables/dev-auth' import { rpc } from '~/composables/rpc' import { useAllTabs } from '~/composables/state-tabs' @@ -68,7 +68,7 @@ onMounted(() => { :title="tab.view.title || tab.title" :description="tab.view.description" :actions="tab.view.actions" - @action="idx => rpc.customTabAction(tab!.name, idx)" + @action="async idx => rpc.customTabAction(await ensureDevAuthToken(), tab!.name, idx)" />