diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 2ceab100..75e81367 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -2,3 +2,7 @@ **Vulnerability:** Raw error objects from FFmpeg and browser APIs (`error.message` and `console.error`) were being rendered directly in the user interface and browser console. **Learning:** Returning unhandled exception messages to the client risks exposing internal stack traces, system paths, and unexpected framework vulnerabilities to potential attackers. **Prevention:** Catch error blocks should log a sanitized version of the error or omit sensitive details, while the state presented to the user should be a generic, friendly, and secure fallback message (e.g. "Processing failed securely"). +## 2024-05-30 - Fix Path Traversal Risk in Server API Uploads +**Vulnerability:** User-uploaded file names were concatenated directly into temporary storage file paths in `src/app/api/video/[tool]/route.ts` and `src/app/api/plugins/install/route.ts` (e.g., `const inputPath = ${tmpDir}/${crypto.randomUUID()}_${file.name};`). +**Learning:** While using a UUID prefix makes brute-forcing paths more difficult, it does not guarantee protection if the user input `file.name` contains directory traversal sequences (like `../` or newline characters). Trusting unvalidated inputs inside path strings is a systemic risk for path traversal vulnerabilities when passing these paths to file system commands (like `fs.writeFile`). +**Prevention:** Always sanitize the user-provided filename using `path.basename()` before combining it into server-side file paths to strip off any directory structures provided by the client. diff --git a/src/app/api/plugins/install/route.ts b/src/app/api/plugins/install/route.ts index 338847e7..e9575c4d 100644 --- a/src/app/api/plugins/install/route.ts +++ b/src/app/api/plugins/install/route.ts @@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from 'next/server'; import { pluginDb } from '@/lib/db'; import AdmZip from 'adm-zip'; import { existsSync, mkdirSync, rmSync, writeFileSync } from 'fs'; -import { join, resolve } from 'path'; +import { join, resolve, basename } from 'path'; import { validateManifest } from '@/lib/validators/plugin'; import { PLUGINS_DIR } from '@/modules/plugin-engine/system/health-monitor'; @@ -20,8 +20,10 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: 'File harus berformat ZIP' }, { status: 400 }); } + // Sanitize user-provided filename to prevent path traversal vulnerability + const safeFileName = basename(file.name); const buffer = Buffer.from(await file.arrayBuffer()); - tempZipPath = join(process.cwd(), '.next', 'tmp', `${Date.now()}-${file.name}`); + tempZipPath = join(process.cwd(), '.next', 'tmp', `${Date.now()}-${safeFileName}`); if (!existsSync(join(process.cwd(), '.next', 'tmp'))) { mkdirSync(join(process.cwd(), '.next', 'tmp'), { recursive: true }); diff --git a/src/app/api/video/[tool]/route.ts b/src/app/api/video/[tool]/route.ts index a7307430..31f34cd6 100644 --- a/src/app/api/video/[tool]/route.ts +++ b/src/app/api/video/[tool]/route.ts @@ -126,7 +126,9 @@ export async function POST( const tmpDir = `/tmp/omni/video`; await fs.mkdir(tmpDir, { recursive: true }); - const inputPath = `${tmpDir}/${crypto.randomUUID()}_${file.name}`; + // Sanitize user-provided filename to prevent path traversal vulnerability + const safeFileName = path.basename(file.name); + const inputPath = `${tmpDir}/${crypto.randomUUID()}_${safeFileName}`; // Write file to disk const arrayBuffer = await file.arrayBuffer(); @@ -138,7 +140,7 @@ export async function POST( success: true, jobId, tool, - fileName: file.name, + fileName: safeFileName, fileSize: file.size, status: "queued", message: `Job queued.`,