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 {NextResponse} from "next/server";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import path from "path";
|
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 {env} from "@/env.mjs";
|
||||||
import {logger} from "@/lib/logger";
|
import {logger} from "@/lib/logger";
|
||||||
|
|
||||||
const log = logger.child({module: "api/tus/hooks"});
|
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) {
|
export async function POST(request: Request) {
|
||||||
try {
|
try {
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
@@ -24,17 +29,61 @@ export async function POST(request: Request) {
|
|||||||
event.Upload.Offset === event.Upload.Size
|
event.Upload.Offset === event.Upload.Size
|
||||||
) {
|
) {
|
||||||
const id = event.Upload.ID;
|
const id = event.Upload.ID;
|
||||||
const fileName = headers["X-File-Name"]?.[0];
|
|
||||||
const filePath = headers["X-File-Path"]?.[0];
|
const filePath = headers["X-File-Path"]?.[0];
|
||||||
|
|
||||||
if (!filePath) {
|
if (!filePath || !FILE_PATH_RE.test(filePath)) {
|
||||||
return NextResponse.json({error: "Missing X-File-Path"}, {status: 500});
|
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 uploadDir = path.join(env.PRIVATE_PATH!, "/uploads/");
|
||||||
|
|
||||||
const oldFilePath = path.join(uploadDir, "tmp", id);
|
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});
|
fs.mkdirSync(path.dirname(newFilePath), {recursive: true});
|
||||||
|
|
||||||
@@ -74,7 +123,7 @@ export async function POST(request: Request) {
|
|||||||
}
|
}
|
||||||
return NextResponse.json({});
|
return NextResponse.json({});
|
||||||
} catch (error) {
|
} 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});
|
return NextResponse.json({error: "Internal server error"}, {status: 500});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,11 @@ http {
|
|||||||
proxy_set_header Connection "upgrade";
|
proxy_set_header Connection "upgrade";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location = /api/tus/hooks {
|
||||||
|
deny all;
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
proxy_pass http://127.0.0.1:3000;
|
proxy_pass http://127.0.0.1:3000;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user