Skip to content

Commit 26d7c6e

Browse files
committed
fix(dev,curl,task): ignore lock file urls that don't match machine
1 parent d8fc5d0 commit 26d7c6e

2 files changed

Lines changed: 52 additions & 2 deletions

File tree

packages/nuxt-cli/src/utils/dev-server.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { existsSync, readFileSync } from 'node:fs'
2+
import { isIP } from 'node:net'
3+
import { networkInterfaces } from 'node:os'
24
import { styleText } from 'node:util'
35

46
import { join, resolve } from 'pathe'
@@ -51,11 +53,35 @@ export async function findDevServer(cwd: string, buildDir?: string): Promise<Run
5153
const dir = buildDir ? resolve(cwd, buildDir) : await resolveLockDir(cwd)
5254

5355
const lock = readActiveLock(dir)
54-
if (lock?.command === 'dev' && lock.url) {
56+
if (lock?.command === 'dev' && lock.url && isOwnAddress(lock.url)) {
5557
return { url: toLoopback(lock.url), pid: lock.pid, cwd: lock.cwd }
5658
}
5759
}
5860

61+
/**
62+
* Whether `url` names this machine. A dev server records the address it bound,
63+
* which is always local, but the lock file it is read from lives in the project
64+
* and so may have been written by someone else. Requests carrying the user's
65+
* headers and payloads are only sent to an origin this machine could have bound.
66+
*/
67+
function isOwnAddress(url: string): boolean {
68+
let hostname: string
69+
try {
70+
hostname = new URL(url).hostname
71+
}
72+
catch {
73+
return false
74+
}
75+
if (isLocalHost(hostname)) {
76+
return true
77+
}
78+
const address = hostname.startsWith('[') ? hostname.slice(1, -1) : hostname
79+
if (!isIP(address)) {
80+
return false
81+
}
82+
return Object.values(networkInterfaces()).flat().some(info => info?.address === address)
83+
}
84+
5985
/**
6086
* A dev server bound to every interface records the wildcard it was given, but
6187
* that is not an address to connect to: Windows refuses `0.0.0.0` outright, and

packages/nuxt-cli/test/unit/dev-server.spec.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
2-
import { tmpdir } from 'node:os'
2+
import { networkInterfaces, tmpdir } from 'node:os'
33
import process from 'node:process'
44

55
import { join } from 'pathe'
@@ -100,6 +100,30 @@ describe('findDevServer', () => {
100100

101101
await expect(findDevServer(cwd, 'custom')).resolves.toMatchObject({ pid: 424242 })
102102
})
103+
104+
it('ignores a lock whose URL names another machine', async () => {
105+
await writeLock('.nuxt', { url: 'https://attacker.example' })
106+
await expect(findDevServer(cwd)).resolves.toBeUndefined()
107+
108+
await writeLock('.nuxt', { url: 'http://203.0.113.7:3000' })
109+
await expect(findDevServer(cwd)).resolves.toBeUndefined()
110+
})
111+
112+
it('accepts a lock bound to one of this machine\'s own addresses', async () => {
113+
const address = Object.values(networkInterfaces()).flat().find(info => info && !info.internal && info.family === 'IPv4')?.address
114+
if (!address) {
115+
return
116+
}
117+
await writeLock('.nuxt', { url: `http://${address}:3000` })
118+
119+
await expect(findDevServer(cwd)).resolves.toMatchObject({ url: `http://${address}:3000` })
120+
})
121+
122+
it('accepts a lock bound to IPv6 loopback', async () => {
123+
await writeLock('.nuxt', { url: 'http://[::1]:3000' })
124+
125+
await expect(findDevServer(cwd)).resolves.toMatchObject({ url: 'http://[::1]:3000' })
126+
})
103127
})
104128

105129
describe('findNitroDevWorker', () => {

0 commit comments

Comments
 (0)