mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 22:26:13 +00:00
fix(security): require admin Umami and GlitchTip URLs to be publicly reachable
Both fields are admin-supplied and feed into outbound server-side fetches: the Umami script URL is proxied through `/api/monitoring/umami-script` (and the script body is re-served from the app origin under a strict CSP), and the GlitchTip DSN derives the `envelopeUrl` / `storeUrl` the server posts errors to. The previous validators only checked the protocol, so an admin (or an attacker holding the admin account) could point either at a private-IP target and drive the server into the RFC1918 / loopback / metadata space. The `isPublicUrl` helper already governs every other admin-supplied outbound URL the app dials; this aligns Umami + GlitchTip with that posture. GlitchTip DSNs carry the public key in userinfo (`https://<key>@host/projectid`) — the WHATWG URL parser strips the userinfo from the hostname, so the helper validates the host as expected. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
a4e4aaf009
commit
e16f4038ed
1 file changed
+18
-14
@@ -1,4 +1,5 @@
|
||||
import { z } from "zod/v4";
|
||||
import { isPublicUrl } from "@/lib/validations/notifications";
|
||||
|
||||
/**
|
||||
* Schema for admin settings update (PUT /api/admin/settings).
|
||||
@@ -29,14 +30,16 @@ export const adminSettingsSchema = z
|
||||
(v) => {
|
||||
const trimmed = v.trim();
|
||||
if (!trimmed) return true;
|
||||
try {
|
||||
const parsed = new URL(trimmed);
|
||||
return ["https:", "http:"].includes(parsed.protocol);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
// The Umami script proxy fetches this URL server-side and
|
||||
// re-serves the body from the app origin under a strict CSP
|
||||
// — a private-IP target would be an SSRF channel. Match the
|
||||
// posture every other admin-supplied outbound URL takes.
|
||||
return isPublicUrl(trimmed);
|
||||
},
|
||||
{
|
||||
message:
|
||||
"Umami script URL must be a public HTTP(S) endpoint (no private / loopback / metadata addresses)",
|
||||
},
|
||||
{ message: "Umami script URL is invalid" },
|
||||
)
|
||||
.optional(),
|
||||
umamiWebsiteId: z.string().optional(),
|
||||
@@ -47,14 +50,15 @@ export const adminSettingsSchema = z
|
||||
(v) => {
|
||||
const trimmed = v.trim();
|
||||
if (!trimmed) return true;
|
||||
try {
|
||||
const parsed = new URL(trimmed);
|
||||
return ["https:", "http:"].includes(parsed.protocol);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
// Glitchtip DSNs are `https://<key>@host/projectid`. The
|
||||
// userinfo is the auth material; the SSRF surface is the
|
||||
// host — same `isPublicUrl` posture as the Umami URL.
|
||||
return isPublicUrl(trimmed);
|
||||
},
|
||||
{
|
||||
message:
|
||||
"Glitchtip DSN must point to a public HTTP(S) endpoint (no private / loopback / metadata addresses)",
|
||||
},
|
||||
{ message: "Glitchtip DSN is invalid" },
|
||||
)
|
||||
.optional(),
|
||||
glitchtipEnvironment: z.string().optional(),
|
||||
|
||||
Reference in new issue
Block a user