Phase B2: mehrere Domains fuer Kurz-URLs
Eine Domain kann mehreren Gruppen dienen (n:m ueber DomainGrant), und der Slug eines Codes ist nur noch je Domain eindeutig - derselbe sprechende Slug darf also auf zwei Domains liegen. Sicherheitsrelevanter Kern: die Weiterleitung liest den Hostnamen aus X-Forwarded-Host bzw. Host und prueft ihn gegen die Domain-Tabelle. Ein unbekannter Hostname endet mit 404, faellt also auf die Standard-Domain zurueck und findet dort nichts. Ueber einen gefaelschten Header laesst sich damit keine fremde Domain ansprechen. Auch der Hostname fuer die Vorschau kommt aus der Datenbank, nicht aus der Anfrage. - Migration legt die Spalte erst nullable an, fuellt sie mit der Standard-Domain und macht sie danach verpflichtend; der globale Slug-Index weicht dem zusammengesetzten aus (domainId, slug). - Verwaltung -> Domains: registrieren, Gruppen zuteilen, Voreinstellung je Gruppe, abschalten. Domains mit Codes lassen sich weder loeschen noch einer Gruppe entziehen, die noch Codes darunter hat - beides wuerde gedruckte Codes ins Leere laufen lassen. Ein Fehler dabei gefunden und behoben: die Migration kennt BASE_URL nicht und legt die Standard-Domain mit einem Platzhalter-Hostnamen an. Korrigiert wurde er bisher nur beim Oeffnen der Domain-Verwaltung - auf einer frischen Installation waeren also Kurz-URLs auf 'standard.invalid' entstanden. ensureDefaultDomain() laeuft jetzt vor jeder Verwendung. Geprueft: 34 neue Pruefungen fuer Host-Aufloesung, Spoofing, Slug je Domain und Zuteilung; dazu 280 bestehende bei ausgeschalteter Anmeldung.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import type { NextRequest } from "next/server";
|
||||
|
||||
import { prisma } from "@/lib/db";
|
||||
import { ensureDefaultDomain, resolveRequestDomain } from "@/lib/domains";
|
||||
import { getQrType } from "@/lib/qr/registry";
|
||||
import { isValidSlug } from "@/lib/slug";
|
||||
|
||||
@@ -34,6 +35,10 @@ function notFound(message: string): Response {
|
||||
*
|
||||
* Bewusst 302 (temporär): Das Ziel ist jederzeit änderbar, ein permanenter
|
||||
* Redirect würde in Browsern und Zwischenspeichern hängen bleiben.
|
||||
*
|
||||
* Der Slug ist nur je Domain eindeutig, deshalb wird zuerst die Domain aus
|
||||
* dem `Host`-Header bestimmt. Ein unbekannter Hostname endet hier — sonst
|
||||
* ließe sich über einen gefälschten Header ein fremder Code ansprechen.
|
||||
*/
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
@@ -45,7 +50,25 @@ export async function GET(
|
||||
return notFound("Diese Kurz-Adresse ist ungültig.");
|
||||
}
|
||||
|
||||
const code = await prisma.qrCode.findUnique({ where: { slug } });
|
||||
const matched = await resolveRequestDomain(request.headers);
|
||||
|
||||
// Aufruf über eine nicht registrierte Adresse — etwa direkt über die IP im
|
||||
// LAN. Dann gilt die Standard-Domain aus BASE_URL.
|
||||
const domain = matched ?? {
|
||||
...(await ensureDefaultDomain()),
|
||||
isActive: true,
|
||||
};
|
||||
|
||||
if (!domain.isActive) {
|
||||
return notFound(
|
||||
"Diese Adresse ist zurzeit abgeschaltet. Bitte wende dich an denjenigen, der den Code erstellt hat.",
|
||||
);
|
||||
}
|
||||
|
||||
const code = await prisma.qrCode.findUnique({
|
||||
where: { domainId_slug: { domainId: domain.id, slug } },
|
||||
});
|
||||
|
||||
if (!code) {
|
||||
return notFound(
|
||||
"Der Code wurde gelöscht oder hat nie existiert. Bitte wende dich an denjenigen, der ihn erstellt hat.",
|
||||
|
||||
Reference in New Issue
Block a user