T05 · Unauthorized Access and Privilege Escalation
Error
- Location
- scripts/upload-images.js:202
- Finding
- Arbitrary Local File Read and Exfiltration Through Image Path Traversal<![CDATA[ ## Vulnerability Details **File Location**: `scripts/upload-images.js`, lines 202–209 and 241–274 **Vulnerability Type**: Path traversal leading to unauthorized file disclosure and remote upload **Risk Level**: High ### Vulnerable Code ```js for (const relPath of imageRelPaths) { const srcPath = path.join(PROJECT_ROOT, relPath); if (!fs.existsSync(srcPath)) { console.warn(`Image does not exist, skipping: ${relPath}`); continue; } const destPath = path.join(filesDir, relPath); fs.mkdirSync(path.dirname(destPath), { recursive: true }); fs.copyFileSync(srcPath, destPath); copiedCount++; } ``` The resulting archive is subsequently read and transmitted: ```js const zipBuf = fs.readFileSync(zipPath); const totalBytes = zipBuf.length; const totalChunks = Math.ceil(totalBytes / CHUNK_SIZE); for (let i = 0; i < totalChunks; i++) { const start = i * CHUNK_SIZE; const end = Math.min(start + CHUNK_SIZE, totalBytes); const chunkBuf = zipBuf.slice(start, end); const chunkB64 = chunkBuf.toString('base64'); const partNum = i + 1; const partResult = await mcpCall('UploadScanFilesPartMcp', { file_id: fileId, part_number: partNum, content_base64: chunkB64 }); const etag = typeof partResult === 'string' ? partResult : (partResult && partResult.etag); if (!etag) { throw new Error(`Part ${partNum} did not return an etag: ${JSON.stringify(partResult)}`); } partList.push({ part_number: partNum, etag: String(etag) }); } ``` ### Technical Analysis Image paths are accepted through `--images` or `--images-file` and used directly with `path.join(PROJECT_ROOT, relPath)`. The script does not reject absolute paths, `..` components, or symbolic links resolving outside the project. `path.join()` normalizes traversal components but does not enforce containment. A value such as `../private-file` therefore resolves outside the intended project root. The selected file is copied into the upload staging directory ...[truncated 1179 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Resolve every requested path before use: ```js const root = fs.realpathSync(PROJECT_ROOT); const candidate = fs.realpathSync(path.resolve(root, relPath)); if (candidate !== root && !candidate.startsWith(root + path.sep)) { throw new Error(`Image path escapes project root: ${relPath}`); } ``` 2. Reject absolute paths and paths containing parent traversal components before resolution. 3. Resolve symbolic links with `fs.realpathSync()` and perform containment checks on the resolved result. 4. Allow only expected image extensions and verify file signatures before uploading. 5. Do not derive archive destination names from user-controlled relative paths. Generate sanitized internal names and maintain a separate mapping. 6. Apply the same containment validation to both source and staging destination paths. 7. Require explicit user confirmation of the final normalized file list before network upload. ]]>
