diff --git a/packages/nuxt-cli/src/dev/listen.ts b/packages/nuxt-cli/src/dev/listen.ts index 2181c6d56..478df2fa8 100644 --- a/packages/nuxt-cli/src/dev/listen.ts +++ b/packages/nuxt-cli/src/dev/listen.ts @@ -74,7 +74,6 @@ export interface Listener { } const ANY_HOSTS = new Set(['', '0.0.0.0', '::']) -const LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '::1']) const DEFAULT_PORTS = { http: 80, https: 443 } as const /** How long in-flight requests have to finish before their sockets are killed. */ @@ -336,7 +335,7 @@ export async function createListener(bound: BoundServer, options: ListenOptions console.log(`${qr ? '' : '\n'}${lines.join('\n')}\n`) } - if (announce && (anyHost || !LOOPBACK_HOSTS.has(hostname))) { + if (announce && !isLoopbackAddress(address.address)) { logger.warn('The dev server is reachable from the network without authentication: anyone who can connect can read the app, build errors and source code.') } diff --git a/packages/nuxt-cli/test/unit/listen.spec.ts b/packages/nuxt-cli/test/unit/listen.spec.ts index 60bd9be63..2caf0c7ba 100644 --- a/packages/nuxt-cli/test/unit/listen.spec.ts +++ b/packages/nuxt-cli/test/unit/listen.spec.ts @@ -6,6 +6,7 @@ import { networkInterfaces } from 'node:os' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { canOpenBrowser, copyURL, formatDisplayURL, getNetworkAddresses, isReusePortSupported, listen, matchesBoundTarget, openBrowser, parsePort, resolveOpenCommand, validateHostname, writeClipboard } from '../../src/dev/listen' +import { logger } from '../../src/utils/logger' const writeText = vi.hoisted(() => vi.fn()) const isolatedEnvironment = vi.hoisted(() => ({ current: undefined as string | undefined })) @@ -277,6 +278,34 @@ describe('listen', () => { expect(listener.url).toBe(`http://localhost:${listener.address.port}/`) }) + describe('network warning', () => { + const NETWORK_WARNING = 'The dev server is reachable from the network without authentication' + + async function warnsFor(hostname: string) { + const warn = vi.spyOn(logger, 'warn').mockImplementation(() => {}) + try { + await start({ port: 0, hostname }) + return warn.mock.calls.some(([message]) => String(message).includes(NETWORK_WARNING)) + } + finally { + warn.mockRestore() + } + } + + it('should warn when bound to all interfaces', async () => { + expect(await warnsFor('0.0.0.0')).toBe(true) + }) + + it('should not warn when bound to loopback', async () => { + expect(await warnsFor('127.0.0.1')).toBe(false) + }) + + // Only Linux binds `127.0.0.0/8` addresses beyond `127.0.0.1` without configuration. + it.runIf(process.platform === 'linux')('should not warn when bound to another loopback address', async () => { + expect(await warnsFor('127.0.0.2')).toBe(false) + }) + }) + it('should reject an invalid port before binding', async () => { await expect(start({ port: 'nope' })).rejects.toThrow('Invalid port `nope`; expected an integer between 0 and 65535.') })