Skip to content

Commit c4f6908

Browse files
ijjkeps1lon
authored andcommitted
router-server: guard upgrade proxy against absolute-url SSRF (#77) (#102)
* test: add rewrite request smuggling coverage * router-server: guard upgrade proxy against absolute-url SSRF * test: relax rewrite smuggling socket timeout * router-server: preserve local websocket upgrades (cherry picked from commit 846b7f825db2205c79e48b6fdc4acd8908235a43)
1 parent 6c72e0b commit c4f6908

2 files changed

Lines changed: 82 additions & 9 deletions

File tree

packages/next/src/server/lib/router-server.ts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -817,21 +817,26 @@ export async function initialize(opts: {
817817
)
818818
},
819819
})
820-
const { matchedOutput, parsedUrl } = await resolveRoutes({
821-
req,
822-
res,
823-
isUpgradeReq: true,
824-
signal: signalFromNodeResponse(socket),
825-
})
820+
const { finished, matchedOutput, parsedUrl, statusCode } =
821+
await resolveRoutes({
822+
req,
823+
res,
824+
isUpgradeReq: true,
825+
signal: signalFromNodeResponse(socket),
826+
})
826827

827828
// TODO: allow upgrade requests to pages/app paths?
828829
// this was not previously supported
829830
if (matchedOutput) {
830831
return socket.end()
831832
}
832833

833-
if (parsedUrl.protocol) {
834-
return await proxyRequest(req, socket, parsedUrl, head)
834+
if (finished && parsedUrl.protocol) {
835+
if (!statusCode) {
836+
return await proxyRequest(req, socket, parsedUrl, head)
837+
}
838+
839+
return socket.end()
835840
}
836841

837842
// If there's no matched output, we don't handle the request as user's

test/e2e/rewrite-request-smuggling/rewrite-request-smuggling.test.ts

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ describe('rewrite-request-smuggling', () => {
99
return
1010
}
1111

12+
const ssrfProbePath = '/secret-upgrade'
13+
const ssrfProbeBody =
14+
'SSRF_CONFIRMED: You reached the internal service at 127.0.0.1'
1215
let backend: http.Server
1316
let backendPort: number
1417
let intermediary: http.Server
@@ -56,11 +59,53 @@ describe('rewrite-request-smuggling', () => {
5659
socket.write(payload)
5760
})
5861
socket.once('error', reject)
59-
socket.setTimeout(1000, () => socket.destroy())
62+
socket.setTimeout(5000, () => socket.destroy())
6063
socket.once('close', () => resolve())
6164
})
6265
}
6366

67+
async function sendAbsoluteUrlUpgradePayload({
68+
nextPort,
69+
targetPort,
70+
}: {
71+
nextPort: number
72+
targetPort: number
73+
}) {
74+
const payload = Buffer.from(
75+
`GET http://127.0.0.1:${targetPort}${ssrfProbePath} HTTP/1.1\r\nHost: 127.0.0.1:${nextPort}\r\nConnection: Upgrade\r\nUpgrade: websocket\r\nSec-WebSocket-Version: 13\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n\r\n`,
76+
'latin1'
77+
)
78+
79+
return await new Promise<string>((resolve, reject) => {
80+
const socket = net.createConnection({
81+
host: '127.0.0.1',
82+
port: nextPort,
83+
})
84+
const chunks: Buffer[] = []
85+
let settled = false
86+
87+
const finish = () => {
88+
if (settled) return
89+
settled = true
90+
resolve(Buffer.concat(chunks).toString('latin1'))
91+
}
92+
93+
socket.once('connect', () => {
94+
socket.write(payload)
95+
})
96+
socket.on('data', (chunk) => {
97+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))
98+
})
99+
socket.once('error', (err) => {
100+
if (settled) return
101+
settled = true
102+
reject(err)
103+
})
104+
socket.setTimeout(1000, () => socket.destroy())
105+
socket.once('close', finish)
106+
})
107+
}
108+
64109
beforeAll(async () => {
65110
backendPort = await findPort()
66111
intermediaryPort = await findPort()
@@ -84,6 +129,14 @@ describe('rewrite-request-smuggling', () => {
84129
res.end('not-found')
85130
})
86131

132+
backend.on('upgrade', (req, socket) => {
133+
backendRequests.push(`${req.method} ${req.url}`)
134+
socket.write(
135+
`HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Type: text/plain\r\nContent-Length: ${Buffer.byteLength(ssrfProbeBody)}\r\n\r\n${ssrfProbeBody}`
136+
)
137+
socket.end()
138+
})
139+
87140
intermediary = http.createServer((req, res) => {
88141
const connectionHeader = Array.isArray(req.headers['connection'])
89142
? req.headers['connection'].join(',')
@@ -231,4 +284,19 @@ describe('rewrite-request-smuggling', () => {
231284
})
232285
expect(backendRequests).not.toContain('GET /secret')
233286
})
287+
288+
it('does not proxy upgrade requests with absolute URLs without an external rewrite', async () => {
289+
backendRequests.length = 0
290+
291+
const nextPort = Number(new URL(next.url).port)
292+
const response = await sendAbsoluteUrlUpgradePayload({
293+
nextPort,
294+
targetPort: backendPort,
295+
})
296+
297+
expect(response).not.toContain(ssrfProbeBody)
298+
expect(
299+
backendRequests.some((request) => request.includes(ssrfProbePath))
300+
).toBe(false)
301+
})
234302
})

0 commit comments

Comments
 (0)