diff --git a/README.md b/README.md index d7435d8..ca3b139 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,8 @@ request. Rewrite the prefix to the specified string. Default: `''`. +Destination paths containing dot-segments that could escape the rewritten prefix are rejected with a `400` response. This check also accounts for percent-encoded `/` and `\` path separators. + ### `preHandler` A `preHandler` to be applied on all routes. Useful for performing actions before the proxy is executed (e.g. check for authentication). diff --git a/index.js b/index.js index e0936cd..509ef9b 100644 --- a/index.js +++ b/index.js @@ -116,7 +116,9 @@ function validateDestination (dest) { throw invalidDestination() } - if (decoded === '..' || decoded.includes('/..') || decoded.includes('../')) { + decoded = decoded.replaceAll('\\', '/') + + if (decoded.split('/').includes('..')) { throw invalidDestination() } } diff --git a/test/http-prefix-rewrite.js b/test/http-prefix-rewrite.js index eaa4181..307c001 100644 --- a/test/http-prefix-rewrite.js +++ b/test/http-prefix-rewrite.js @@ -51,7 +51,11 @@ test('HTTP paths cannot escape rewritePrefix', async t => { '/pub/dir\\..\\..\\secret', '/pub/dir/../../secret', '/pub/%2E%2E/secret', - '/pub/..%2F..%2Fsecret' + '/pub/..%2F..%2Fsecret', + '/pub/dir%5c..%5c..%5csecret', + '/pub/dir%5C..%5C..%5Csecret', + '/pub/%5c..%5csecret', + '/pub/%5C..%5Csecret' ] for (const path of escapePaths) { const escaped = await request(port, path) @@ -63,9 +67,9 @@ test('HTTP paths cannot escape rewritePrefix', async t => { t.assert.strictEqual(regular.statusCode, 200) t.assert.strictEqual(regular.body, 'internal:/internal/foo') - const encodedBackslashes = await request(port, '/pub/dir%5C..%5C..%5Csecret') - t.assert.strictEqual(encodedBackslashes.statusCode, 200) - t.assert.strictEqual(encodedBackslashes.body, 'internal:/internal/dir%5C..%5C..%5Csecret') + const encodedBackslash = await request(port, '/pub/foo%5Cbar') + t.assert.strictEqual(encodedBackslash.statusCode, 200) + t.assert.strictEqual(encodedBackslash.body, 'internal:/internal/foo%5Cbar') }) test('HTTP paths are validated against a dynamic upstream once per request', async t => { diff --git a/test/ws-prefix-rewrite.js b/test/ws-prefix-rewrite.js index eebff9d..31de2d4 100644 --- a/test/ws-prefix-rewrite.js +++ b/test/ws-prefix-rewrite.js @@ -320,7 +320,11 @@ const escapeVectors = [ ['/api/%2E%2E/secret', '/secret', parameterizedProxyOptions], ['/api/..%2F..%2Fsecret', '/secret', parameterizedProxyOptions], ['/api/..\\..\\secret', '/secret', parameterizedProxyOptions], - ['/api/..%5C..%5Csecret', '/secret', parameterizedProxyOptions] + ['/api/..%5C..%5Csecret', '/secret', parameterizedProxyOptions], + ['/pub/%5c..%5csecret', '/secret'], + ['/pub/%5C..%5Csecret', '/secret'], + ['/pub/%5c..%5c..%5csecret', '/secret'], + ['/pub/%5C..%5C..%5Csecret', '/secret'] ] for (const [vector, escapedPath, proxyOptions = defaultProxyOptions] of escapeVectors) { diff --git a/types/index.d.ts b/types/index.d.ts index 0e59686..7fd048a 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -99,6 +99,7 @@ declare namespace fastifyHttpProxy { export interface FastifyHttpProxyOptions extends FastifyReplyFromOptions { upstream: string; prefix?: string; + /** Rewritten destination prefix; paths that traverse outside it are rejected. */ rewritePrefix?: string; proxyPayloads?: boolean; preHandler?: ProxyPreHandlerHookHandler;