woodland 22.0.6 → 22.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.cjs CHANGED
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * @copyright 2026 Jason Mulligan <jason.mulligan@avoidwork.com>
6
6
  * @license BSD-3-Clause
7
- * @version 22.0.6
7
+ * @version 22.1.0
8
8
  */
9
9
  'use strict';
10
10
 
package/dist/woodland.cjs CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * @copyright 2026 Jason Mulligan <jason.mulligan@avoidwork.com>
5
5
  * @license BSD-3-Clause
6
- * @version 22.0.6
6
+ * @version 22.1.0
7
7
  */
8
8
  'use strict';
9
9
 
@@ -53,6 +53,7 @@ const INT_400 = 400;
53
53
  const INT_403 = 403;
54
54
  const INT_404 = 404;
55
55
  const INT_405 = 405;
56
+ const INT_413 = 413;
56
57
  const INT_416 = 416;
57
58
  const INT_500 = 500;
58
59
 
@@ -109,6 +110,7 @@ const INT_60 = 60;
109
110
  const INT_255 = 255;
110
111
  const INT_1e3 = 1e3;
111
112
  const INT_1e4 = 1e4;
113
+ const INT_1e6$1 = 1e6;
112
114
  const INT_8000 = 8000;
113
115
  const INT_NEG_1 = -1;
114
116
 
@@ -227,6 +229,7 @@ const BYTES_SPACE = "bytes ";
227
229
  // EVENT & STREAM CONSTANTS
228
230
  // =============================================================================
229
231
  const EVT_CLOSE = "close";
232
+ const EVT_DATA = "data";
230
233
  const EVT_FINISH = "finish";
231
234
  const EVT_STREAM = "stream";
232
235
  const EVT_CONNECT = "connect";
@@ -246,6 +249,7 @@ const HTTP_VERSION = "HTTP/1.1";
246
249
  const ITEM = "item";
247
250
  const NOTICE = "notice";
248
251
  const SHORT = "short";
252
+ const RESPONSE_TIME_UNIT = " ms";
249
253
  const TO_STRING = "toString";
250
254
  const TRUE = "true";
251
255
  const WARN = "warn";
