feat(security): alle Daten auf den Besitzer einschraenken

Bisher sah jede Anfrage alles. Jetzt traegt jede Abfrage eine
Besitzerbedingung, und zwar an erster Stelle in der where-Klausel statt als
nachtraeglicher Vergleich - so kann sie nicht versehentlich uebersprungen
werden.

- Lesen: findFirst statt findUnique, immer mit ownerId
- Schreiben und Loeschen: updateMany/deleteMany mit ownerId in der
  where-Klausel. Fremde Datensaetze werden dadurch gar nicht erst getroffen,
  und die betroffene Zeilenzahl sagt, ob es den Datensatz gab.
- Notizen haengen ueber project.ownerId am Besitzer
- Beim Verknuepfen eines Projekts wird geprueft, dass die Git-Verbindung dem
  Aufrufer gehoert - sonst liesse sich eine fremde Verbindung und damit ein
  fremder Token unterschieben
- Fremde IDs liefern 404 statt 403: die Antwort bestaetigt nicht einmal,
  dass es den Datensatz gibt
- Ausgehende Endpunkte (Sync, Verbindungstest, Repo-Liste) zusaetzlich
  ratenbegrenzt, Basis-URLs gegen den SSRF-Schutz geprueft
- Slug und Verbindungsname werden je Besitzer auf Eindeutigkeit geprueft

Nachgewiesen: ein zweites Konto erhaelt auf GET, PATCH, DELETE, Notiz
anlegen und Sync eines fremden Projekts durchgaengig 404, auf die
Admin-Endpunkte 403, und das Zielprojekt bleibt unveraendert.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-25 21:30:59 +02:00
co-authored by Claude Opus 5
parent cf4103a46c
commit ed77ffaed2
13 changed files with 351 additions and 122 deletions
+19 -4
View File
@@ -4,6 +4,8 @@ import { FileText } from "lucide-react";
import { GitPanel } from "@/components/GitPanel";
import { NotesPanel } from "@/components/NotesPanel";
import { ProjectHeader } from "@/components/ProjectHeader";
import { requireUserPage } from "@/lib/auth/guard";
import { getSessionUser } from "@/lib/auth/session";
import { prisma } from "@/lib/db";
import { connectionSafeSelect, projectDetailInclude } from "@/lib/projects";
import type { ConnectionSafe, ProjectDetail } from "@/types";
@@ -13,9 +15,12 @@ export const dynamic = "force-dynamic";
type PageProps = { params: Promise<{ id: string }> };
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
const user = await getSessionUser();
if (!user) return { title: "Projekt" };
const { id } = await params;
const project = await prisma.project.findUnique({
where: { id },
const project = await prisma.project.findFirst({
where: { id, ownerId: user.id },
select: { name: true, summary: true },
});
return {
@@ -25,11 +30,21 @@ export async function generateMetadata({ params }: PageProps): Promise<Metadata>
}
export default async function ProjectPage({ params }: PageProps) {
const user = await requireUserPage();
const { id } = await params;
const [project, connections] = await Promise.all([
prisma.project.findUnique({ where: { id }, include: projectDetailInclude }),
prisma.gitConnection.findMany({ select: connectionSafeSelect, orderBy: { name: "asc" } }),
// findFirst mit Besitzerbedingung: ein fremdes Projekt fuehrt zu 404,
// nicht zu einer Fehlermeldung, die seine Existenz bestaetigt.
prisma.project.findFirst({
where: { id, ownerId: user.id },
include: projectDetailInclude,
}),
prisma.gitConnection.findMany({
where: { ownerId: user.id },
select: connectionSafeSelect,
orderBy: { name: "asc" },
}),
]);
if (!project) notFound();