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
+11 -4
View File
@@ -2,16 +2,23 @@ import { Prisma } from "@prisma/client";
import { prisma } from "@/lib/db";
import { slugify } from "@/lib/utils";
/** Erzeugt einen eindeutigen Slug, notfalls mit angehaengtem Zaehler. */
export async function uniqueSlug(name: string, ignoreId?: string): Promise<string> {
/**
* Erzeugt einen je Besitzer eindeutigen Slug, notfalls mit angehaengtem
* Zaehler. Zwei Benutzer duerfen also beide ein Projekt "dashboard" haben.
*/
export async function uniqueSlug(
name: string,
ownerId: string,
ignoreId?: string,
): Promise<string> {
const base = slugify(name) || "projekt";
let candidate = base;
let counter = 2;
// In der Praxis endet die Schleife nach wenigen Runden.
for (let attempt = 0; attempt < 50; attempt += 1) {
const existing = await prisma.project.findUnique({
where: { slug: candidate },
const existing = await prisma.project.findFirst({
where: { slug: candidate, ownerId },
select: { id: true },
});
if (!existing || existing.id === ignoreId) return candidate;