mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
fix: security (#355)
This commit is contained in:
@@ -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});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user