fix: Weiterleitungen innerhalb der Seite relativ statt auf die Produktionsdomain
Sechs Route-Handler (Hinweis-Weiterleitung, E-Mail-Bestaetigung, Newsletter bestaetigen/abmelden, Entra-Login und -Callback) bauten ihr Weiterleitungsziel mit getBaseUrl() absolut zusammen. Ohne gesetztes NEXT_PUBLIC_SITE_URL (lokal, Staging, Vorschau-Host) sprang man damit auf https://wabenhain.com, und die gerade gesetzte Flash-Meldung ging auf dem fremden Host verloren - aufgefallen, als die Admin-Pruefung nach /api/hinweis umleitete. Neuer Helfer lib/redirect.ts (redirectWithinSite) mit relativem Location-Header, analog zu redirect() in Server Actions. Er lehnt alles ausser Pfaden dieser Seite ab ("//host", "/\host", absolute URLs), damit daraus keine offene Weiterleitung werden kann. getSiteUrl() bleibt fuer Adressen, die die Seite verlassen (E-Mails, Stripe, Entra-redirect_uri, Sitemap, Canonicals). proxy.ts bleibt bei new URL("/konto", request.url) (eigener Host): der Proxy verlangt einen absoluten Location-Header, ein relativer endet dort in "Invalid URL" - als Kommentar festgehalten. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
4f442ec06d
commit
52ce37e216
9 files changed
+76
-29
No files matched your search
@@ -0,0 +1,28 @@
|
||||
import { describe, test, expect } from "vitest";
|
||||
import { redirectWithinSite } from "./redirect";
|
||||
|
||||
describe("redirectWithinSite", () => {
|
||||
test("setzt einen relativen Location-Header statt einer absoluten Domain", () => {
|
||||
const response = redirectWithinSite("/konto");
|
||||
expect(response.status).toBe(307);
|
||||
expect(response.headers.get("location")).toBe("/konto");
|
||||
});
|
||||
|
||||
test("behaelt Query-Parameter bei", () => {
|
||||
const response = redirectWithinSite("/newsletter-bestaetigt?status=ok");
|
||||
expect(response.headers.get("location")).toBe("/newsletter-bestaetigt?status=ok");
|
||||
});
|
||||
|
||||
test("laesst sich mit Cookies (Flash-Meldung) versehen", () => {
|
||||
const response = redirectWithinSite("/konto");
|
||||
response.cookies.set("probe", "1");
|
||||
expect(response.headers.get("set-cookie")).toContain("probe=1");
|
||||
});
|
||||
|
||||
test.each(["https://evil.example/konto", "//evil.example/konto", "konto", "/\\evil.example"])(
|
||||
"lehnt %s ab (keine offene Weiterleitung)",
|
||||
(target) => {
|
||||
expect(() => redirectWithinSite(target)).toThrow();
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
/**
|
||||
* Weiterleitung innerhalb der Seite aus einem Route-Handler — mit relativem
|
||||
* Location-Header ("/konto"), den der Browser gegen die Adresse aufloest, unter
|
||||
* der er die Seite gerade aufgerufen hat.
|
||||
*
|
||||
* Gegenstueck zu redirect("/konto") in Server Actions und Seiten, das ebenfalls
|
||||
* relativ weiterleitet. Frueher bauten einige Route-Handler das Ziel mit
|
||||
* getBaseUrl() absolut zusammen: ohne gesetztes NEXT_PUBLIC_SITE_URL (lokal,
|
||||
* Staging, Vorschau-Host) sprang man damit auf die Produktionsseite, und die
|
||||
* gerade gesetzte Flash-Meldung ging auf dem fremden Host verloren.
|
||||
*
|
||||
* Absolut bleiben nur Adressen, die die Seite verlassen: E-Mail-Links,
|
||||
* Stripe-Ruecksprung-URLs, die Entra-redirect_uri, Sitemap und Canonicals —
|
||||
* dafuer ist getSiteUrl() da (lib/url.ts).
|
||||
*/
|
||||
export function redirectWithinSite(path: string, status: 303 | 307 = 307): NextResponse {
|
||||
// Nur echte Pfade dieser Seite: "//host" und "/\host" werten Browser als
|
||||
// Adresse auf einem fremden Host — das waere eine offene Weiterleitung.
|
||||
if (!path.startsWith("/") || path.startsWith("//") || path.startsWith("/\\")) {
|
||||
throw new Error(`redirectWithinSite erwartet einen Pfad dieser Seite, bekam: ${path}`);
|
||||
}
|
||||
return new NextResponse(null, { status, headers: { Location: path } });
|
||||
}
|
||||
Reference in new issue
Block a user