Skip to content

Commit 4fa6cd8

Browse files
authored
Merge commit from fork
fix: validate fileId to prevent path traversal in chunked import
2 parents 59f1bd6 + 90766fe commit 4fa6cd8

4 files changed

Lines changed: 40 additions & 2 deletions

File tree

bun.lock

Lines changed: 7 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,15 @@
1919
"zod": "4.1.13"
2020
},
2121
"devDependencies": {
22-
"@types/node": "24.10.2",
2322
"@nuxtjs/sitemap": "7.4.9",
2423
"@types/bcrypt": "6.0.0",
2524
"@types/busboy": "1.5.4",
25+
"@types/node": "24.10.2",
2626
"@vite-pwa/nuxt": "1.1.0",
2727
"@waradu/keyboard": "8.0.0",
2828
"badgen": "3.2.3",
2929
"chart.js": "4.5.1",
30+
"dotenv": "^17.4.2",
3031
"lucide-vue-next": "0.556.0",
3132
"nuxt": "4.2.1",
3233
"nuxt-cron": "1.8.0",

prisma.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { defineConfig } from "prisma/config";
2+
import "dotenv/config";
23

34
export default defineConfig({
45
schema: "prisma/schema.prisma",

server/api/import/index.post.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,30 @@ const wakaTimeExportSchema = z.object({
294294
),
295295
});
296296

297+
const FILE_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
298+
299+
function assertSafeFileId(fileId: unknown): void {
300+
if (typeof fileId !== "string" || !FILE_ID_PATTERN.test(fileId)) {
301+
throw handleApiError(
302+
400,
303+
`Rejected import with invalid fileId: ${JSON.stringify(fileId)}`,
304+
"Invalid upload identifier.",
305+
);
306+
}
307+
}
308+
309+
function assertWithinUserDir(userTempDir: string, targetPath: string): void {
310+
const base = path.resolve(userTempDir);
311+
const resolved = path.resolve(targetPath);
312+
if (resolved !== base && !resolved.startsWith(base + path.sep)) {
313+
throw handleApiError(
314+
400,
315+
`Path traversal detected: ${resolved} escapes ${base}`,
316+
"Invalid upload path.",
317+
);
318+
}
319+
}
320+
297321
async function handleChunkUpload(formData: any[], userId: string) {
298322
const fileId = formData.find((p) => p.name === "fileId")?.data.toString();
299323
const chunkIndex = parseInt(
@@ -316,6 +340,8 @@ async function handleChunkUpload(formData: any[], userId: string) {
316340
);
317341
}
318342

343+
assertSafeFileId(fileId);
344+
319345
const MAX_CHUNK_SIZE = 100 * 1024 * 1024;
320346
if (chunk.data && chunk.data.length > MAX_CHUNK_SIZE) {
321347
throw handleApiError(
@@ -336,6 +362,7 @@ async function handleChunkUpload(formData: any[], userId: string) {
336362

337363
const userTempDir = path.join(tmpdir(), "ziit-chunks", userId);
338364
const chunksDir = path.join(userTempDir, fileId);
365+
assertWithinUserDir(userTempDir, chunksDir);
339366
mkdirSync(chunksDir, { recursive: true });
340367

341368
let job = activeJobs.get(fileId);
@@ -387,6 +414,7 @@ async function handleChunkUpload(formData: any[], userId: string) {
387414
}
388415

389416
async function processFileInBackground(fileId: string, userId: string) {
417+
assertSafeFileId(fileId);
390418
const job = activeJobs.get(fileId);
391419
if (!job || job.fileId !== fileId) {
392420
throw handleApiError(
@@ -403,6 +431,8 @@ async function processFileInBackground(fileId: string, userId: string) {
403431
const userTempDir = path.join(tmpdir(), "ziit-chunks", userId);
404432
const chunksDir = path.join(userTempDir, fileId);
405433
const combinedFilePath = path.join(userTempDir, `${fileId}-combined.json`);
434+
assertWithinUserDir(userTempDir, chunksDir);
435+
assertWithinUserDir(userTempDir, combinedFilePath);
406436

407437
try {
408438
handleLog(

0 commit comments

Comments
 (0)