@@ -11,7 +11,7 @@ import url from 'url';
11
11
import fs from 'fs' ;
12
12
import pino from 'pino' ;
13
13
14
- import leChallengeFs from './third-party/le-challenge-fs.js' ;
14
+ import LeChallengeFs from './third-party/le-challenge-fs.js' ;
15
15
16
16
/**
17
17
* LetsEncrypt certificates are stored like the following:
@@ -44,20 +44,27 @@ function init(certPath: string, port: number, logger: pino.Logger<never, boolean
44
44
45
45
// we need to proxy for example: 'example.com/.well-known/acme-challenge' -> 'localhost:port/example.com/'
46
46
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
54
49
res . end ( ) ;
55
50
return ;
56
51
}
57
52
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 ) ;
59
66
60
- fs . stat ( filename , function ( err : Error , stats : any ) {
67
+ fs . stat ( fullPath , function ( err : Error , stats : any ) {
61
68
if ( err || ! stats . isFile ( ) ) {
62
69
res . writeHead ( 404 , { 'Content-Type' : 'text/plain' } ) ;
63
70
res . write ( '404 Not Found\n' ) ;
@@ -66,7 +73,7 @@ function init(certPath: string, port: number, logger: pino.Logger<never, boolean
66
73
}
67
74
68
75
res . writeHead ( 200 ) ;
69
- fs . createReadStream ( filename , 'binary' ) . pipe ( res ) ;
76
+ fs . createReadStream ( fullPath , 'binary' ) . pipe ( res ) ;
70
77
} ) ;
71
78
} ) . listen ( port ) ;
72
79
}
@@ -93,7 +100,7 @@ async function getCertificates(
93
100
const leStore = ( await import ( 'le-store-certbot' ) ) . create ( leStoreConfig ) ;
94
101
95
102
// ACME Challenge Handlers
96
- const leChallenge = leChallengeFs . create ( {
103
+ const leChallenge = LeChallengeFs . create ( {
97
104
loopbackPort : loopbackPort ,
98
105
webrootPath,
99
106
debug : false ,
0 commit comments