fix(admin): scrub raw Prisma error from restore-failed response (v1.4.15 H1)

The catch block around the restore transaction echoed the raw error
message back to the admin client (`Restore transaction failed: <full
prisma message>`). Even on an admin-only endpoint that leaks column
names, constraint names and query fragments to anyone with admin
access — lower than CRITICAL but unnecessary surface for a future
supply-chain or impersonation scenario.

Response now returns a stable "Restore failed" 500. The verbose text
still lands in the audit-log row (admin-readable, forensic) and in
the Wide Event under `meta.restoreFailReason` (operator-readable),
so root-cause investigation is unaffected.
This commit is contained in:
Marc-André Bombeck committed 2026-05-10 00:23:33 +02:00
1 parent 2a7ef72b0f
commit fdac9e2517
1 file changed
+10 -5
@@ -376,6 +376,13 @@ const handler = apiHandler(
};
});
} catch (err) {
// Scrub the raw Prisma / driver error from the wire response —
// even on an admin endpoint, leaking column names, constraint
// names or query fragments lowers the cost of a future supply-
// chain attack against this surface. The verbose text still
// lands in the audit row (admin-readable) and the Wide Event
// (operator-readable), so root-cause investigation is unaffected.
const verbose = err instanceof Error ? err.message : String(err);
await auditLog("admin.backups.restore.failed", {
userId: admin.id,
ipAddress: getClientIp(request),
@@ -383,13 +390,11 @@ const handler = apiHandler(
backupId: id,
ownerId,
reason: "transaction_failed",
message: err instanceof Error ? err.message : String(err),
message: verbose,
},
});
return apiError(
`Restore transaction failed: ${err instanceof Error ? err.message : "unknown"}`,
500,
);
annotate({ meta: { restoreFailReason: verbose } });
return apiError("Restore failed", 500);
}
const summary = summarizeBackup(payload);