Skip to content

Commit 96a6ff2

Browse files
committed
fix: correct path generation for LE challenges
1 parent 0e42773 commit 96a6ff2

File tree

4 files changed

+28
-16
lines changed

4 files changed

+28
-16
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,4 @@ build/Release
2525
node_modules
2626

2727
dist
28+
.letsencrypt

lib/letsencrypt.ts

+19-12
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import url from 'url';
1111
import fs from 'fs';
1212
import pino from 'pino';
1313

14-
import leChallengeFs from './third-party/le-challenge-fs.js';
14+
import LeChallengeFs from './third-party/le-challenge-fs.js';
1515

1616
/**
1717
* LetsEncrypt certificates are stored like the following:
@@ -44,20 +44,27 @@ function init(certPath: string, port: number, logger: pino.Logger<never, boolean
4444

4545
// we need to proxy for example: 'example.com/.well-known/acme-challenge' -> 'localhost:port/example.com/'
4646
createServer(function (req: IncomingMessage, res: ServerResponse) {
47-
var uri = url.parse(req.url).pathname;
48-
var filename = path.join(certPath, uri);
49-
var isForbiddenPath = uri.length < 3 || filename.indexOf(certPath) !== 0;
50-
51-
if (isForbiddenPath) {
52-
logger && logger.info('Forbidden request on LetsEncrypt port %s: %s', port, filename);
53-
res.writeHead(403);
47+
if (req.method !== 'GET') {
48+
res.statusCode = 405; // Method Not Allowed
5449
res.end();
5550
return;
5651
}
5752

58-
logger && logger.info('LetsEncrypt CA trying to validate challenge %s', filename);
53+
const reqPath = url.parse(req.url).pathname;
54+
const basePath = path.resolve(certPath);
55+
const safePath = path.normalize(reqPath).replace(/^(\.\.[\/\\])+/, ''); // Prevent directory traversal
56+
const fullPath = path.join(basePath, safePath);
57+
58+
if (!fullPath.startsWith(basePath)) {
59+
logger?.info(`Attempted directory traversal attack: ${req.url}`);
60+
res.statusCode = 403; // Forbidden
61+
res.end('Access denied');
62+
return;
63+
}
64+
65+
logger?.info('LetsEncrypt CA trying to validate challenge %s', fullPath);
5966

60-
fs.stat(filename, function (err: Error, stats: any) {
67+
fs.stat(fullPath, function (err: Error, stats: any) {
6168
if (err || !stats.isFile()) {
6269
res.writeHead(404, { 'Content-Type': 'text/plain' });
6370
res.write('404 Not Found\n');
@@ -66,7 +73,7 @@ function init(certPath: string, port: number, logger: pino.Logger<never, boolean
6673
}
6774

6875
res.writeHead(200);
69-
fs.createReadStream(filename, 'binary').pipe(res);
76+
fs.createReadStream(fullPath, 'binary').pipe(res);
7077
});
7178
}).listen(port);
7279
}
@@ -93,7 +100,7 @@ async function getCertificates(
93100
const leStore = (await import('le-store-certbot')).create(leStoreConfig);
94101

95102
// ACME Challenge Handlers
96-
const leChallenge = leChallengeFs.create({
103+
const leChallenge = LeChallengeFs.create({
97104
loopbackPort: loopbackPort,
98105
webrootPath,
99106
debug: false,

lib/proxy.ts

+3-4
Original file line numberDiff line numberDiff line change
@@ -303,11 +303,10 @@ export class Redbird {
303303
const targetHost = 'http://' + this.letsencryptHost;
304304
const challengeResolver = (host: string, url: string) => {
305305
if (/^\/.well-known\/acme-challenge/.test(url)) {
306-
return targetHost + '/' + host;
306+
return `${targetHost}/${host}`;
307307
}
308308
};
309-
challengeResolver.priority = 9999;
310-
this.addResolver(challengeResolver);
309+
this.addResolver(challengeResolver, 9999);
311310
}
312311

313312
setupHttpsProxy(proxy: httpProxy, websocketsUpgrade: any, sslOpts: any) {
@@ -703,7 +702,7 @@ export class Redbird {
703702
(<any>req).host = target.host;
704703
}
705704

706-
if (route.opts.onRequest) {
705+
if (route.opts?.onRequest) {
707706
const resultFromRequestHandler = route.opts.onRequest(req, res, target);
708707
if (resultFromRequestHandler !== undefined) {
709708
this.log?.info('Proxying %s received result from onRequest handler, returning.', src + url);

yarn.lock

+5
Original file line numberDiff line numberDiff line change
@@ -1121,6 +1121,11 @@ fs.realpath@^1.0.0:
11211121
resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz"
11221122
integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
11231123

1124+
fsevents@~2.3.2, fsevents@~2.3.3:
1125+
version "2.3.3"
1126+
resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz"
1127+
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
1128+
11241129
function-bind@^1.1.2:
11251130
version "1.1.2"
11261131
resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz"

0 commit comments

Comments
 (0)