-
Notifications
You must be signed in to change notification settings - Fork 45
fix: prevent users from inviting themselves in referral flow #386
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -4,6 +4,8 @@ import { referralInviteEmail, sendEmail } from "@/lib/email"; | |||||||||||||||||||||||||||||||
| import { createServiceClient } from "@/lib/supabase/service"; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| type AnySupabase = any; | ||||||||||||||||||||||||||||||||
| const MAX_EMAIL_ENTRIES_PER_REQUEST = 200; | ||||||||||||||||||||||||||||||||
| const MAX_INVITES_PER_REQUEST = 20; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // GET /api/referrals - List my referrals | ||||||||||||||||||||||||||||||||
| export async function GET(request: NextRequest) { | ||||||||||||||||||||||||||||||||
|
|
@@ -69,9 +71,9 @@ export async function POST(request: NextRequest) { | |||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| if (emails.length > 20) { | ||||||||||||||||||||||||||||||||
| if (emails.length > MAX_EMAIL_ENTRIES_PER_REQUEST) { | ||||||||||||||||||||||||||||||||
| return NextResponse.json( | ||||||||||||||||||||||||||||||||
| { error: "Maximum 20 invites at a time" }, | ||||||||||||||||||||||||||||||||
| { error: `Maximum ${MAX_EMAIL_ENTRIES_PER_REQUEST} email entries at a time` }, | ||||||||||||||||||||||||||||||||
| { status: 400 } | ||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
@@ -80,11 +82,22 @@ export async function POST(request: NextRequest) { | |||||||||||||||||||||||||||||||
| // Only valid emails should count toward throttle limits | ||||||||||||||||||||||||||||||||
| const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; | ||||||||||||||||||||||||||||||||
| const normalizedEmails = emails.map((e: string) => e.trim().toLowerCase()); | ||||||||||||||||||||||||||||||||
| const validEmails = normalizedEmails.filter((e: string) => emailRegex.test(e)); | ||||||||||||||||||||||||||||||||
| const userEmail = user.email?.toLowerCase(); | ||||||||||||||||||||||||||||||||
| const validEmails = Array.from( | ||||||||||||||||||||||||||||||||
| new Set(normalizedEmails.filter((e: string) => emailRegex.test(e) && e !== userEmail)) | ||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| if (validEmails.length === 0) { | ||||||||||||||||||||||||||||||||
| const onlySelf = normalizedEmails.every(e => e === userEmail || !emailRegex.test(e)); | ||||||||||||||||||||||||||||||||
| return NextResponse.json( | ||||||||||||||||||||||||||||||||
| { error: onlySelf ? "You cannot invite yourself" : "No valid email addresses provided" }, | ||||||||||||||||||||||||||||||||
| { status: 400 } | ||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
Comment on lines
90
to
+96
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| if (validEmails.length > MAX_INVITES_PER_REQUEST) { | ||||||||||||||||||||||||||||||||
| return NextResponse.json( | ||||||||||||||||||||||||||||||||
| { error: "No valid email addresses provided" }, | ||||||||||||||||||||||||||||||||
| { error: `Maximum ${MAX_INVITES_PER_REQUEST} invites at a time` }, | ||||||||||||||||||||||||||||||||
| { status: 400 } | ||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
user.emailis typed asstring | undefinedin Supabase Auth — the optional-chain on line 85 confirms the developer knows this. WhenuserEmailresolves toundefined, the filter conditione !== userEmailbecomese !== undefined, which is alwaystruefor any email string. That means the entire self-invite guard is silently bypassed for accounts without a stored email (e.g. certain OAuth providers), and the PR's primary goal is not met for those users.