@fastify/reply-from 12.6.1 → 12.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.github/workflows/ci.yml
CHANGED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
name: Lock Threads
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
schedule:
|
|
5
|
+
- cron: '0 0 1 * *'
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
|
|
8
|
+
concurrency:
|
|
9
|
+
group: lock
|
|
10
|
+
|
|
11
|
+
permissions:
|
|
12
|
+
contents: read
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
lock-threads:
|
|
16
|
+
permissions:
|
|
17
|
+
issues: write
|
|
18
|
+
pull-requests: write
|
|
19
|
+
uses: fastify/workflows/.github/workflows/lock-threads.yml@v6
|
package/index.js
CHANGED
|
@@ -11,6 +11,7 @@ const {
|
|
|
11
11
|
filterPseudoHeaders,
|
|
12
12
|
copyHeaders,
|
|
13
13
|
stripHttp1ConnectionHeaders,
|
|
14
|
+
getConnectionHeaders,
|
|
14
15
|
buildURL
|
|
15
16
|
} = require('./lib/utils')
|
|
16
17
|
|
|
@@ -89,6 +90,18 @@ const fastifyReplyFrom = fp(function from (fastify, opts, next) {
|
|
|
89
90
|
|
|
90
91
|
const sourceHttp2 = req.httpVersionMajor === 2
|
|
91
92
|
const headers = sourceHttp2 ? filterPseudoHeaders(req.headers) : { ...req.headers }
|
|
93
|
+
|
|
94
|
+
// Strip client-supplied Connection header and all header names listed in it
|
|
95
|
+
// before rewriteRequestHeaders runs. This prevents clients from stripping
|
|
96
|
+
// headers added by the proxy itself.
|
|
97
|
+
const connectionHeaderNames = getConnectionHeaders(headers)
|
|
98
|
+
if (headers.connection || connectionHeaderNames.length > 0) {
|
|
99
|
+
delete headers.connection
|
|
100
|
+
for (let i = 0; i < connectionHeaderNames.length; i++) {
|
|
101
|
+
delete headers[connectionHeaderNames[i]]
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
92
105
|
headers.host = url.host
|
|
93
106
|
const qs = getQueryString(url.search, req.url, opts, this.request)
|
|
94
107
|
let body = ''
|
package/lib/utils.js
CHANGED
|
@@ -92,7 +92,12 @@ function stripHttp1ConnectionHeaders (headers) {
|
|
|
92
92
|
|
|
93
93
|
// issue ref: https://github.com/fastify/fast-proxy/issues/42
|
|
94
94
|
function buildURL (source, reqBase) {
|
|
95
|
-
|
|
95
|
+
// Check for path traversal: '..' must be a complete path segment to be an
|
|
96
|
+
// attack. A bare substring match incorrectly rejects legitimate URLs that
|
|
97
|
+
// contain '...' (e.g. Next.js catch-all routes like '[...slug]').
|
|
98
|
+
// See: https://github.com/fastify/fastify-reply-from/issues/460
|
|
99
|
+
const decoded = decodeURIComponent(source)
|
|
100
|
+
if (decoded === '..' || decoded.includes('/..') || decoded.includes('../')) {
|
|
96
101
|
const err = new Error('source/request contain invalid characters')
|
|
97
102
|
err.statusCode = 400
|
|
98
103
|
throw err
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fastify/reply-from",
|
|
3
|
-
"version": "12.6.
|
|
3
|
+
"version": "12.6.2",
|
|
4
4
|
"description": "forward your HTTP request to another server, for fastify",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -68,9 +68,9 @@
|
|
|
68
68
|
"fastify": "^5.0.0",
|
|
69
69
|
"form-data": "^4.0.0",
|
|
70
70
|
"h2url": "^0.2.0",
|
|
71
|
-
"neostandard": "^0.
|
|
71
|
+
"neostandard": "^0.13.0",
|
|
72
72
|
"nock": "^14.0.0",
|
|
73
|
-
"proxy": "^
|
|
73
|
+
"proxy": "^4.0.0",
|
|
74
74
|
"proxyquire": "^2.1.3",
|
|
75
75
|
"split2": "^4.2.0",
|
|
76
76
|
"tsd": "^0.33.0"
|
package/test/build-url.test.js
CHANGED
|
@@ -77,6 +77,15 @@ test('should throw when trying to override base', async (t) => {
|
|
|
77
77
|
await Promise.all(promises)
|
|
78
78
|
})
|
|
79
79
|
|
|
80
|
+
test('should not throw on URLs containing "..." (e.g. Next.js catch-all routes)', (t) => {
|
|
81
|
+
t.plan(2)
|
|
82
|
+
// Next.js catch-all routes like [...slug] appear percent-encoded in static chunk URLs.
|
|
83
|
+
// These contain '...' as a substring but are not path traversal.
|
|
84
|
+
// See: https://github.com/fastify/fastify-reply-from/issues/460
|
|
85
|
+
t.assert.doesNotThrow(() => buildURL('/_next/static/chunks/pages/%5B...slug%5D.js', 'http://localhost'))
|
|
86
|
+
t.assert.doesNotThrow(() => buildURL('/_next/static/chunks/pages/%5B%5B...slug%5D%5D.js', 'http://localhost'))
|
|
87
|
+
})
|
|
88
|
+
|
|
80
89
|
test('should throw on path traversal attempts', (t) => {
|
|
81
90
|
t.assert.throws(
|
|
82
91
|
() => buildURL('/foo/bar/../', 'http://localhost'),
|
|
@@ -230,3 +230,79 @@ t.test('handles Connection header with keep-alive and custom headers (undici)',
|
|
|
230
230
|
t.assert.strictEqual(result.statusCode, 200)
|
|
231
231
|
t.assert.strictEqual(result.body, 'ok')
|
|
232
232
|
})
|
|
233
|
+
|
|
234
|
+
t.test('does not strip headers added by rewriteRequestHeaders (undici)', async (t) => {
|
|
235
|
+
t.plan(4)
|
|
236
|
+
const instance = Fastify()
|
|
237
|
+
instance.register(From)
|
|
238
|
+
|
|
239
|
+
t.after(() => instance.close())
|
|
240
|
+
|
|
241
|
+
let seenForwardedBy
|
|
242
|
+
const target = http.createServer((req, res) => {
|
|
243
|
+
seenForwardedBy = req.headers['x-forwarded-by']
|
|
244
|
+
t.assert.ok('request proxied')
|
|
245
|
+
res.statusCode = 200
|
|
246
|
+
res.setHeader('Content-Type', 'text/plain')
|
|
247
|
+
res.end('ok')
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
instance.get('/', (_request, reply) => {
|
|
251
|
+
reply.from(`http://localhost:${target.address().port}`, {
|
|
252
|
+
rewriteRequestHeaders: (_request, headers) => {
|
|
253
|
+
return { ...headers, 'x-forwarded-by': 'fastify-proxy' }
|
|
254
|
+
}
|
|
255
|
+
})
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
t.after(() => target.close())
|
|
259
|
+
|
|
260
|
+
await new Promise((resolve) => instance.listen({ port: 0 }, resolve))
|
|
261
|
+
await new Promise((resolve) => target.listen({ port: 0 }, resolve))
|
|
262
|
+
|
|
263
|
+
const result = await makeRequest(instance.server.address().port, {
|
|
264
|
+
Connection: 'X-Forwarded-By'
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
t.assert.strictEqual(result.statusCode, 200)
|
|
268
|
+
t.assert.strictEqual(result.body, 'ok')
|
|
269
|
+
t.assert.strictEqual(seenForwardedBy, 'fastify-proxy', 'X-Forwarded-By should not be stripped')
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
t.test('does not strip headers added by rewriteRequestHeaders (http)', async (t) => {
|
|
273
|
+
t.plan(4)
|
|
274
|
+
const instance = Fastify()
|
|
275
|
+
instance.register(From, { undici: false })
|
|
276
|
+
|
|
277
|
+
t.after(() => instance.close())
|
|
278
|
+
|
|
279
|
+
let seenForwardedBy
|
|
280
|
+
const target = http.createServer((req, res) => {
|
|
281
|
+
seenForwardedBy = req.headers['x-forwarded-by']
|
|
282
|
+
t.assert.ok('request proxied')
|
|
283
|
+
res.statusCode = 200
|
|
284
|
+
res.setHeader('Content-Type', 'text/plain')
|
|
285
|
+
res.end('ok')
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
instance.get('/', (_request, reply) => {
|
|
289
|
+
reply.from(`http://localhost:${target.address().port}`, {
|
|
290
|
+
rewriteRequestHeaders: (_request, headers) => {
|
|
291
|
+
return { ...headers, 'x-forwarded-by': 'fastify-proxy' }
|
|
292
|
+
}
|
|
293
|
+
})
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
t.after(() => target.close())
|
|
297
|
+
|
|
298
|
+
await new Promise((resolve) => instance.listen({ port: 0 }, resolve))
|
|
299
|
+
await new Promise((resolve) => target.listen({ port: 0 }, resolve))
|
|
300
|
+
|
|
301
|
+
const result = await makeRequest(instance.server.address().port, {
|
|
302
|
+
Connection: 'X-Forwarded-By'
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
t.assert.strictEqual(result.statusCode, 200)
|
|
306
|
+
t.assert.strictEqual(result.body, 'ok')
|
|
307
|
+
t.assert.strictEqual(seenForwardedBy, 'fastify-proxy', 'X-Forwarded-By should not be stripped')
|
|
308
|
+
})
|