From 96a6ff244d562a2a29bd10312217276799b18e5c Mon Sep 17 00:00:00 2001 From: Manuel Astudillo Date: Sun, 15 Sep 2024 22:17:56 +0200 Subject: [PATCH] fix: correct path generation for LE challenges --- .gitignore | 1 + lib/letsencrypt.ts | 31 +++++++++++++++++++------------ lib/proxy.ts | 7 +++---- yarn.lock | 5 +++++ 4 files changed, 28 insertions(+), 16 deletions(-) diff --git a/.gitignore b/.gitignore index c3fc675..6973dde 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,4 @@ build/Release node_modules dist +.letsencrypt diff --git a/lib/letsencrypt.ts b/lib/letsencrypt.ts index 1c606e5..e691d15 100644 --- a/lib/letsencrypt.ts +++ b/lib/letsencrypt.ts @@ -11,7 +11,7 @@ import url from 'url'; import fs from 'fs'; import pino from 'pino'; -import leChallengeFs from './third-party/le-challenge-fs.js'; +import LeChallengeFs from './third-party/le-challenge-fs.js'; /** * LetsEncrypt certificates are stored like the following: @@ -44,20 +44,27 @@ function init(certPath: string, port: number, logger: pino.Logger 'localhost:port/example.com/' createServer(function (req: IncomingMessage, res: ServerResponse) { - var uri = url.parse(req.url).pathname; - var filename = path.join(certPath, uri); - var isForbiddenPath = uri.length < 3 || filename.indexOf(certPath) !== 0; - - if (isForbiddenPath) { - logger && logger.info('Forbidden request on LetsEncrypt port %s: %s', port, filename); - res.writeHead(403); + if (req.method !== 'GET') { + res.statusCode = 405; // Method Not Allowed res.end(); return; } - logger && logger.info('LetsEncrypt CA trying to validate challenge %s', filename); + const reqPath = url.parse(req.url).pathname; + const basePath = path.resolve(certPath); + const safePath = path.normalize(reqPath).replace(/^(\.\.[\/\\])+/, ''); // Prevent directory traversal + const fullPath = path.join(basePath, safePath); + + if (!fullPath.startsWith(basePath)) { + logger?.info(`Attempted directory traversal attack: ${req.url}`); + res.statusCode = 403; // Forbidden + res.end('Access denied'); + return; + } + + logger?.info('LetsEncrypt CA trying to validate challenge %s', fullPath); - fs.stat(filename, function (err: Error, stats: any) { + fs.stat(fullPath, function (err: Error, stats: any) { if (err || !stats.isFile()) { res.writeHead(404, { 'Content-Type': 'text/plain' }); res.write('404 Not Found\n'); @@ -66,7 +73,7 @@ function init(certPath: string, port: number, logger: pino.Logger { if (/^\/.well-known\/acme-challenge/.test(url)) { - return targetHost + '/' + host; + return `${targetHost}/${host}`; } }; - challengeResolver.priority = 9999; - this.addResolver(challengeResolver); + this.addResolver(challengeResolver, 9999); } setupHttpsProxy(proxy: httpProxy, websocketsUpgrade: any, sslOpts: any) { @@ -703,7 +702,7 @@ export class Redbird { (req).host = target.host; } - if (route.opts.onRequest) { + if (route.opts?.onRequest) { const resultFromRequestHandler = route.opts.onRequest(req, res, target); if (resultFromRequestHandler !== undefined) { this.log?.info('Proxying %s received result from onRequest handler, returning.', src + url); diff --git a/yarn.lock b/yarn.lock index faa7d7d..1b49c1d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1121,6 +1121,11 @@ fs.realpath@^1.0.0: resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== +fsevents@~2.3.2, fsevents@~2.3.3: + version "2.3.3" + resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + function-bind@^1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz"