@mandujs/core 0.9.26 → 0.9.27
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 +1 -1
- package/src/runtime/server.ts +38 -6
package/package.json
CHANGED
package/src/runtime/server.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
237
|
-
|
|
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 {
|