hono 4.13.4 → 4.13.5

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.
@@ -206,7 +206,7 @@ const saveContentToFile = async (data, fsModule, outDir, extensionMap) => {
206
206
  const { routePath, content, mimeType } = awaitedData;
207
207
  const filePath = generateFilePath(routePath, outDir, mimeType, extensionMap);
208
208
  const dirPath = (0, import_utils2.dirname)(filePath);
209
- if (!createdDirs.has(dirPath)) {
209
+ if (dirPath !== "" && !createdDirs.has(dirPath)) {
210
210
  await fsModule.mkdir(dirPath, { recursive: true });
211
211
  createdDirs.add(dirPath);
212
212
  }
@@ -33,8 +33,14 @@ const dirname = (path) => {
33
33
  const normalizePath = (path) => {
34
34
  return path.replace(/(\\)/g, "/").replace(/\/$/g, "");
35
35
  };
36
- const handleParent = (resultPaths, beforeParentFlag) => {
37
- if (resultPaths.length === 0 || beforeParentFlag) {
36
+ const getUncRoot = (path) => {
37
+ const uncRoot = path.replace(/\\/g, "/").match(/^\/\/([^/]+)\/([^/]+)/);
38
+ if (uncRoot) {
39
+ return `${uncRoot[1].toLowerCase()}/${uncRoot[2].toLowerCase()}`;
40
+ }
41
+ };
42
+ const handleParent = (resultPaths) => {
43
+ if (resultPaths.length === 0 || resultPaths[resultPaths.length - 1] === "..") {
38
44
  resultPaths.push("..");
39
45
  } else {
40
46
  resultPaths.pop();
@@ -47,22 +53,20 @@ const handleNonDot = (path, resultPaths) => {
47
53
  }
48
54
  };
49
55
  const handleSegments = (paths, resultPaths) => {
50
- let beforeParentFlag = false;
51
56
  for (const path of paths) {
52
57
  if (path === "..") {
53
- handleParent(resultPaths, beforeParentFlag);
54
- beforeParentFlag = true;
58
+ handleParent(resultPaths);
55
59
  } else {
56
60
  handleNonDot(path, resultPaths);
57
- beforeParentFlag = false;
58
61
  }
59
62
  }
60
63
  };
61
64
  const joinPaths = (...paths) => {
65
+ const hasUncPrefix = getUncRoot(paths[0]) !== void 0;
62
66
  paths = paths.map(normalizePath);
63
67
  const resultPaths = [];
64
68
  handleSegments(paths.join("/").split("/"), resultPaths);
65
- return (paths[0][0] === "/" ? "/" : "") + resultPaths.join("/");
69
+ return (hasUncPrefix ? "//" : paths[0][0] === "/" ? "/" : "") + resultPaths.join("/");
66
70
  };
67
71
  const filterStaticGenerateRoutes = (hono) => {
68
72
  return hono.routes.reduce((acc, { method, handler, path }) => {
@@ -76,10 +80,26 @@ const filterStaticGenerateRoutes = (hono) => {
76
80
  const isDynamicRoute = (path) => {
77
81
  return path.split("/").some((segment) => segment.startsWith(":") || segment.includes("*"));
78
82
  };
83
+ const toSegments = (path) => path === "" ? [] : path.split("/");
84
+ const getPathRoot = (path) => {
85
+ const normalizedPath = path.replace(/\\/g, "/");
86
+ const uncRoot = getUncRoot(normalizedPath);
87
+ if (uncRoot) {
88
+ return `unc:${uncRoot}`;
89
+ }
90
+ const driveRoot = normalizedPath.match(/^([A-Za-z]):/);
91
+ if (driveRoot) {
92
+ const kind = normalizedPath[2] === "/" ? "drive-absolute" : "drive-relative";
93
+ return `${kind}:${driveRoot[1].toLowerCase()}`;
94
+ }
95
+ return normalizedPath.startsWith("/") ? "absolute" : "relative";
96
+ };
79
97
  const ensureWithinOutDir = (outDir, filePath) => {
80
- const normalizedOutDir = joinPaths("/", outDir);
81
- const normalizedFilePath = joinPaths("/", filePath);
82
- if (normalizedFilePath !== normalizedOutDir && !normalizedFilePath.startsWith(`${normalizedOutDir}/`)) {
98
+ const outDirSegments = toSegments(joinPaths(outDir));
99
+ const filePathSegments = toSegments(joinPaths(filePath));
100
+ const hasMismatchedPathRoot = getPathRoot(outDir) !== getPathRoot(filePath);
101
+ const climbsAboveOutDir = filePathSegments[outDirSegments.length] === "..";
102
+ if (hasMismatchedPathRoot || filePathSegments.length <= outDirSegments.length || !outDirSegments.every((segment, i) => segment === filePathSegments[i]) || climbsAboveOutDir) {
83
103
  throw new Error(`Path traversal detected: "${filePath}" is outside the output directory`);
84
104
  }
85
105
  };
@@ -44,7 +44,7 @@ const parseVaryDirectives = (vary) => {
44
44
  };
45
45
  const createCacheKey = (key, requestUrl, request, varyHeaders) => {
46
46
  const url = new URL(cacheKeyPath, requestUrl);
47
- url.searchParams.append(cacheKeyParameter, key.split("#", 1)[0]);
47
+ url.searchParams.append(cacheKeyParameter, key);
48
48
  url.searchParams.append(cacheMethodKeyParameter, request.method);
49
49
  if (request.method === "QUERY") {
50
50
  url.searchParams.append(queryDigestKeyParameter, request.digest);
@@ -21,6 +21,8 @@ __export(body_exports, {
21
21
  });
22
22
  module.exports = __toCommonJS(body_exports);
23
23
  var import_buffer = require("./buffer");
24
+ const MAX_NESTING_DEPTH = 32;
25
+ const MAX_NESTED_OBJECTS = 1e4;
24
26
  const isRawRequest = (request) => "headers" in request;
25
27
  const parseBody = async (request, options = /* @__PURE__ */ Object.create(null)) => {
26
28
  const { all = false, dot = false } = options;
@@ -53,6 +55,7 @@ async function parseFormData(request, options) {
53
55
  }
54
56
  function convertFormDataToBodyData(formData, options) {
55
57
  const form = /* @__PURE__ */ Object.create(null);
58
+ const nestingState = { count: 0 };
56
59
  formData.forEach((value, key) => {
57
60
  const shouldParseAllValues = options.all || key.endsWith("[]");
58
61
  if (!shouldParseAllValues) {
@@ -65,7 +68,7 @@ function convertFormDataToBodyData(formData, options) {
65
68
  Object.entries(form).forEach(([key, value]) => {
66
69
  const shouldParseDotValues = key.includes(".");
67
70
  if (shouldParseDotValues) {
68
- handleParsingNestedValues(form, key, value);
71
+ handleParsingNestedValues(form, key, value, nestingState);
69
72
  delete form[key];
70
73
  }
71
74
  });
@@ -88,23 +91,32 @@ const handleParsingAllValues = (form, key, value) => {
88
91
  }
89
92
  }
90
93
  };
91
- const handleParsingNestedValues = (form, key, value) => {
94
+ const handleParsingNestedValues = (form, key, value, state) => {
92
95
  if (/(?:^|\.)__proto__\./.test(key)) {
93
96
  return;
94
97
  }
95
98
  let nestedForm = form;
96
- const keys = key.split(".");
99
+ const keys = key.split(".", MAX_NESTING_DEPTH + 2);
100
+ if (keys.length > MAX_NESTING_DEPTH + 1) {
101
+ throwNestingLimitExceeded();
102
+ }
97
103
  keys.forEach((key2, index) => {
98
104
  if (index === keys.length - 1) {
99
105
  nestedForm[key2] = value;
100
106
  } else {
101
107
  if (!nestedForm[key2] || typeof nestedForm[key2] !== "object" || Array.isArray(nestedForm[key2]) || nestedForm[key2] instanceof File) {
108
+ if (state.count++ >= MAX_NESTED_OBJECTS) {
109
+ throwNestingLimitExceeded();
110
+ }
102
111
  nestedForm[key2] = /* @__PURE__ */ Object.create(null);
103
112
  }
104
113
  nestedForm = nestedForm[key2];
105
114
  }
106
115
  });
107
116
  };
117
+ const throwNestingLimitExceeded = () => {
118
+ throw new Error("Nesting limit exceeded");
119
+ };
108
120
  // Annotate the CommonJS export names for ESM import in node:
109
121
  0 && (module.exports = {
110
122
  parseBody
@@ -119,7 +119,11 @@ const getPath = (request) => {
119
119
  };
120
120
  const getQueryStrings = (url) => {
121
121
  const queryIndex = url.indexOf("?", 8);
122
- return queryIndex === -1 ? "" : "?" + url.slice(queryIndex + 1);
122
+ if (queryIndex === -1) {
123
+ return "";
124
+ }
125
+ const hashIndex = url.indexOf("#", 8);
126
+ return hashIndex === -1 ? url.slice(queryIndex) : queryIndex < hashIndex ? url.slice(queryIndex, hashIndex) : "";
123
127
  };
124
128
  const getPathNoStrict = (request) => {
125
129
  const result = getPath(request);
@@ -166,6 +170,10 @@ const _decodeURI = (value) => {
166
170
  return tryDecodeURIComponent(value);
167
171
  };
168
172
  const _getQueryParam = (url, key, multiple) => {
173
+ const hashIndex = url.indexOf("#", 8);
174
+ if (hashIndex !== -1) {
175
+ url = url.slice(0, hashIndex);
176
+ }
169
177
  let encoded;
170
178
  if (!multiple && key && key.indexOf("%") === -1 && key.indexOf("+") === -1) {
171
179
  let keyIndex2 = url.indexOf("?", 8);
@@ -184,7 +184,7 @@ var saveContentToFile = async (data, fsModule, outDir, extensionMap) => {
184
184
  const { routePath, content, mimeType } = awaitedData;
185
185
  const filePath = generateFilePath(routePath, outDir, mimeType, extensionMap);
186
186
  const dirPath = dirname(filePath);
187
- if (!createdDirs.has(dirPath)) {
187
+ if (dirPath !== "" && !createdDirs.has(dirPath)) {
188
188
  await fsModule.mkdir(dirPath, { recursive: true });
189
189
  createdDirs.add(dirPath);
190
190
  }
@@ -8,8 +8,14 @@ var dirname = (path) => {
8
8
  var normalizePath = (path) => {
9
9
  return path.replace(/(\\)/g, "/").replace(/\/$/g, "");
10
10
  };
11
- var handleParent = (resultPaths, beforeParentFlag) => {
12
- if (resultPaths.length === 0 || beforeParentFlag) {
11
+ var getUncRoot = (path) => {
12
+ const uncRoot = path.replace(/\\/g, "/").match(/^\/\/([^/]+)\/([^/]+)/);
13
+ if (uncRoot) {
14
+ return `${uncRoot[1].toLowerCase()}/${uncRoot[2].toLowerCase()}`;
15
+ }
16
+ };
17
+ var handleParent = (resultPaths) => {
18
+ if (resultPaths.length === 0 || resultPaths[resultPaths.length - 1] === "..") {
13
19
  resultPaths.push("..");
14
20
  } else {
15
21
  resultPaths.pop();
@@ -22,22 +28,20 @@ var handleNonDot = (path, resultPaths) => {
22
28
  }
23
29
  };
24
30
  var handleSegments = (paths, resultPaths) => {
25
- let beforeParentFlag = false;
26
31
  for (const path of paths) {
27
32
  if (path === "..") {
28
- handleParent(resultPaths, beforeParentFlag);
29
- beforeParentFlag = true;
33
+ handleParent(resultPaths);
30
34
  } else {
31
35
  handleNonDot(path, resultPaths);
32
- beforeParentFlag = false;
33
36
  }
34
37
  }
35
38
  };
36
39
  var joinPaths = (...paths) => {
40
+ const hasUncPrefix = getUncRoot(paths[0]) !== void 0;
37
41
  paths = paths.map(normalizePath);
38
42
  const resultPaths = [];
39
43
  handleSegments(paths.join("/").split("/"), resultPaths);
40
- return (paths[0][0] === "/" ? "/" : "") + resultPaths.join("/");
44
+ return (hasUncPrefix ? "//" : paths[0][0] === "/" ? "/" : "") + resultPaths.join("/");
41
45
  };
42
46
  var filterStaticGenerateRoutes = (hono) => {
43
47
  return hono.routes.reduce((acc, { method, handler, path }) => {
@@ -51,10 +55,26 @@ var filterStaticGenerateRoutes = (hono) => {
51
55
  var isDynamicRoute = (path) => {
52
56
  return path.split("/").some((segment) => segment.startsWith(":") || segment.includes("*"));
53
57
  };
58
+ var toSegments = (path) => path === "" ? [] : path.split("/");
59
+ var getPathRoot = (path) => {
60
+ const normalizedPath = path.replace(/\\/g, "/");
61
+ const uncRoot = getUncRoot(normalizedPath);
62
+ if (uncRoot) {
63
+ return `unc:${uncRoot}`;
64
+ }
65
+ const driveRoot = normalizedPath.match(/^([A-Za-z]):/);
66
+ if (driveRoot) {
67
+ const kind = normalizedPath[2] === "/" ? "drive-absolute" : "drive-relative";
68
+ return `${kind}:${driveRoot[1].toLowerCase()}`;
69
+ }
70
+ return normalizedPath.startsWith("/") ? "absolute" : "relative";
71
+ };
54
72
  var ensureWithinOutDir = (outDir, filePath) => {
55
- const normalizedOutDir = joinPaths("/", outDir);
56
- const normalizedFilePath = joinPaths("/", filePath);
57
- if (normalizedFilePath !== normalizedOutDir && !normalizedFilePath.startsWith(`${normalizedOutDir}/`)) {
73
+ const outDirSegments = toSegments(joinPaths(outDir));
74
+ const filePathSegments = toSegments(joinPaths(filePath));
75
+ const hasMismatchedPathRoot = getPathRoot(outDir) !== getPathRoot(filePath);
76
+ const climbsAboveOutDir = filePathSegments[outDirSegments.length] === "..";
77
+ if (hasMismatchedPathRoot || filePathSegments.length <= outDirSegments.length || !outDirSegments.every((segment, i) => segment === filePathSegments[i]) || climbsAboveOutDir) {
58
78
  throw new Error(`Path traversal detected: "${filePath}" is outside the output directory`);
59
79
  }
60
80
  };
@@ -23,7 +23,7 @@ var parseVaryDirectives = (vary) => {
23
23
  };
24
24
  var createCacheKey = (key, requestUrl, request, varyHeaders) => {
25
25
  const url = new URL(cacheKeyPath, requestUrl);
26
- url.searchParams.append(cacheKeyParameter, key.split("#", 1)[0]);
26
+ url.searchParams.append(cacheKeyParameter, key);
27
27
  url.searchParams.append(cacheMethodKeyParameter, request.method);
28
28
  if (request.method === "QUERY") {
29
29
  url.searchParams.append(queryDigestKeyParameter, request.digest);
@@ -19,6 +19,10 @@ export declare const tryDecode: (str: string, decoder: Decoder) => string;
19
19
  */
20
20
  export declare const tryDecodeURI: (str: string) => string;
21
21
  export declare const getPath: (request: Request) => string;
22
+ /**
23
+ * @deprecated
24
+ * Use the `URL` API instead.
25
+ */
22
26
  export declare const getQueryStrings: (url: string) => string;
23
27
  export declare const getPathNoStrict: (request: Request) => string;
24
28
  /**
@@ -1,5 +1,7 @@
1
1
  // src/utils/body.ts
2
2
  import { bufferToFormData } from "./buffer.js";
3
+ var MAX_NESTING_DEPTH = 32;
4
+ var MAX_NESTED_OBJECTS = 1e4;
3
5
  var isRawRequest = (request) => "headers" in request;
4
6
  var parseBody = async (request, options = /* @__PURE__ */ Object.create(null)) => {
5
7
  const { all = false, dot = false } = options;
@@ -32,6 +34,7 @@ async function parseFormData(request, options) {
32
34
  }
33
35
  function convertFormDataToBodyData(formData, options) {
34
36
  const form = /* @__PURE__ */ Object.create(null);
37
+ const nestingState = { count: 0 };
35
38
  formData.forEach((value, key) => {
36
39
  const shouldParseAllValues = options.all || key.endsWith("[]");
37
40
  if (!shouldParseAllValues) {
@@ -44,7 +47,7 @@ function convertFormDataToBodyData(formData, options) {
44
47
  Object.entries(form).forEach(([key, value]) => {
45
48
  const shouldParseDotValues = key.includes(".");
46
49
  if (shouldParseDotValues) {
47
- handleParsingNestedValues(form, key, value);
50
+ handleParsingNestedValues(form, key, value, nestingState);
48
51
  delete form[key];
49
52
  }
50
53
  });
@@ -67,23 +70,32 @@ var handleParsingAllValues = (form, key, value) => {
67
70
  }
68
71
  }
69
72
  };
70
- var handleParsingNestedValues = (form, key, value) => {
73
+ var handleParsingNestedValues = (form, key, value, state) => {
71
74
  if (/(?:^|\.)__proto__\./.test(key)) {
72
75
  return;
73
76
  }
74
77
  let nestedForm = form;
75
- const keys = key.split(".");
78
+ const keys = key.split(".", MAX_NESTING_DEPTH + 2);
79
+ if (keys.length > MAX_NESTING_DEPTH + 1) {
80
+ throwNestingLimitExceeded();
81
+ }
76
82
  keys.forEach((key2, index) => {
77
83
  if (index === keys.length - 1) {
78
84
  nestedForm[key2] = value;
79
85
  } else {
80
86
  if (!nestedForm[key2] || typeof nestedForm[key2] !== "object" || Array.isArray(nestedForm[key2]) || nestedForm[key2] instanceof File) {
87
+ if (state.count++ >= MAX_NESTED_OBJECTS) {
88
+ throwNestingLimitExceeded();
89
+ }
81
90
  nestedForm[key2] = /* @__PURE__ */ Object.create(null);
82
91
  }
83
92
  nestedForm = nestedForm[key2];
84
93
  }
85
94
  });
86
95
  };
96
+ var throwNestingLimitExceeded = () => {
97
+ throw new Error("Nesting limit exceeded");
98
+ };
87
99
  export {
88
100
  parseBody
89
101
  };
package/dist/utils/url.js CHANGED
@@ -85,7 +85,11 @@ var getPath = (request) => {
85
85
  };
86
86
  var getQueryStrings = (url) => {
87
87
  const queryIndex = url.indexOf("?", 8);
88
- return queryIndex === -1 ? "" : "?" + url.slice(queryIndex + 1);
88
+ if (queryIndex === -1) {
89
+ return "";
90
+ }
91
+ const hashIndex = url.indexOf("#", 8);
92
+ return hashIndex === -1 ? url.slice(queryIndex) : queryIndex < hashIndex ? url.slice(queryIndex, hashIndex) : "";
89
93
  };
90
94
  var getPathNoStrict = (request) => {
91
95
  const result = getPath(request);
@@ -132,6 +136,10 @@ var _decodeURI = (value) => {
132
136
  return tryDecodeURIComponent(value);
133
137
  };
134
138
  var _getQueryParam = (url, key, multiple) => {
139
+ const hashIndex = url.indexOf("#", 8);
140
+ if (hashIndex !== -1) {
141
+ url = url.slice(0, hashIndex);
142
+ }
135
143
  let encoded;
136
144
  if (!multiple && key && key.indexOf("%") === -1 && key.indexOf("+") === -1) {
137
145
  let keyIndex2 = url.indexOf("?", 8);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hono",
3
- "version": "4.13.4",
3
+ "version": "4.13.5",
4
4
  "description": "Web framework built on Web Standards",
5
5
  "main": "dist/cjs/index.js",
6
6
  "type": "module",