mirror of
https://github.com/yusufipk/OpenFrame.git
synced 2026-09-11 09:36:08 +00:00
The invitation preview surfaces (/invitations/accept and /register?invitationToken=) are the
only unauthenticated reads of invitation data, and each render costs two database queries.
They are now rate limited before the lookup can touch the database: a generous per-IP bucket
that bounds enumeration across tokens, plus a tight per-IP+token bucket that stops repeated
probing of a single invitation. Tokens are hashed before they reach the rate_limits table.
A throttled lookup says so ("we couldn't check this invitation right now") instead of claiming
the invitation is invalid, and signed-in acceptance is not gated by it.
The callback sanitizer also checked only the origin, which is not enough: an attacker can
smuggle a host into the path of an otherwise same-origin URL — new URL('https://app//evil.com')
keeps our origin but yields a pathname of //evil.com, which navigation sinks resolve as
protocol-relative and follow off-site. Paths are now required to be rooted at a single slash,
and the login redirect re-checks at the sink.
getClientIp is split so server components that only have `await headers()` resolve the client
IP through the same trusted-proxy logic as route handlers.
41 lines
1.6 KiB
TypeScript
41 lines
1.6 KiB
TypeScript
import { isInviteCodeRequired } from '@/lib/feature-flags';
|
|
import { getInvitationPreviewByToken } from '@/lib/invitations';
|
|
import { isInvitationPreviewAllowed } from '@/lib/invitation-preview-limit';
|
|
import RegisterPageClient from './register-page-client';
|
|
|
|
interface RegisterPageProps {
|
|
searchParams: Promise<{ invitationToken?: string }>;
|
|
}
|
|
|
|
export default async function RegisterPage({ searchParams }: RegisterPageProps) {
|
|
const googleEnabled = Boolean(process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET);
|
|
const githubEnabled = Boolean(process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET);
|
|
|
|
const token = (await searchParams)?.invitationToken?.trim();
|
|
|
|
// Unauthenticated invitation lookup — throttled per IP (and per token) before it can
|
|
// touch the database. When throttled we skip the lookup instead of guessing at a verdict.
|
|
const previewAllowed = token ? await isInvitationPreviewAllowed(token) : false;
|
|
const preview = token && previewAllowed ? await getInvitationPreviewByToken(token) : null;
|
|
const invitation =
|
|
preview && preview.status === 'PENDING' && !preview.isExpired
|
|
? {
|
|
email: preview.email,
|
|
inviterName: preview.inviterName,
|
|
roleLabel: preview.roleLabel,
|
|
scopeLabel: preview.scopeLabel,
|
|
targetName: preview.targetName,
|
|
}
|
|
: null;
|
|
|
|
return (
|
|
<RegisterPageClient
|
|
requireInviteCode={isInviteCodeRequired()}
|
|
googleEnabled={googleEnabled}
|
|
githubEnabled={githubEnabled}
|
|
invitation={invitation}
|
|
invitationLookupThrottled={Boolean(token) && !previewAllowed}
|
|
/>
|
|
);
|
|
}
|