fix(admin): secure proxied model mutations
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { createHmac, timingSafeEqual } from "node:crypto";
|
||||
|
||||
import type { IdentityUser } from "@/modules/identity/contracts";
|
||||
import { normalizeIdentityHost } from "@/modules/identity/host";
|
||||
|
||||
export type AdminRole =
|
||||
| "owner"
|
||||
@@ -95,6 +96,67 @@ export function isSameOriginAdminMutation(
|
||||
}
|
||||
}
|
||||
|
||||
function singleForwardedValue(value: string | null): string | null {
|
||||
const normalized = value?.trim();
|
||||
return normalized && !normalized.includes(",") ? normalized : null;
|
||||
}
|
||||
|
||||
function configuredAdminOrigin(value: string): URL | null {
|
||||
try {
|
||||
const url = new URL(value);
|
||||
const isLocalhost = url.hostname === "localhost" || url.hostname.endsWith(".localhost");
|
||||
if (
|
||||
(url.protocol !== "https:"
|
||||
&& !(isLocalhost && url.protocol === "http:"))
|
||||
|| url.username
|
||||
|| url.password
|
||||
|| url.pathname !== "/"
|
||||
|| url.search
|
||||
|| url.hash
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return url;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function isTrustedAdminMutationRequest(
|
||||
request: Request,
|
||||
adminOriginValue?: string,
|
||||
): boolean {
|
||||
const origin = request.headers.get("origin");
|
||||
const configuredValue = adminOriginValue?.trim();
|
||||
if (!configuredValue) {
|
||||
return isSameOriginAdminMutation(origin, request.url);
|
||||
}
|
||||
|
||||
const adminOrigin = configuredAdminOrigin(configuredValue);
|
||||
if (!adminOrigin || origin !== adminOrigin.origin) return false;
|
||||
|
||||
const hasForwardedHost = request.headers.has("x-forwarded-host");
|
||||
const hasForwardedProto = request.headers.has("x-forwarded-proto");
|
||||
if (!hasForwardedHost && !hasForwardedProto) {
|
||||
return isSameOriginAdminMutation(origin, request.url);
|
||||
}
|
||||
|
||||
const forwardedHostValue = request.headers.get("x-forwarded-host");
|
||||
const forwardedProtoValue = request.headers.get("x-forwarded-proto");
|
||||
|
||||
const host = normalizeIdentityHost(request.headers.get("host"));
|
||||
const forwardedHost = normalizeIdentityHost(forwardedHostValue);
|
||||
const forwardedProto = singleForwardedValue(forwardedProtoValue)?.toLowerCase();
|
||||
return Boolean(
|
||||
host
|
||||
&& forwardedHost
|
||||
&& forwardedProto
|
||||
&& host === forwardedHost
|
||||
&& forwardedHost === adminOrigin.host.toLowerCase()
|
||||
&& `${forwardedProto}:` === adminOrigin.protocol,
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveAdminMfaStatus(
|
||||
required: boolean,
|
||||
enrolled: boolean,
|
||||
|
||||
Reference in New Issue
Block a user