@@ -724,17 +728,20 @@ function escapeHtml(str = EMPTY) {
724
728
  * @param {Object} req - Request object
725
729
  * @param {Object} res - Response object
726
730
  * @param {EventEmitter} emitter - EventEmitter for error events
727
- * @returns {Function} Error handler function
731
+ * @param {boolean} [exposeErrorMessages=false] - Expose internal error messages to clients
728
732
  */
729
- function createErrorHandler(req, res, emitter) {
730
- return (status = res.statusCode, body) => {
731
- error(req, res, status);
733
+ function createErrorHandler(req, res, emitter, exposeErrorMessages = false) {
734
+ return (inputStatus = res.statusCode, body) => {
735
+ error(req, res, inputStatus);
732
736
  const err = body instanceof Error ? body : new Error(body ?? getStatusText(res.statusCode));
733
737
  emitter.emit(EVT_ERROR, req, res, err);
734
738
  if (req.headers) {
735
739
  delete req.headers.range;
736
740
  }
737
- res.send(err.message || getStatusText(res.statusCode));
741
+ const message = exposeErrorMessages
742
+ ? err.message || getStatusText(res.statusCode)
743
+ : getStatusText(res.statusCode);
744
+ res.send(message);
738
745
  };
739
746
  }
740
747
 
@@ -1350,13 +1357,16 @@ function registerMiddleware(middleware, ignored, methods, rpath, ...fn) {
1350
1357
 
1351
1358
  const DEFAULTS = {
1352
1359
  autoIndex: false,
1360
+ bodyLimit: INT_10 * INT_1e6$1,
1353
1361
  cacheSize: INT_1e3,
1354
1362
  cacheTTL: INT_1e4,
1355
1363
  charset: UTF_8,
1356
1364
  corsExpose: EMPTY,
1357
1365
  defaultHeaders: {},
1358
1366
  digit: INT_3,
1367
+ disableTrace: true,
1359
1368
  etags: true,
1369
+ exposeErrorMessages: false,
1360
1370
  indexes: [INDEX_HTM, INDEX_HTML],
1361
1371
  logging: {},
1362
1372
  origins: [],
@@ -1369,13 +1379,16 @@ const CONFIG_SCHEMA = {
1369
1379
  type: OBJECT,
1370
1380
  properties: {
1371
1381
  autoIndex: { type: BOOLEAN },
1382
+ bodyLimit: { type: NUMBER, minimum: INT_1 },
1372
1383
  cacheSize: { type: NUMBER, minimum: INT_1 },
1373
1384
  cacheTTL: { type: NUMBER, minimum: INT_1 },
1374
1385
  charset: { type: STRING },
1375
1386
  corsExpose: { type: STRING },
1376
1387
  defaultHeaders: { type: OBJECT },
1377
1388
  digit: { type: NUMBER, minimum: INT_1, maximum: INT_10 },
1389
+ disableTrace: { type: BOOLEAN },
1378
1390
  etags: { type: BOOLEAN },
1391
+ exposeErrorMessages: { type: BOOLEAN },
1379
1392
  indexes: { type: ARRAY, items: { type: STRING } },
1380
1393
  logging: { type: OBJECT },
1381
1394
  origins: { type: ARRAY, items: { type: STRING } },
@@ -1851,12 +1864,15 @@ class Woodland extends node_events.EventEmitter {
1851
1864
  #charset;
1852
1865
  #corsExpose;
1853
1866
  #defaultHeaders;
1867
+ #disableTrace;
1854
1868
  #digit;
1855
1869
  #etags;
1870
+ #exposeErrorMessages;
1856
1871
  #indexes;
1857
1872
  #logging;
1858
1873
  #origins;
1859
1874
  #time;
1875
+ #bodyLimit;
1860
1876
  #cache;
1861
1877
  #methods;
1862
1878
  #logger;
@@ -1866,14 +1882,17 @@ class Woodland extends node_events.EventEmitter {
1866
1882
  /**
1867
1883
  * Creates a new Woodland instance
1868
1884
  * @param {Object} [config={}] - Configuration object
1885
+ * @param {number} [config.bodyLimit=10000000] - Max request body size in bytes
1869
1886
  * @param {boolean} [config.autoIndex=false] - Enable automatic directory indexing
1870
1887
  * @param {number} [config.cacheSize=1000] - Size of internal cache
1871
1888
  * @param {number} [config.cacheTTL=10000] - Cache TTL in milliseconds
1872
1889
  * @param {string} [config.charset='utf-8'] - Default charset
1873
1890
  * @param {string} [config.corsExpose=''] - CORS expose headers
1891
+ * @param {boolean} [config.disableTrace=true] - Disable TRACE method (XST vulnerability prevention)
1874
1892
  * @param {Object} [config.defaultHeaders={}] - Default headers to set
1875
1893
  * @param {number} [config.digit=3] - Digit precision for timing
1876
1894
  * @param {boolean} [config.etags=true] - Enable ETags
1895
+ * @param {boolean} [config.exposeErrorMessages=false] - Expose internal error messages to clients
1877
1896
  * @param {Array} [config.indexes=['index.htm','index.html']] - Index files
1878
1897
  * @param {Object} [config.logging={}] - Logging configuration
1879
1898
  * @param {Array} [config.origins=[]] - Allowed CORS origins
@@ -1886,13 +1905,16 @@ class Woodland extends node_events.EventEmitter {
1886
1905
  const validated = validateConfig(config);
1887
1906
 
1888
1907
  this.#autoIndex = validated.autoIndex;
1908
+ this.#bodyLimit = validated.bodyLimit;
1889
1909
  this.#charset = validated.charset;
1890
1910
  this.#corsExpose = validated.corsExpose;
1891
1911
  this.#defaultHeaders = this.#buildFinalHeaders(validated.defaultHeaders, validated.silent);
1912
+ this.#disableTrace = validated.disableTrace;
1892
1913
  this.#digit = validated.digit;
1893
1914
  this.#etags = validated.etags
1894
1915
  ? Object.freeze(tinyEtag.etag({ cacheSize: validated.cacheSize, cacheTTL: validated.cacheTTL }))
1895
1916
  : null;
1917
+ this.#exposeErrorMessages = validated.exposeErrorMessages;
1896
1918
  this.#indexes = [...validated.indexes];
1897
1919
  this.#logging = Object.freeze(validateLogging(validated.logging));
1898
1920
  this.#origins = new Set(validated.origins);
@@ -1905,6 +1927,7 @@ class Woodland extends node_events.EventEmitter {
1905
1927
 
1906
1928
  this.#setupMiddleware();
1907
1929
  this.#setupErrorHandling();
1930
+ this.#setupBodyLimit();
1908
1931
  }
1909
1932
 
1910
1933
  /**
@@ -1977,15 +2000,38 @@ class Woodland extends node_events.EventEmitter {
1977
2000
  );
1978
2001
  }
1979
2002
 
2003
+ /**
2004
+ * Sets up body size limit middleware
2005
+ */
2006
+ #setupBodyLimit() {
2007
+ const maxLimit = this.#bodyLimit;
2008
+ const handler = (req, res, next) => {
2009
+ let size = INT_0;
2010
+ /* node:coverage ignore next 1 */
2011
+ if (typeof req.on !== FUNCTION) {
2012
+ return next();
2013
+ }
2014
+ req.on(EVT_DATA, (chunk) => {
2015
+ size += chunk.length;
2016
+ /* node:coverage ignore next 3 */
2017
+ if (size > maxLimit) {
2018
+ req.destroy();
2019
+ res.error(INT_413);
2020
+ }
2021
+ });
2022
+ next();
2023
+ };
2024
+ this.always(handler);
2025
+ }
2026
+
1980
2027
  /**
1981
2028
  * Determines allowed methods for a URI
1982
2029
  * @param {string} uri - URI to check
1983
2030
  * @param {boolean} [override=false] - Override cache
1984
- * @param {boolean} [isCorsRequest=false] - Whether this is a CORS request
1985
2031
  * @returns {string} Comma-separated list of allowed methods
1986
2032
  */
1987
- #allows(uri, override = false, isCorsRequest = false) {
1988
- const key = `perm${DELIMITER}${uri}${DELIMITER}${isCorsRequest ? INT_1 : INT_0}`;
2033
+ #allows(uri, override = false) {
2034
+ const key = `${DELIMITER}${uri}`;
1989
2035
  let result = !override ? this.#cache.get(key) : void 0;
1990
2036
 
1991
2037
  if (override || result === void 0) {
@@ -1997,7 +2043,7 @@ class Woodland extends node_events.EventEmitter {
1997
2043
  }
1998
2044
  }
1999
2045
 
2000
- const list = this.#buildAllowedList(methodSet, isCorsRequest);
2046
+ const list = this.#buildAllowedList(methodSet);
2001
2047
  result = list.sort().join(COMMA_SPACE);
2002
2048
  this.#cache.set(key, result);
2003
2049
  this.#logger.log(
@@ -2009,12 +2055,11 @@ class Woodland extends node_events.EventEmitter {
2009
2055
  }
2010
2056
 
2011
2057
  /**
2012
- * Builds the list of allowed methods including implicit HEAD and OPTIONS
2058
+ * Builds the list of allowed methods including implicit HEAD, OPTIONS
2013
2059
  * @param {Set} methodSet - Set of explicitly registered methods
2014
- * @param {boolean} isCorsRequest - Whether this is a CORS request
2015
2060
  * @returns {Array} Array of allowed methods
2016
2061
  */
2017
- #buildAllowedList(methodSet, isCorsRequest = false) {
2062
+ #buildAllowedList(methodSet) {
2018
2063
  const list = [...methodSet];
2019
2064
 
2020
2065
  if (list.length > INT_0) {
@@ -2022,8 +2067,7 @@ class Woodland extends node_events.EventEmitter {
2022
2067
  list.push(HEAD);
2023
2068
  }
2024
2069
 
2025
- /* node:coverage ignore next 3 */
2026
- if (!methodSet.has(OPTIONS) && isCorsRequest) {
2070
+ if (!methodSet.has(OPTIONS)) {
2027
2071
  list.push(OPTIONS);
2028
2072
  }
2029
2073
  }
@@ -2046,13 +2090,23 @@ class Woodland extends node_events.EventEmitter {
2046
2090
  return this.use(...args, WILDCARD);
2047
2091
  }
2048
2092
 
2093
+ /**
2094
+ * Registers middleware for a given HTTP method
2095
+ * @param {string} method - HTTP method name
2096
+ * @param {...*} args - Middleware function(s)
2097
+ * @returns {Woodland} Returns self for chaining
2098
+ */
2099
+ #registerMethod(method, ...args) {
2100
+ return this.use(...args, method);
2101
+ }
2102
+
2049
2103
  /**
2050
2104
  * Registers CONNECT middleware
2051
2105
  * @param {...*} args - Middleware function(s)
2052
2106
  * @returns {Woodland} Returns self for chaining
2053
2107
  */
2054
2108
  connect(...args) {
2055
- return this.use(...args, CONNECT);
2109
+ return this.#registerMethod(CONNECT, ...args);
2056
2110
  }
2057
2111
 
2058
2112
  /**
@@ -2074,8 +2128,20 @@ class Woodland extends node_events.EventEmitter {
2074
2128
  req.params = {};
2075
2129
  req.valid = true;
2076
2130
 
2077
- const allowString = this.#allows(parsed.pathname, false, req.cors);
2078
- const headersBatch = this.#buildDefaultHeaders(allowString);
2131
+ const allowString = this.#allows(parsed.pathname);
2132
+ const headersBatch = Object.create(null);
2133
+ headersBatch[ALLOW] = allowString;
2134
+ headersBatch[X_CONTENT_TYPE_OPTIONS] = NO_SNIFF;
2135
+
2136
+ const defaultHeaders = this.#defaultHeaders;
2137
+ const headerCount = defaultHeaders.length;
2138
+ for (let i = INT_0; i < headerCount; i++) {
2139
+ const [key, value] = defaultHeaders[i];
2140
+ if (typeof key === STRING && (typeof value === STRING || typeof value === NUMBER)) {
2141
+ headersBatch[key] = value;
2142
+ }
2143
+ }
2144
+
2079
2145
  req.allow = allowString;
2080
2146
 
2081
2147
  if (timing) {
@@ -2092,28 +2158,6 @@ class Woodland extends node_events.EventEmitter {
2092
2158
  );
2093
2159
  }
2094
2160
 
2095
- /**
2096
- * Builds default headers batch
2097
- * @param {string} allowString - Allow header value
2098
- * @returns {Object} Headers batch object
2099
- */
2100
- #buildDefaultHeaders(allowString) {
2101
- const headersBatch = Object.create(null);
2102
- headersBatch[ALLOW] = allowString;
2103
- headersBatch[X_CONTENT_TYPE_OPTIONS] = NO_SNIFF;
2104
-
2105
- const defaultHeaders = this.#defaultHeaders;
2106
- const headerCount = defaultHeaders.length;
2107
- for (let i = INT_0; i < headerCount; i++) {
2108
- const [key, value] = defaultHeaders[i];
2109
- if (typeof key === STRING && (typeof value === STRING || typeof value === NUMBER)) {
2110
- headersBatch[key] = value;
2111
- }
2112
- }
2113
-
2114
- return headersBatch;
2115
- }
2116
-
2117
2161
  /**
2118
2162
  * Decorates response object with methods and event handlers
2119
2163
  * @param {Object} res - HTTP response object
@@ -2122,7 +2166,7 @@ class Woodland extends node_events.EventEmitter {
2122
2166
  */
2123
2167
  #decorateResponse(res, req, headersBatch) {
2124
2168
  res.locals = {};
2125
- res.error = createErrorHandler(req, res, this);
2169
+ res.error = createErrorHandler(req, res, this, this.#exposeErrorMessages);
2126
2170
  res.header = res.setHeader;
2127
2171
  res.json = createJsonHandler(res);
2128
2172
  res.redirect = createRedirectHandler(res);
@@ -2141,8 +2185,7 @@ class Woodland extends node_events.EventEmitter {
2141
2185
  */
2142
2186
  #addCorsHeaders(req, headersBatch) {
2143
2187
  const origin = req.headers.origin;
2144
- const corsHeaders = req.headers[ACCESS_CONTROL_REQUEST_HEADERS] ?? this.#corsExpose;
2145
- const originAllowed = this.#origins.has(origin);
2188
+ const originAllowed = this.#isSafeOrigin(origin) && this.#origins.has(origin);
2146
2189
  const hasWildcard = this.#origins.has(WILDCARD);
2147
2190
 
2148
2191
  /* node:coverage ignore next 9 */
@@ -2151,21 +2194,44 @@ class Woodland extends node_events.EventEmitter {
2151
2194
  headersBatch[TIMING_ALLOW_ORIGIN] = origin;
2152
2195
  headersBatch[ACCESS_CONTROL_ALLOW_CREDENTIALS] = TRUE;
2153
2196
  headersBatch[ACCESS_CONTROL_ALLOW_METHODS] = req.allow;
2154
-
2155
- if (corsHeaders !== void 0) {
2156
- headersBatch[
2157
- req.method === OPTIONS ? ACCESS_CONTROL_ALLOW_HEADERS : ACCESS_CONTROL_EXPOSE_HEADERS
2158
- ] = corsHeaders;
2159
- }
2160
2197
  } else if (hasWildcard) {
2161
2198
  headersBatch[ACCESS_CONTROL_ALLOW_ORIGIN] = WILDCARD;
2162
2199
  headersBatch[ACCESS_CONTROL_ALLOW_METHODS] = req.allow;
2200
+ }
2163
2201
 
2164
- if (corsHeaders !== void 0) {
2165
- headersBatch[
2166
- req.method === OPTIONS ? ACCESS_CONTROL_ALLOW_HEADERS : ACCESS_CONTROL_EXPOSE_HEADERS
2167
- ] = corsHeaders;
2168
- }
2202
+ const corsHeaders = req.headers[ACCESS_CONTROL_REQUEST_HEADERS] ?? this.#corsExpose;
2203
+ this.#setCorsAllowAndExposeHeaders(headersBatch, req, corsHeaders);
2204
+ }
2205
+
2206
+ /**
2207
+ * Validates origin header for safety
2208
+ * @param {string} origin - Origin header value
2209
+ * @returns {boolean} True if origin is safe
2210
+ */
2211
+ #isSafeOrigin(origin) {
2212
+ if (!origin || typeof origin !== STRING) {
2213
+ return false;
2214
+ }
2215
+ if (origin.length > INT_255) {
2216
+ return false;
2217
+ }
2218
+ if (CONTROL_CHAR_PATTERN.test(origin)) {
2219
+ return false;
2220
+ }
2221
+ return /^https?:\/\//.test(origin);
2222
+ }
2223
+
2224
+ /**
2225
+ * Sets Access-Control-Allow-Headers or Access-Control-Expose-Headers based on method
2226
+ * @param {Object} headersBatch - Headers batch object
2227
+ * @param {Object} req - HTTP request object
2228
+ * @param {*} corsHeaders - CORS headers value
2229
+ */
2230
+ #setCorsAllowAndExposeHeaders(headersBatch, req, corsHeaders) {
2231
+ if (corsHeaders !== void 0) {
2232
+ headersBatch[
2233
+ req.method === OPTIONS ? ACCESS_CONTROL_ALLOW_HEADERS : ACCESS_CONTROL_EXPOSE_HEADERS
2234
+ ] = corsHeaders;
2169
2235
  }
2170
2236
  }
2171
2237
 
@@ -2175,7 +2241,7 @@ class Woodland extends node_events.EventEmitter {
2175
2241
  * @returns {Woodland} Returns self for chaining
2176
2242
  */
2177
2243
  delete(...args) {
2178
- return this.use(...args, DELETE);
2244
+ return this.#registerMethod(DELETE, ...args);
2179
2245
  }
2180
2246
 
2181
2247
  /**
@@ -2185,40 +2251,15 @@ class Woodland extends node_events.EventEmitter {
2185
2251
  * @returns {string} ETag string or empty string
2186
2252
  */
2187
2253
  etag(method, ...args) {
2188
- if (!this.#isHashableMethod(method) || !this.#etagsEnabled()) {
2254
+ if ((method !== GET && method !== HEAD && method !== OPTIONS) || !this.#etags) {
2189
2255
  return EMPTY;
2190
2256
  }
2191
2257
 
2192
- const hashed = this.#hashArgs(args);
2193
- return this.#etags.create(hashed);
2194
- }
2195
-
2196
- /**
2197
- * Checks if a method can be hashed for ETag generation
2198
- * @param {string} method - HTTP method
2199
- * @returns {boolean} True if method is GET, HEAD, or OPTIONS
2200
- */
2201
- #isHashableMethod(method) {
2202
- return method === GET || method === HEAD || method === OPTIONS;
2203
- }
2204
-
2205
- /**
2206
- * Checks if ETags are enabled
2207
- * @returns {boolean} True if ETags are enabled
2208
- */
2209
- #etagsEnabled() {
2210
- return this.#etags !== null;
2211
- }
2212
-
2213
- /**
2214
- * Hashes arguments for ETag generation
2215
- * @param {Array} args - Arguments to hash
2216
- * @returns {string} Hashed string
2217
- */
2218
- #hashArgs(args) {
2219
- return args
2258
+ const hashed = args
2220
2259
  .map((i) => (typeof i !== STRING ? JSON.stringify(i).replace(/^"|"$/g, EMPTY) : i))
2221
2260
  .join(HYPHEN);
2261
+
2262
+ return this.#etags.create(hashed);
2222
2263
  }
2223
2264
 
2224
2265
  /**
@@ -2239,7 +2280,7 @@ class Woodland extends node_events.EventEmitter {
2239
2280
  * @returns {Woodland} Returns self for chaining
2240
2281
  */
2241
2282
  get(...args) {
2242
- return this.use(...args, GET);
2283
+ return this.#registerMethod(GET, ...args);
2243
2284
  }
2244
2285
 
2245
2286
  /**
@@ -2299,8 +2340,8 @@ class Woodland extends node_events.EventEmitter {
2299
2340
  /* node:coverage ignore next 5 */
2300
2341
  if (this.#time && res.getHeader(X_RESPONSE_TIME) === void 0) {
2301
2342
  const diff = req.precise.stop().diff();
2302
- const msValue = Number(diff / 1e6).toFixed(this.#digit);
2303
- res.header(X_RESPONSE_TIME, `${msValue} ms`);
2343
+ const msValue = Number(diff / INT_1e6).toFixed(this.#digit);
2344
+ res.header(X_RESPONSE_TIME, `${msValue}${RESPONSE_TIME_UNIT}`);
2304
2345
  }
2305
2346
 
2306
2347
  return this.#onSend(req, res, body, status, headers);
@@ -2332,7 +2373,7 @@ class Woodland extends node_events.EventEmitter {
2332
2373
  * @returns {Woodland} Returns self for chaining
2333
2374
  */
2334
2375
  options(...args) {
2335
- return this.use(...args, OPTIONS);
2376
+ return this.#registerMethod(OPTIONS, ...args);
2336
2377
  }
2337
2378
 
2338
2379
  /**
@@ -2341,7 +2382,7 @@ class Woodland extends node_events.EventEmitter {
2341
2382
  * @returns {Woodland} Returns self for chaining
2342
2383
  */
2343
2384
  patch(...args) {
2344
- return this.use(...args, PATCH);
2385
+ return this.#registerMethod(PATCH, ...args);
2345
2386
  }
2346
2387
 
2347
2388
  /**
@@ -2350,7 +2391,7 @@ class Woodland extends node_events.EventEmitter {
2350
2391
  * @returns {Woodland} Returns self for chaining
2351
2392
  */
2352
2393
  post(...args) {
2353
- return this.use(...args, POST);
2394
+ return this.#registerMethod(POST, ...args);
2354
2395
  }
2355
2396
 
2356
2397
  /**
@@ -2359,7 +2400,7 @@ class Woodland extends node_events.EventEmitter {
2359
2400
  * @returns {Woodland} Returns self for chaining
2360
2401
  */
2361
2402
  put(...args) {
2362
- return this.use(...args, PUT);
2403
+ return this.#registerMethod(PUT, ...args);
2363
2404
  }
2364
2405
 
2365
2406
  /**
@@ -2473,8 +2514,15 @@ class Woodland extends node_events.EventEmitter {
2473
2514
  * Registers TRACE middleware
2474
2515
  * @param {...*} args - Middleware function(s)
2475
2516
  * @returns {Woodland} Returns self for chaining
2517
+ * @security TRACE method is vulnerable to XST attacks. Disabled by default.
2476
2518
  */
2477
2519
  trace(...args) {
2520
+ if (this.#disableTrace) {
2521
+ this.#logger.log(
2522
+ `type=trace, message="TRACE method is disabled by default (XST vulnerability prevention)"`,
2523
+ );
2524
+ return this;
2525
+ }
2478
2526
  return this.use(...args, TRACE);
2479
2527
  }
2480
2528
 
package/dist/woodland.js CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * @copyright 2026 Jason Mulligan <jason.mulligan@avoidwork.com>
5
5
  * @license BSD-3-Clause
6
- * @version 22.0.6
6
+ * @version 22.1.0
7
7
  */
8
8
  import {STATUS_CODES}from'node:http';import {EventEmitter}from'node:events';import {readFileSync,createReadStream}from'node:fs';import {etag}from'tiny-etag';import {lru}from'tiny-lru';import {precise}from'precise';import {createRequire}from'node:module';import {join,extname,resolve,sep}from'node:path';import {fileURLToPath,URL as URL$1}from'node:url';import mimeDb from'mime-db';import {coerce}from'tiny-coerce';import {Validator}from'jsonschema';import {realpath,stat,readdir}from'node:fs/promises';const __dirname$2 = fileURLToPath(new URL$1(".", import.meta.url));
9
9
  const require$1 = createRequire(import.meta.url);
@@ -36,6 +36,7 @@ const INT_400 = 400;
36
36
  const INT_403 = 403;
37
37
  const INT_404 = 404;
38
38
  const INT_405 = 405;
39
+ const INT_413 = 413;
39
40
  const INT_416 = 416;
40
41
  const INT_500 = 500;
41
42
 
@@ -92,6 +93,7 @@ const INT_60 = 60;
92
93
  const INT_255 = 255;
93
94
  const INT_1e3 = 1e3;
94
95
  const INT_1e4 = 1e4;
96
+ const INT_1e6$1 = 1e6;
95
97
  const INT_8000 = 8000;
96
98
  const INT_NEG_1 = -1;
97
99
 
@@ -210,6 +212,7 @@ const BYTES_SPACE = "bytes ";
210
212
  // EVENT & STREAM CONSTANTS
211
213
  // =============================================================================
212
214
  const EVT_CLOSE = "close";
215
+ const EVT_DATA = "data";
213
216
  const EVT_FINISH = "finish";
214
217
  const EVT_STREAM = "stream";
215
218
  const EVT_CONNECT = "connect";
@@ -229,6 +232,7 @@ const HTTP_VERSION = "HTTP/1.1";
229
232
  const ITEM = "item";
230
233
  const NOTICE = "notice";
231
234
  const SHORT = "short";
235
+ const RESPONSE_TIME_UNIT = " ms";
232
236
  const TO_STRING = "toString";
233
237
  const TRUE = "true";
234
238
  const WARN = "warn";
@@ -705,17 +709,20 @@ function escapeHtml(str = EMPTY) {
705
709
  * @param {Object} req - Request object
706
710
  * @param {Object} res - Response object
707
711
  * @param {EventEmitter} emitter - EventEmitter for error events
708
- * @returns {Function} Error handler function
712
+ * @param {boolean} [exposeErrorMessages=false] - Expose internal error messages to clients
709
713
  */
710
- function createErrorHandler(req, res, emitter) {
711
- return (status = res.statusCode, body) => {
712
- error(req, res, status);
714
+ function createErrorHandler(req, res, emitter, exposeErrorMessages = false) {
715
+ return (inputStatus = res.statusCode, body) => {
716
+ error(req, res, inputStatus);
713
717
  const err = body instanceof Error ? body : new Error(body ?? getStatusText(res.statusCode));
714
718
  emitter.emit(EVT_ERROR, req, res, err);
715
719
  if (req.headers) {
716
720
  delete req.headers.range;
717
721
  }
718
- res.send(err.message || getStatusText(res.statusCode));
722
+ const message = exposeErrorMessages
723
+ ? err.message || getStatusText(res.statusCode)
724
+ : getStatusText(res.statusCode);
725
+ res.send(message);
719
726
  };
720
727
  }
721
728
 
@@ -1325,13 +1332,16 @@ function registerMiddleware(middleware, ignored, methods, rpath, ...fn) {
1325
1332
  });
1326
1333
  }const DEFAULTS = {
1327
1334
  autoIndex: false,
1335
+ bodyLimit: INT_10 * INT_1e6$1,
1328
1336
  cacheSize: INT_1e3,
1329
1337
  cacheTTL: INT_1e4,
1330
1338
  charset: UTF_8,
1331
1339
  corsExpose: EMPTY,
1332
1340
  defaultHeaders: {},
1333
1341
  digit: INT_3,
1342
+ disableTrace: true,
1334
1343
  etags: true,
1344
+ exposeErrorMessages: false,
1335
1345
  indexes: [INDEX_HTM, INDEX_HTML],
1336
1346
  logging: {},
1337
1347
  origins: [],
@@ -1344,13 +1354,16 @@ const CONFIG_SCHEMA = {
1344
1354
  type: OBJECT,
1345
1355
  properties: {
1346
1356
  autoIndex: { type: BOOLEAN },
1357
+ bodyLimit: { type: NUMBER, minimum: INT_1 },
1347
1358
  cacheSize: { type: NUMBER, minimum: INT_1 },
1348
1359
  cacheTTL: { type: NUMBER, minimum: INT_1 },
1349
1360
  charset: { type: STRING },
1350
1361
  corsExpose: { type: STRING },
1351
1362
  defaultHeaders: { type: OBJECT },
1352
1363
  digit: { type: NUMBER, minimum: INT_1, maximum: INT_10 },
1364
+ disableTrace: { type: BOOLEAN },
1353
1365
  etags: { type: BOOLEAN },
1366
+ exposeErrorMessages: { type: BOOLEAN },
1354
1367
  indexes: { type: ARRAY, items: { type: STRING } },
1355
1368
  logging: { type: OBJECT },
1356
1369
  origins: { type: ARRAY, items: { type: STRING } },
@@ -1820,12 +1833,15 @@ class Woodland extends EventEmitter {
1820
1833
  #charset;
1821
1834
  #corsExpose;
1822
1835
  #defaultHeaders;
1836
+ #disableTrace;
1823
1837
  #digit;
1824
1838
  #etags;
1839
+ #exposeErrorMessages;
1825
1840
  #indexes;
1826
1841
  #logging;
1827
1842
  #origins;
1828
1843
  #time;
1844
+ #bodyLimit;
1829
1845
  #cache;
1830
1846
  #methods;
1831
1847
  #logger;
@@ -1835,14 +1851,17 @@ class Woodland extends EventEmitter {
1835
1851
  /**
1836
1852
  * Creates a new Woodland instance
1837
1853
  * @param {Object} [config={}] - Configuration object
1854
+ * @param {number} [config.bodyLimit=10000000] - Max request body size in bytes
1838
1855
  * @param {boolean} [config.autoIndex=false] - Enable automatic directory indexing
1839
1856
  * @param {number} [config.cacheSize=1000] - Size of internal cache
1840
1857
  * @param {number} [config.cacheTTL=10000] - Cache TTL in milliseconds
1841
1858
  * @param {string} [config.charset='utf-8'] - Default charset
1842
1859
  * @param {string} [config.corsExpose=''] - CORS expose headers
1860
+ * @param {boolean} [config.disableTrace=true] - Disable TRACE method (XST vulnerability prevention)
1843
1861
  * @param {Object} [config.defaultHeaders={}] - Default headers to set
1844
1862
  * @param {number} [config.digit=3] - Digit precision for timing
1845
1863
  * @param {boolean} [config.etags=true] - Enable ETags
1864
+ * @param {boolean} [config.exposeErrorMessages=false] - Expose internal error messages to clients
1846
1865
  * @param {Array} [config.indexes=['index.htm','index.html']] - Index files
1847
1866
  * @param {Object} [config.logging={}] - Logging configuration
1848
1867
  * @param {Array} [config.origins=[]] - Allowed CORS origins
@@ -1855,13 +1874,16 @@ class Woodland extends EventEmitter {
1855
1874
  const validated = validateConfig(config);
1856
1875
 
1857
1876
  this.#autoIndex = validated.autoIndex;
1877
+ this.#bodyLimit = validated.bodyLimit;
1858
1878
  this.#charset = validated.charset;
1859
1879
  this.#corsExpose = validated.corsExpose;
1860
1880
  this.#defaultHeaders = this.#buildFinalHeaders(validated.defaultHeaders, validated.silent);
1881
+ this.#disableTrace = validated.disableTrace;
1861
1882
  this.#digit = validated.digit;
1862
1883
  this.#etags = validated.etags
1863
1884
  ? Object.freeze(etag({ cacheSize: validated.cacheSize, cacheTTL: validated.cacheTTL }))
1864
1885
  : null;
1886
+ this.#exposeErrorMessages = validated.exposeErrorMessages;
1865
1887
  this.#indexes = [...validated.indexes];
1866
1888
  this.#logging = Object.freeze(validateLogging(validated.logging));
1867
1889
  this.#origins = new Set(validated.origins);
@@ -1874,6 +1896,7 @@ class Woodland extends EventEmitter {
1874
1896
 
1875
1897
  this.#setupMiddleware();
1876
1898
  this.#setupErrorHandling();
1899
+ this.#setupBodyLimit();
1877
1900
  }
1878
1901
 
1879
1902
  /**
@@ -1946,15 +1969,38 @@ class Woodland extends EventEmitter {
1946
1969
  );
1947
1970
  }
1948
1971
 
1972
+ /**
1973
+ * Sets up body size limit middleware
1974
+ */
1975
+ #setupBodyLimit() {
1976
+ const maxLimit = this.#bodyLimit;
1977
+ const handler = (req, res, next) => {
1978
+ let size = INT_0;
1979
+ /* node:coverage ignore next 1 */
1980
+ if (typeof req.on !== FUNCTION) {
1981
+ return next();
1982
+ }
1983
+ req.on(EVT_DATA, (chunk) => {
1984
+ size += chunk.length;
1985
+ /* node:coverage ignore next 3 */
1986
+ if (size > maxLimit) {
1987
+ req.destroy();
1988
+ res.error(INT_413);
1989
+ }
1990
+ });
1991
+ next();
1992
+ };
1993
+ this.always(handler);
1994
+ }
1995
+
1949
1996
  /**
1950
1997
  * Determines allowed methods for a URI
1951
1998
  * @param {string} uri - URI to check
1952
1999
  * @param {boolean} [override=false] - Override cache
1953
- * @param {boolean} [isCorsRequest=false] - Whether this is a CORS request
1954
2000
  * @returns {string} Comma-separated list of allowed methods
1955
2001
  */
1956
- #allows(uri, override = false, isCorsRequest = false) {
1957
- const key = `perm${DELIMITER}${uri}${DELIMITER}${isCorsRequest ? INT_1 : INT_0}`;
2002
+ #allows(uri, override = false) {
2003
+ const key = `${DELIMITER}${uri}`;
1958
2004
  let result = !override ? this.#cache.get(key) : void 0;
1959
2005
 
1960
2006
  if (override || result === void 0) {
@@ -1966,7 +2012,7 @@ class Woodland extends EventEmitter {
1966
2012
  }
1967
2013
  }
1968
2014
 
1969
- const list = this.#buildAllowedList(methodSet, isCorsRequest);
2015
+ const list = this.#buildAllowedList(methodSet);
1970
2016
  result = list.sort().join(COMMA_SPACE);
1971
2017
  this.#cache.set(key, result);
1972
2018
  this.#logger.log(
@@ -1978,12 +2024,11 @@ class Woodland extends EventEmitter {
1978
2024
  }
1979
2025
 
1980
2026
  /**
1981
- * Builds the list of allowed methods including implicit HEAD and OPTIONS
2027
+ * Builds the list of allowed methods including implicit HEAD, OPTIONS
1982
2028
  * @param {Set} methodSet - Set of explicitly registered methods
1983
- * @param {boolean} isCorsRequest - Whether this is a CORS request
1984
2029
  * @returns {Array} Array of allowed methods
1985
2030
  */
1986
- #buildAllowedList(methodSet, isCorsRequest = false) {
2031
+ #buildAllowedList(methodSet) {
1987
2032
  const list = [...methodSet];
1988
2033
 
1989
2034
  if (list.length > INT_0) {
@@ -1991,8 +2036,7 @@ class Woodland extends EventEmitter {
1991
2036
  list.push(HEAD);
1992
2037
  }
1993
2038
 
1994
- /* node:coverage ignore next 3 */
1995
- if (!methodSet.has(OPTIONS) && isCorsRequest) {
2039
+ if (!methodSet.has(OPTIONS)) {
1996
2040
  list.push(OPTIONS);
1997
2041
  }
1998
2042
  }
@@ -2015,13 +2059,23 @@ class Woodland extends EventEmitter {
2015
2059
  return this.use(...args, WILDCARD);
2016
2060
  }
2017
2061
 
2062
+ /**
2063
+ * Registers middleware for a given HTTP method
2064
+ * @param {string} method - HTTP method name
2065
+ * @param {...*} args - Middleware function(s)
2066
+ * @returns {Woodland} Returns self for chaining
2067
+ */
2068
+ #registerMethod(method, ...args) {
2069
+ return this.use(...args, method);
2070
+ }
2071
+
2018
2072
  /**
2019
2073
  * Registers CONNECT middleware
2020
2074
  * @param {...*} args - Middleware function(s)
2021
2075
  * @returns {Woodland} Returns self for chaining
2022
2076
  */
2023
2077
  connect(...args) {
2024
- return this.use(...args, CONNECT);
2078
+ return this.#registerMethod(CONNECT, ...args);
2025
2079
  }
2026
2080
 
2027
2081
  /**
@@ -2043,8 +2097,20 @@ class Woodland extends EventEmitter {
2043
2097
  req.params = {};
2044
2098
  req.valid = true;
2045
2099
 
2046
- const allowString = this.#allows(parsed.pathname, false, req.cors);
2047
- const headersBatch = this.#buildDefaultHeaders(allowString);
2100
+ const allowString = this.#allows(parsed.pathname);
2101
+ const headersBatch = Object.create(null);
2102
+ headersBatch[ALLOW] = allowString;
2103
+ headersBatch[X_CONTENT_TYPE_OPTIONS] = NO_SNIFF;
2104
+
2105
+ const defaultHeaders = this.#defaultHeaders;
2106
+ const headerCount = defaultHeaders.length;
2107
+ for (let i = INT_0; i < headerCount; i++) {
2108
+ const [key, value] = defaultHeaders[i];
2109
+ if (typeof key === STRING && (typeof value === STRING || typeof value === NUMBER)) {
2110
+ headersBatch[key] = value;
2111
+ }
2112
+ }
2113
+
2048
2114
  req.allow = allowString;
2049
2115
 
2050
2116
  if (timing) {
@@ -2061,28 +2127,6 @@ class Woodland extends EventEmitter {
2061
2127
  );
2062
2128
  }
2063
2129
 
2064
- /**
2065
- * Builds default headers batch
2066
- * @param {string} allowString - Allow header value
2067
- * @returns {Object} Headers batch object
2068
- */
2069
- #buildDefaultHeaders(allowString) {
2070
- const headersBatch = Object.create(null);
2071
- headersBatch[ALLOW] = allowString;
2072
- headersBatch[X_CONTENT_TYPE_OPTIONS] = NO_SNIFF;
2073
-
2074
- const defaultHeaders = this.#defaultHeaders;
2075
- const headerCount = defaultHeaders.length;
2076
- for (let i = INT_0; i < headerCount; i++) {
2077
- const [key, value] = defaultHeaders[i];
2078
- if (typeof key === STRING && (typeof value === STRING || typeof value === NUMBER)) {
2079
- headersBatch[key] = value;
2080
- }
2081
- }
2082
-
2083
- return headersBatch;
2084
- }
2085
-
2086
2130
  /**
2087
2131
  * Decorates response object with methods and event handlers
2088
2132
  * @param {Object} res - HTTP response object
@@ -2091,7 +2135,7 @@ class Woodland extends EventEmitter {
2091
2135
  */
2092
2136
  #decorateResponse(res, req, headersBatch) {
2093
2137
  res.locals = {};
2094
- res.error = createErrorHandler(req, res, this);
2138
+ res.error = createErrorHandler(req, res, this, this.#exposeErrorMessages);
2095
2139
  res.header = res.setHeader;
2096
2140
  res.json = createJsonHandler(res);
2097
2141
  res.redirect = createRedirectHandler(res);
@@ -2110,8 +2154,7 @@ class Woodland extends EventEmitter {
2110
2154
  */
2111
2155
  #addCorsHeaders(req, headersBatch) {
2112
2156
  const origin = req.headers.origin;
2113
- const corsHeaders = req.headers[ACCESS_CONTROL_REQUEST_HEADERS] ?? this.#corsExpose;
2114
- const originAllowed = this.#origins.has(origin);
2157
+ const originAllowed = this.#isSafeOrigin(origin) && this.#origins.has(origin);
2115
2158
  const hasWildcard = this.#origins.has(WILDCARD);
2116
2159
 
2117
2160
  /* node:coverage ignore next 9 */
@@ -2120,21 +2163,44 @@ class Woodland extends EventEmitter {
2120
2163
  headersBatch[TIMING_ALLOW_ORIGIN] = origin;
2121
2164
  headersBatch[ACCESS_CONTROL_ALLOW_CREDENTIALS] = TRUE;
2122
2165
  headersBatch[ACCESS_CONTROL_ALLOW_METHODS] = req.allow;
2123
-
2124
- if (corsHeaders !== void 0) {
2125
- headersBatch[
2126
- req.method === OPTIONS ? ACCESS_CONTROL_ALLOW_HEADERS : ACCESS_CONTROL_EXPOSE_HEADERS
2127
- ] = corsHeaders;
2128
- }
2129
2166
  } else if (hasWildcard) {
2130
2167
  headersBatch[ACCESS_CONTROL_ALLOW_ORIGIN] = WILDCARD;
2131
2168
  headersBatch[ACCESS_CONTROL_ALLOW_METHODS] = req.allow;
2169
+ }
2132
2170
 
2133
- if (corsHeaders !== void 0) {
2134
- headersBatch[
2135
- req.method === OPTIONS ? ACCESS_CONTROL_ALLOW_HEADERS : ACCESS_CONTROL_EXPOSE_HEADERS
2136
- ] = corsHeaders;
2137
- }
2171
+ const corsHeaders = req.headers[ACCESS_CONTROL_REQUEST_HEADERS] ?? this.#corsExpose;
2172
+ this.#setCorsAllowAndExposeHeaders(headersBatch, req, corsHeaders);
2173
+ }
2174
+
2175
+ /**
2176
+ * Validates origin header for safety
2177
+ * @param {string} origin - Origin header value
2178
+ * @returns {boolean} True if origin is safe
2179
+ */
2180
+ #isSafeOrigin(origin) {
2181
+ if (!origin || typeof origin !== STRING) {
2182
+ return false;
2183
+ }
2184
+ if (origin.length > INT_255) {
2185
+ return false;
2186
+ }
2187
+ if (CONTROL_CHAR_PATTERN.test(origin)) {
2188
+ return false;
2189
+ }
2190
+ return /^https?:\/\//.test(origin);
2191
+ }
2192
+
2193
+ /**
2194
+ * Sets Access-Control-Allow-Headers or Access-Control-Expose-Headers based on method
2195
+ * @param {Object} headersBatch - Headers batch object
2196
+ * @param {Object} req - HTTP request object
2197
+ * @param {*} corsHeaders - CORS headers value
2198
+ */
2199
+ #setCorsAllowAndExposeHeaders(headersBatch, req, corsHeaders) {
2200
+ if (corsHeaders !== void 0) {
2201
+ headersBatch[
2202
+ req.method === OPTIONS ? ACCESS_CONTROL_ALLOW_HEADERS : ACCESS_CONTROL_EXPOSE_HEADERS
2203
+ ] = corsHeaders;
2138
2204
  }
2139
2205
  }
2140
2206
 
@@ -2144,7 +2210,7 @@ class Woodland extends EventEmitter {
2144
2210
  * @returns {Woodland} Returns self for chaining
2145
2211
  */
2146
2212
  delete(...args) {
2147
- return this.use(...args, DELETE);
2213
+ return this.#registerMethod(DELETE, ...args);
2148
2214
  }
2149
2215
 
2150
2216
  /**
@@ -2154,40 +2220,15 @@ class Woodland extends EventEmitter {
2154
2220
  * @returns {string} ETag string or empty string
2155
2221
  */
2156
2222
  etag(method, ...args) {
2157
- if (!this.#isHashableMethod(method) || !this.#etagsEnabled()) {
2223
+ if ((method !== GET && method !== HEAD && method !== OPTIONS) || !this.#etags) {
2158
2224
  return EMPTY;
2159
2225
  }
2160
2226
 
2161
- const hashed = this.#hashArgs(args);
2162
- return this.#etags.create(hashed);
2163
- }
2164
-
2165
- /**
2166
- * Checks if a method can be hashed for ETag generation
2167
- * @param {string} method - HTTP method
2168
- * @returns {boolean} True if method is GET, HEAD, or OPTIONS
2169
- */
2170
- #isHashableMethod(method) {
2171
- return method === GET || method === HEAD || method === OPTIONS;
2172
- }
2173
-
2174
- /**
2175
- * Checks if ETags are enabled
2176
- * @returns {boolean} True if ETags are enabled
2177
- */
2178
- #etagsEnabled() {
2179
- return this.#etags !== null;
2180
- }
2181
-
2182
- /**
2183
- * Hashes arguments for ETag generation
2184
- * @param {Array} args - Arguments to hash
2185
- * @returns {string} Hashed string
2186
- */
2187
- #hashArgs(args) {
2188
- return args
2227
+ const hashed = args
2189
2228
  .map((i) => (typeof i !== STRING ? JSON.stringify(i).replace(/^"|"$/g, EMPTY) : i))
2190
2229
  .join(HYPHEN);
2230
+
2231
+ return this.#etags.create(hashed);
2191
2232
  }
2192
2233
 
2193
2234
  /**
@@ -2208,7 +2249,7 @@ class Woodland extends EventEmitter {
2208
2249
  * @returns {Woodland} Returns self for chaining
2209
2250
  */
2210
2251
  get(...args) {
2211
- return this.use(...args, GET);
2252
+ return this.#registerMethod(GET, ...args);
2212
2253
  }
2213
2254
 
2214
2255
  /**
@@ -2268,8 +2309,8 @@ class Woodland extends EventEmitter {
2268
2309
  /* node:coverage ignore next 5 */
2269
2310
  if (this.#time && res.getHeader(X_RESPONSE_TIME) === void 0) {
2270
2311
  const diff = req.precise.stop().diff();
2271
- const msValue = Number(diff / 1e6).toFixed(this.#digit);
2272
- res.header(X_RESPONSE_TIME, `${msValue} ms`);
2312
+ const msValue = Number(diff / INT_1e6).toFixed(this.#digit);
2313
+ res.header(X_RESPONSE_TIME, `${msValue}${RESPONSE_TIME_UNIT}`);
2273
2314
  }
2274
2315
 
2275
2316
  return this.#onSend(req, res, body, status, headers);
@@ -2301,7 +2342,7 @@ class Woodland extends EventEmitter {
2301
2342
  * @returns {Woodland} Returns self for chaining
2302
2343
  */
2303
2344
  options(...args) {
2304
- return this.use(...args, OPTIONS);
2345
+ return this.#registerMethod(OPTIONS, ...args);
2305
2346
  }
2306
2347
 
2307
2348
  /**
@@ -2310,7 +2351,7 @@ class Woodland extends EventEmitter {
2310
2351
  * @returns {Woodland} Returns self for chaining
2311
2352
  */
2312
2353
  patch(...args) {
2313
- return this.use(...args, PATCH);
2354
+ return this.#registerMethod(PATCH, ...args);
2314
2355
  }
2315
2356
 
2316
2357
  /**
@@ -2319,7 +2360,7 @@ class Woodland extends EventEmitter {
2319
2360
  * @returns {Woodland} Returns self for chaining
2320
2361
  */
2321
2362
  post(...args) {
2322
- return this.use(...args, POST);
2363
+ return this.#registerMethod(POST, ...args);
2323
2364
  }
2324
2365
 
2325
2366
  /**
@@ -2328,7 +2369,7 @@ class Woodland extends EventEmitter {
2328
2369
  * @returns {Woodland} Returns self for chaining
2329
2370
  */
2330
2371
  put(...args) {
2331
- return this.use(...args, PUT);
2372
+ return this.#registerMethod(PUT, ...args);
2332
2373
  }
2333
2374
 
2334
2375
  /**
@@ -2442,8 +2483,15 @@ class Woodland extends EventEmitter {
2442
2483
  * Registers TRACE middleware
2443
2484
  * @param {...*} args - Middleware function(s)
2444
2485
  * @returns {Woodland} Returns self for chaining
2486
+ * @security TRACE method is vulnerable to XST attacks. Disabled by default.
2445
2487
  */
2446
2488
  trace(...args) {
2489
+ if (this.#disableTrace) {
2490
+ this.#logger.log(
2491
+ `type=trace, message="TRACE method is disabled by default (XST vulnerability prevention)"`,
2492
+ );
2493
+ return this;
2494
+ }
2447
2495
  return this.use(...args, TRACE);
2448
2496
  }
2449
2497
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "woodland",
3
- "version": "22.0.6",
3
+ "version": "22.1.0",
4
4
  "description": "Secure HTTP framework for Node.js. Express-compatible with built-in security, no performance tradeoff.",
5
5
  "keywords": [
6
6
  "api",
@@ -73,7 +73,7 @@
73
73
  "devDependencies": {
74
74
  "auto-changelog": "^2.5.0",
75
75
  "husky": "^9.1.7",
76
- "oxfmt": "^0.45.0",
76
+ "oxfmt": "^0.46.0",
77
77
  "oxlint": "^1.57.0",
78
78
  "rimraf": "^6.1.3",
79
79
  "rollup": "^4.60.0"
@@ -2,13 +2,16 @@ import { EventEmitter } from "node:events";
2
2
 
3
3
  export interface WoodlandConfig {
4
4
  autoIndex?: boolean;
5
+ bodyLimit?: number;
5
6
  cacheSize?: number;
6
7
  cacheTTL?: number;
7
8
  charset?: string;
8
9
  corsExpose?: string;
9
10
  defaultHeaders?: Record<string, string>;
10
11
  digit?: number;
12
+ disableTrace?: boolean;
11
13
  etags?: boolean;
14
+ exposeErrorMessages?: boolean;
12
15
  indexes?: string[];
13
16
  logging?: object;
14
17
  origins?: string[];