Просмотр исходного кода

fix: reject absolute paths in isPathSafe, remove duplicate chapter save in Step5

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
nonvioce 6 месяцев назад
Родитель
Сommit
0beee9ad1f

+ 28 - 0
src/app/api/novel/files/route.ts

@@ -190,6 +190,13 @@ export async function POST(request: NextRequest) {
           );
         }
 
+        if (path.isAbsolute(filePath)) {
+          return NextResponse.json(
+            { success: false, error: 'filePath 必须是相对路径' },
+            { status: 400 }
+          );
+        }
+
         const fullPath = path.join(projectPath, filePath);
         const tmpPath = fullPath + '.tmp';
 
@@ -213,6 +220,13 @@ export async function POST(request: NextRequest) {
           );
         }
 
+        if (path.isAbsolute(filePath)) {
+          return NextResponse.json(
+            { success: false, error: 'filePath 必须是相对路径' },
+            { status: 400 }
+          );
+        }
+
         const fullPath = path.join(projectPath, filePath);
         const stat = await fs.stat(fullPath);
         const content = await fs.readFile(fullPath, 'utf-8');
@@ -250,6 +264,13 @@ export async function POST(request: NextRequest) {
           );
         }
 
+        if (path.isAbsolute(filePath)) {
+          return NextResponse.json(
+            { success: false, error: 'filePath 必须是相对路径' },
+            { status: 400 }
+          );
+        }
+
         const fullPath = path.join(projectPath, filePath);
         await fs.unlink(fullPath);
 
@@ -266,6 +287,13 @@ export async function POST(request: NextRequest) {
           );
         }
 
+        if (path.isAbsolute(filePath)) {
+          return NextResponse.json(
+            { success: false, error: 'filePath 必须是相对路径' },
+            { status: 400 }
+          );
+        }
+
         const fullPath = path.join(projectPath, filePath);
 
         // 检查文件是否存在

+ 2 - 18
src/app/features/novel-writer/components/pipeline/Step5ReviewSubmit.tsx

@@ -317,26 +317,10 @@ export default function Step5ReviewSubmit({
     setSaveError(null);
 
     try {
-      // 1. 保存章节文件
-      const fileName = `chapter_${chapterIndex}_${chapterTitle.trim()}.md`;
-      const response = await fetch('/api/novel/files', {
-        method: 'POST',
-        headers: { 'Content-Type': 'application/json' },
-        body: JSON.stringify({
-          action: 'write',
-          projectPath,
-          filePath: `chapters/${fileName}`,
-          content: chapterContent,
-        }),
-      });
-
-      const data = await response.json();
-      if (!data.success) throw new Error(data.error || '保存章节失败');
-
-      // 2. 处理钩子更新
+      // 1. 处理钩子更新
       await updateHooksInJson();
 
-      // 3. After successful chapter save, mark chapter events as written
+      // 2. After successful chapter save, mark chapter events as written
       try {
         const graphRes = await fetch(`/api/novel/event-graph?projectPath=${encodeURIComponent(projectPath)}`);
         const graphData = await graphRes.json();

+ 4 - 0
src/app/lib/novel-server-utils.ts

@@ -19,6 +19,10 @@ export function isPathSafe(inputPath: string): boolean {
   if (inputPath.includes('../') || inputPath.includes('..\\')) return false;
   // Block URL-encoded traversal
   if (/%2e%2e/i.test(inputPath)) return false;
+  // Block absolute paths (path.join collapses them, bypassing prefix)
+  if (inputPath.startsWith('/') || inputPath.startsWith('\\')) return false;
+  // Block Windows drive letter absolute paths (e.g. C:\)
+  if (/^[a-zA-Z]:[/\\]/.test(inputPath)) return false;
   // Block null bytes
   if (inputPath.includes('\0')) return false;
   return true;