Skip to content

Commit

Permalink
fix: correct path generation for LE challenges
Browse files Browse the repository at this point in the history
  • Loading branch information
manast committed Sep 15, 2024
1 parent 0e42773 commit 96a6ff2
Show file tree
Hide file tree
Showing 4 changed files with 28 additions and 16 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ build/Release
node_modules

dist
.letsencrypt
31 changes: 19 additions & 12 deletions lib/letsencrypt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -44,20 +44,27 @@ function init(certPath: string, port: number, logger: pino.Logger<never, boolean

// we need to proxy for example: 'example.com/.well-known/acme-challenge' -> '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');
Expand All @@ -66,7 +73,7 @@ function init(certPath: string, port: number, logger: pino.Logger<never, boolean
}

res.writeHead(200);
fs.createReadStream(filename, 'binary').pipe(res);
fs.createReadStream(fullPath, 'binary').pipe(res);
});
}).listen(port);
}
Expand All @@ -93,7 +100,7 @@ async function getCertificates(
const leStore = (await import('le-store-certbot')).create(leStoreConfig);

// ACME Challenge Handlers
const leChallenge = leChallengeFs.create({
const leChallenge = LeChallengeFs.create({
loopbackPort: loopbackPort,
webrootPath,
debug: false,
Expand Down
7 changes: 3 additions & 4 deletions lib/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,11 +303,10 @@ export class Redbird {
const targetHost = 'http://' + this.letsencryptHost;
const challengeResolver = (host: string, url: string) => {
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) {
Expand Down Expand Up @@ -703,7 +702,7 @@ export class Redbird {
(<any>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);
Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down

0 comments on commit 96a6ff2

Please sign in to comment.