@@ -10,11 +10,14 @@ const getPaths = require("./getPaths");
10
10
const cacheStore = new WeakMap ( ) ;
11
11
12
12
/**
13
+ * @template T
13
14
* @param {Function } fn
14
- * @param {{ cache?: Map<any, any> } } [cache]
15
+ * @param {{ cache?: Map<string, { data: T }> } | undefined } cache
16
+ * @param {(value: T) => T } callback
15
17
* @returns {any }
16
18
*/
17
- const mem = ( fn , { cache = new Map ( ) } = { } ) => {
19
+ // @ts -ignore
20
+ const mem = ( fn , { cache = new Map ( ) } = { } , callback ) => {
18
21
/**
19
22
* @param {any } arguments_
20
23
* @return {any }
@@ -27,7 +30,8 @@ const mem = (fn, { cache = new Map() } = {}) => {
27
30
return cacheItem . data ;
28
31
}
29
32
30
- const result = fn . apply ( this , arguments_ ) ;
33
+ let result = fn . apply ( this , arguments_ ) ;
34
+ result = callback ( result ) ;
31
35
32
36
cache . set ( key , {
33
37
data : result ,
@@ -40,20 +44,52 @@ const mem = (fn, { cache = new Map() } = {}) => {
40
44
41
45
return memoized ;
42
46
} ;
43
- const memoizedParse = mem ( parse ) ;
47
+ // eslint-disable-next-line no-undefined
48
+ const memoizedParse = mem ( parse , undefined , ( value ) => {
49
+ if ( value . pathname ) {
50
+ // eslint-disable-next-line no-param-reassign
51
+ value . pathname = decode ( value . pathname ) ;
52
+ }
53
+
54
+ return value ;
55
+ } ) ;
56
+
57
+ const UP_PATH_REGEXP = / (?: ^ | [ \\ / ] ) \. \. (?: [ \\ / ] | $ ) / ;
58
+
59
+ /**
60
+ * @typedef {Object } Extra
61
+ * @property {import("fs").Stats= } stats
62
+ * @property {number= } errorCode
63
+ */
64
+
65
+ /**
66
+ * decodeURIComponent.
67
+ *
68
+ * Allows V8 to only deoptimize this fn instead of all of send().
69
+ *
70
+ * @param {string } input
71
+ * @returns {string }
72
+ */
73
+
74
+ function decode ( input ) {
75
+ return querystring . unescape ( input ) ;
76
+ }
44
77
45
78
/**
46
79
* @template {IncomingMessage} Request
47
80
* @template {ServerResponse} Response
48
81
* @param {import("../index.js").Context<Request, Response> } context
49
82
* @param {string } url
83
+ * @param {Extra= } extra
50
84
* @returns {string | undefined }
51
85
*/
52
- function getFilenameFromUrl ( context , url ) {
86
+ function getFilenameFromUrl ( context , url , extra = { } ) {
53
87
const { options } = context ;
54
88
const paths = getPaths ( context ) ;
55
89
90
+ /** @type {string | undefined } */
56
91
let foundFilename ;
92
+ /** @type {URL } */
57
93
let urlObject ;
58
94
59
95
try {
@@ -64,7 +100,9 @@ function getFilenameFromUrl(context, url) {
64
100
}
65
101
66
102
for ( const { publicPath, outputPath } of paths ) {
103
+ /** @type {string | undefined } */
67
104
let filename ;
105
+ /** @type {URL } */
68
106
let publicPathObject ;
69
107
70
108
try {
@@ -78,39 +116,51 @@ function getFilenameFromUrl(context, url) {
78
116
continue ;
79
117
}
80
118
81
- if (
82
- urlObject . pathname &&
83
- urlObject . pathname . startsWith ( publicPathObject . pathname )
84
- ) {
85
- filename = outputPath ;
119
+ const { pathname } = urlObject ;
120
+ const { pathname : publicPathPathname } = publicPathObject ;
86
121
87
- // Strip the `pathname` property from the `publicPath` option from the start of requested url
88
- // `/complex/foo.js` => `foo.js`
89
- const pathname = urlObject . pathname . slice (
90
- publicPathObject . pathname . length
91
- ) ;
122
+ if ( pathname && pathname . startsWith ( publicPathPathname ) ) {
123
+ // Null byte(s)
124
+ if ( pathname . includes ( "\0" ) ) {
125
+ // eslint-disable-next-line no-param-reassign
126
+ extra . errorCode = 400 ;
127
+
128
+ return ;
129
+ }
130
+
131
+ // ".." is malicious
132
+ if ( UP_PATH_REGEXP . test ( path . normalize ( `./${ pathname } ` ) ) ) {
133
+ // eslint-disable-next-line no-param-reassign
134
+ extra . errorCode = 403 ;
92
135
93
- if ( pathname ) {
94
- filename = path . join ( outputPath , querystring . unescape ( pathname ) ) ;
136
+ return ;
95
137
}
96
138
97
- let fsStats ;
139
+ // Strip the `pathname` property from the `publicPath` option from the start of requested url
140
+ // `/complex/foo.js` => `foo.js`
141
+ // and add outputPath
142
+ // `foo.js` => `/home/user/my-project/dist/foo.js`
143
+ filename = path . join (
144
+ outputPath ,
145
+ pathname . slice ( publicPathPathname . length )
146
+ ) ;
98
147
99
148
try {
100
- fsStats =
149
+ // eslint-disable-next-line no-param-reassign
150
+ extra . stats =
101
151
/** @type {import("fs").statSync } */
102
152
( context . outputFileSystem . statSync ) ( filename ) ;
103
153
} catch ( _ignoreError ) {
104
154
// eslint-disable-next-line no-continue
105
155
continue ;
106
156
}
107
157
108
- if ( fsStats . isFile ( ) ) {
158
+ if ( extra . stats . isFile ( ) ) {
109
159
foundFilename = filename ;
110
160
111
161
break ;
112
162
} else if (
113
- fsStats . isDirectory ( ) &&
163
+ extra . stats . isDirectory ( ) &&
114
164
( typeof options . index === "undefined" || options . index )
115
165
) {
116
166
const indexValue =
@@ -122,15 +172,16 @@ function getFilenameFromUrl(context, url) {
122
172
filename = path . join ( filename , indexValue ) ;
123
173
124
174
try {
125
- fsStats =
175
+ // eslint-disable-next-line no-param-reassign
176
+ extra . stats =
126
177
/** @type {import("fs").statSync } */
127
178
( context . outputFileSystem . statSync ) ( filename ) ;
128
179
} catch ( __ignoreError ) {
129
180
// eslint-disable-next-line no-continue
130
181
continue ;
131
182
}
132
183
133
- if ( fsStats . isFile ( ) ) {
184
+ if ( extra . stats . isFile ( ) ) {
134
185
foundFilename = filename ;
135
186
136
187
break ;
0 commit comments