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,
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
import {
|
||||
ADMIN_MFA_PROOF_COOKIE,
|
||||
HIGH_RISK_ADMIN_PROOF_COOKIE,
|
||||
isSameOriginAdminMutation,
|
||||
isTrustedAdminMutationRequest,
|
||||
resolveAdminMfaStatus,
|
||||
verifyAdminMfaProof,
|
||||
verifyHighRiskAdminProof,
|
||||
@@ -43,7 +43,7 @@ export function requestId(request: Request): string {
|
||||
}
|
||||
|
||||
export async function requireAdminMutation(request: Request, permission: AdminPermission) {
|
||||
if (!isSameOriginAdminMutation(request.headers.get("origin"), request.url)) {
|
||||
if (!isTrustedAdminMutationRequest(request, process.env.ADMIN_USER_ORIGIN)) {
|
||||
throw new AdminAuthorizationError("请求来源不可信", 403);
|
||||
}
|
||||
return requirePermission(permission, request.headers);
|
||||
|
||||
Reference in New Issue
Block a user