11import process from 'node:process'
2- import { unlink } from 'node:fs/promises'
2+ import { rm } from 'node:fs/promises'
33import type { Socket } from 'node:net'
44import net from 'node:net'
55import os from 'node:os'
66import fs from 'node:fs' // For sync operations like unlinkSync if needed during setup
77import { pathToFileURL } from 'node:url'
88import { Buffer } from 'node:buffer'
9- import { isAbsolute , join , normalize } from 'pathe'
9+ import { dirname , isAbsolute , join , normalize } from 'pathe'
1010import { directoryToURL , resolveAlias , tryUseNuxt , useNitro } from '@nuxt/kit'
1111import type { EnvironmentModuleNode , ModuleNode , PluginContainer , ViteDevServer , Plugin as VitePlugin } from 'vite'
1212import { getQuery } from 'ufo'
@@ -15,7 +15,6 @@ import { ViteNodeServer } from 'vite-node/server'
1515import { normalizeViteManifest } from 'vue-bundle-renderer'
1616import type { Manifest } from 'vue-bundle-renderer'
1717import type { Nuxt } from '@nuxt/schema'
18- import { provider } from 'std-env'
1918import { resolveModulePath } from 'exsolve'
2019
2120import { isCSS } from '../utils/index.ts'
@@ -125,34 +124,28 @@ function getManifest (nuxt: Nuxt, viteServer: ViteDevServer, clientEntry: string
125124 return manifest
126125}
127126
128- function generateSocketPath ( ) {
127+ export interface SocketPathInfo {
128+ socketPath : string
129+ /** mkdtemp directory we own; cleaned up on close. Undefined for Windows pipes. */
130+ parentDir ?: string
131+ }
132+
133+ // only exported for tests
134+ export function pickSocketPath ( platform : NodeJS . Platform ) : SocketPathInfo {
129135 const uniqueSuffix = `${ process . pid } -${ Date . now ( ) } `
130136 const socketName = `nuxt-vite-node-${ uniqueSuffix } `
131137
132- // Windows: pipe
133- if ( process . platform === 'win32' ) {
134- return join ( String . raw `\\.\pipe` , socketName )
138+ if ( platform === 'win32' ) {
139+ return { socketPath : join ( String . raw `\\.\pipe` , socketName ) }
135140 }
136- // Linux: abstract namespace
137- if ( process . platform === 'linux' ) {
138- const nodeMajor = Number . parseInt ( process . versions . node . split ( '.' ) [ 0 ] ! , 10 )
139- if ( nodeMajor >= 20 && provider !== 'stackblitz' ) {
140- // We avoid abstract sockets in Docker due to performance issues
141- let isDocker = false
142-
143- try {
144- isDocker = fs . existsSync ( '/.dockerenv' ) || ( fs . existsSync ( '/proc/1/cgroup' ) && fs . readFileSync ( '/proc/1/cgroup' , 'utf8' ) . includes ( 'docker' ) )
145- } catch {
146- // Ignore errors checking Docker status
147- }
141+ // place the socket inside a freshly-created 0700 directory to gate access.
142+ const parentDir = fs . mkdtempSync ( join ( os . tmpdir ( ) , 'nuxt-vite-node-' ) )
143+ fs . chmodSync ( parentDir , 0o700 )
144+ return { socketPath : join ( parentDir , `${ socketName } .sock` ) , parentDir }
145+ }
148146
149- if ( ! isDocker ) {
150- return `\0${ socketName } .sock`
151- }
152- }
153- }
154- // Unix socket
155- return join ( os . tmpdir ( ) , `${ socketName } .sock` )
147+ function generateSocketPath ( ) : SocketPathInfo {
148+ return pickSocketPath ( process . platform )
156149}
157150
158151function useInvalidates ( ) {
@@ -186,18 +179,21 @@ export function ViteNodePlugin (nuxt: Nuxt): VitePlugin | undefined {
186179 }
187180
188181 let socketServer : net . Server | undefined
189- const socketPath = generateSocketPath ( )
182+ const { socketPath, parentDir } = generateSocketPath ( )
190183 const { invalidates, markInvalidate, markInvalidates } = useInvalidates ( )
191184
185+ let cleanedUp = false
192186 async function cleanupSocket ( ) {
187+ if ( cleanedUp ) { return }
188+ cleanedUp = true
193189 if ( socketServer && socketServer . listening ) {
194190 await new Promise < void > ( resolveClose => socketServer ! . close ( ( ) => resolveClose ( ) ) )
195191 }
196- if ( socketPath && ! socketPath . startsWith ( '\\\\.\\pipe\\' ) ) {
192+ if ( parentDir ) {
197193 try {
198- await unlink ( socketPath )
194+ await rm ( parentDir , { recursive : true , force : true } )
199195 } catch {
200- // Error is ignored if the file doesn't exist or cannot be unlinked
196+ // mkdtemp directory cleanup is best-effort
201197 }
202198 }
203199 }
@@ -473,26 +469,45 @@ function createViteNodeSocketServer (nuxt: Nuxt, ssrServer: ViteDevServer, clien
473469 throw new Error ( 'Socket path not configured for ViteNodeSocketServer.' )
474470 }
475471
476- // Clean up existing socket file (Unix only)
477- if ( ! currentSocketPath . startsWith ( '\\\\.\\pipe\\' ) ) {
478- try {
479- fs . unlinkSync ( currentSocketPath )
480- } catch ( unlinkError : any ) {
481- if ( unlinkError . code !== 'ENOENT' ) {
482- // Socket cleanup failed, but continue anyway
483- }
484- }
485- }
472+ listenAndRestrict ( server , currentSocketPath )
486473
487- server . listen ( currentSocketPath )
488-
489- server . on ( 'error' , ( ) => {
490- // Server error - will be handled by calling code
491- } )
474+ server . on ( 'error' , ( ) => { } )
492475
493476 return server
494477}
495478
479+ export function listenAndRestrict ( server : net . Server , socketPath : string ) : void {
480+ const isWindowsPipe = socketPath . startsWith ( '\\\\.\\pipe\\' )
481+ if ( isWindowsPipe ) {
482+ server . listen ( socketPath )
483+ return
484+ }
485+ // 1. the 0700 parent directory (created by pickSocketPath) is the access-control
486+ // boundary, since AF_UNIX connect requires +x on every parent.
487+ // 2. The post-listen chmod 0600 protects the socket file itself.
488+ // 3. The umask wrap tightens the in-process window between bind(2) and the chmod,
489+ // where the file would otherwise briefly exist with the default umask
490+ // and be observable to other code.
491+ const previousUmask = process . umask ( 0o077 )
492+ try {
493+ server . listen ( socketPath , ( ) => {
494+ try {
495+ fs . chmodSync ( socketPath , 0o600 )
496+ } catch ( error ) {
497+ console . error ( '[nuxt] Failed to restrict vite-node socket permissions; closing.' , error )
498+ server . close ( )
499+ try {
500+ fs . rmSync ( dirname ( socketPath ) , { recursive : true , force : true } )
501+ } catch {
502+ // mkdtemp directory cleanup is best-effort
503+ }
504+ }
505+ } )
506+ } finally {
507+ process . umask ( previousUmask )
508+ }
509+ }
510+
496511function sendResponse < T extends keyof ViteNodeRequestMap > (
497512 socket : net . Socket ,
498513 id : number ,
0 commit comments