fix: security (#355)

This commit is contained in:
Charles GTE
2026-07-04 12:24:23 +02:00
committed by GitHub
parent a3d0a6e1df
commit 8dcb406790
2 changed files with 60 additions and 6 deletions
+55 -6
View File
@@ -1,11 +1,16 @@
import {NextResponse} from "next/server";
import fs from "fs";
import path from "path";
import {and, eq} from "drizzle-orm";
import {db} from "@/db";
import * as drizzleDb from "@/db";
import {env} from "@/env.mjs";
import {logger} from "@/lib/logger";
const log = logger.child({module: "api/tus/hooks"});
const FILE_PATH_RE = /^backups\/\d{4}-\d{2}-\d{2}\/[A-Za-z0-9._-]+$/;
export async function POST(request: Request) {
try {
const body = await request.json();
@@ -24,17 +29,61 @@ export async function POST(request: Request) {
event.Upload.Offset === event.Upload.Size
) {
const id = event.Upload.ID;
const fileName = headers["X-File-Name"]?.[0];
const filePath = headers["X-File-Path"]?.[0];
if (!filePath) {
return NextResponse.json({error: "Missing X-File-Path"}, {status: 500});
if (!filePath || !FILE_PATH_RE.test(filePath)) {
log.warn({filePath}, "Rejected invalid X-File-Path in TUS hook");
return NextResponse.json({error: "Invalid file path"}, {status: 400});
}
const generatedId = headers["X-Generated-Id"]?.[0];
const backupStorageId = headers["X-Backup-Storage-Id"]?.[0];
console.log(`Backup Storage ID : ${backupStorageId}`);
console.log(`generatedId : ${generatedId}`);
if (backupStorageId) {
if (!generatedId) {
log.warn("Missing X-Generated-Id in TUS hook");
return NextResponse.json({error: "Forbidden"}, {status: 403});
}
const database = await db.query.database.findFirst({
where: eq(drizzleDb.schemas.database.agentDatabaseId, generatedId),
});
if (!database) {
log.warn({generatedId}, "No database for X-Generated-Id in TUS hook");
return NextResponse.json({error: "Forbidden"}, {status: 403});
}
const storage = await db.query.backupStorage.findFirst({
where: and(
eq(drizzleDb.schemas.backupStorage.id, backupStorageId),
),
with: {backup: true},
});
if (!storage || storage.backup?.databaseId !== database.id) {
log.warn({backupStorageId, generatedId}, "No pending backup storage matches TUS hook");
return NextResponse.json({error: "Forbidden"}, {status: 403});
}
} else {
log.warn("TUS hook without X-Backup-Storage-Id (legacy agent)");
}
const uploadDir = path.join(env.PRIVATE_PATH!, "/uploads/");
const oldFilePath = path.join(uploadDir, "tmp", id);
const newFilePath = path.join(uploadDir, filePath);
const resolvedBase = path.resolve(uploadDir);
const newFilePath = path.resolve(resolvedBase, filePath);
const rel = path.relative(resolvedBase, newFilePath);
if (rel.startsWith("..") || path.isAbsolute(rel)) {
log.warn({filePath}, "Rejected path traversal in TUS hook");
return NextResponse.json({error: "Invalid file path"}, {status: 400});
}
fs.mkdirSync(path.dirname(newFilePath), {recursive: true});
@@ -74,7 +123,7 @@ export async function POST(request: Request) {
}
return NextResponse.json({});
} catch (error) {
log.error({error: error},"TUS Hook error");
log.error({error: error}, "TUS Hook error");
return NextResponse.json({error: "Internal server error"}, {status: 500});
}
}
}
+5
View File
@@ -30,6 +30,11 @@ http {
proxy_set_header Connection "upgrade";
}
location = /api/tus/hooks {
deny all;
return 404;
}
location / {
proxy_pass http://127.0.0.1:3000;