@mandujs/core 0.9.26 → 0.9.28

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mandujs/core",
3
- "version": "0.9.26",
3
+ "version": "0.9.28",
4
4
  "description": "Mandu Framework Core - Spec, Generator, Guard, Runtime",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -1118,12 +1118,23 @@ export async function buildClientBundles(
1118
1118
  // 1. Hydration이 필요한 라우트 필터링
1119
1119
  const hydratedRoutes = getHydratedRoutes(manifest);
1120
1120
 
1121
+ // 2. 출력 디렉토리 생성 (항상 필요 - 매니페스트 저장용)
1122
+ const outDir = options.outDir || path.join(rootDir, ".mandu/client");
1123
+ await fs.mkdir(outDir, { recursive: true });
1124
+
1125
+ // Hydration 라우트가 없어도 빈 매니페스트를 저장해야 함
1126
+ // (이전 빌드의 stale 매니페스트 참조 방지)
1121
1127
  if (hydratedRoutes.length === 0) {
1128
+ const emptyManifest = createEmptyManifest(env);
1129
+ await fs.writeFile(
1130
+ path.join(rootDir, ".mandu/manifest.json"),
1131
+ JSON.stringify(emptyManifest, null, 2)
1132
+ );
1122
1133
  return {
1123
1134
  success: true,
1124
1135
  outputs: [],
1125
1136
  errors: [],
1126
- manifest: createEmptyManifest(env),
1137
+ manifest: emptyManifest,
1127
1138
  stats: {
1128
1139
  totalSize: 0,
1129
1140
  totalGzipSize: 0,
@@ -1134,10 +1145,6 @@ export async function buildClientBundles(
1134
1145
  };
1135
1146
  }
1136
1147
 
1137
- // 2. 출력 디렉토리 생성
1138
- const outDir = options.outDir || path.join(rootDir, ".mandu/client");
1139
- await fs.mkdir(outDir, { recursive: true });
1140
-
1141
1148
  // 3. Runtime 번들 빌드
1142
1149
  const runtimeResult = await buildRuntime(outDir, options);
1143
1150
  if (!runtimeResult.success) {
@@ -204,24 +204,49 @@ function defaultCreateApp(context: AppContext): React.ReactElement {
204
204
 
205
205
  // ========== Static File Serving ==========
206
206
 
207
+ /**
208
+ * 경로가 허용된 디렉토리 내에 있는지 검증
209
+ * Path traversal 공격 방지
210
+ */
211
+ function isPathSafe(filePath: string, allowedDir: string): boolean {
212
+ const resolvedPath = path.resolve(filePath);
213
+ const resolvedAllowedDir = path.resolve(allowedDir);
214
+ // 경로가 허용된 디렉토리로 시작하는지 확인 (디렉토리 구분자 포함)
215
+ return resolvedPath.startsWith(resolvedAllowedDir + path.sep) ||
216
+ resolvedPath === resolvedAllowedDir;
217
+ }
218
+
207
219
  /**
208
220
  * 정적 파일 서빙
209
221
  * - /.mandu/client/* : 클라이언트 번들 (Island hydration)
210
222
  * - /public/* : 정적 에셋 (이미지, CSS 등)
211
223
  * - /favicon.ico : 파비콘
224
+ *
225
+ * 보안: Path traversal 공격 방지를 위해 모든 경로를 검증합니다.
212
226
  */
213
227
  async function serveStaticFile(pathname: string): Promise<Response | null> {
214
228
  let filePath: string | null = null;
215
229
  let isBundleFile = false;
230
+ let allowedBaseDir: string;
231
+
232
+ // Path traversal 시도 조기 차단 (정규화 전 raw 체크)
233
+ if (pathname.includes("..")) {
234
+ return null;
235
+ }
216
236
 
217
237
  // 1. 클라이언트 번들 파일 (/.mandu/client/*)
218
238
  if (pathname.startsWith("/.mandu/client/")) {
219
- filePath = path.join(serverSettings.rootDir, pathname);
239
+ // pathname에서 prefix 제거 후 안전하게 조합
240
+ const relativePath = pathname.slice("/.mandu/client/".length);
241
+ allowedBaseDir = path.join(serverSettings.rootDir, ".mandu", "client");
242
+ filePath = path.join(allowedBaseDir, relativePath);
220
243
  isBundleFile = true;
221
244
  }
222
- // 2. Public 폴더 파일 (/public/* 또는 직접 접근)
245
+ // 2. Public 폴더 파일 (/public/*)
223
246
  else if (pathname.startsWith("/public/")) {
224
- filePath = path.join(serverSettings.rootDir, pathname);
247
+ const relativePath = pathname.slice("/public/".length);
248
+ allowedBaseDir = path.join(serverSettings.rootDir, "public");
249
+ filePath = path.join(allowedBaseDir, relativePath);
225
250
  }
226
251
  // 3. Public 폴더의 루트 파일 (favicon.ico, robots.txt 등)
227
252
  else if (
@@ -230,11 +255,18 @@ async function serveStaticFile(pathname: string): Promise<Response | null> {
230
255
  pathname === "/sitemap.xml" ||
231
256
  pathname === "/manifest.json"
232
257
  ) {
233
- filePath = path.join(serverSettings.rootDir, serverSettings.publicDir, pathname);
258
+ // 고정된 파일명만 허용 (이미 위에서 정확히 매칭됨)
259
+ const filename = path.basename(pathname);
260
+ allowedBaseDir = path.join(serverSettings.rootDir, serverSettings.publicDir);
261
+ filePath = path.join(allowedBaseDir, filename);
262
+ } else {
263
+ return null; // 정적 파일이 아님
234
264
  }
235
265
 
236
- if (!filePath) {
237
- return null; // 정적 파일이 아님
266
+ // 최종 경로 검증: 허용된 디렉토리 내에 있는지 확인
267
+ if (!isPathSafe(filePath, allowedBaseDir!)) {
268
+ console.warn(`[Mandu Security] Path traversal attempt blocked: ${pathname}`);
269
+ return null;
238
270
  }
239
271
 
240
272
  try {