From c0e1326c4bc5ec6d61fd7c075b9b405805201660 Mon Sep 17 00:00:00 2001 From: Satoshi Qazi Muhammed Date: Fri, 31 Jul 2026 11:04:13 -0700 Subject: [PATCH] fix(security): remove the dead email-verification function MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The function was never used and carried a real hole while sitting there. `send` took an email address from the request body, did not check who was calling, and returned the verification token in the response — with a `// Remove in production` comment on that line. Anyone could mint a token for another account's address and immediately use it. `Access-Control-Allow-Origin` was `*`. It was never reachable in practice: nothing calls `send`, so no token was ever created, so `verify` could never succeed. The database confirms it — `email_verification_tokens` has zero rows, ever. Real email verification runs through Supabase Auth, not this function. EmailVerification.tsx handles `type=signup` / `email_change` via the URL hash and `getSession()`, and Dashboard.tsx resends through `auth.resend()`. Those paths are untouched. The `?token=` branch that called this function was dead code fed by a token nothing produced, so it goes with it. Removing it also took the `verifying` state with it: `setVerifying` had no remaining callers, so the spinner could never render and every `!verifying` guard was permanently true — conditions that read as if the value could vary. Fixing this properly instead of deleting it would mean building the email delivery the function never had ("In production, send email via email service"), duplicating what Supabase Auth already does correctly. --- web/src/pages/EmailVerification.tsx | 42 ++------ .../functions/email-verification/index.ts | 100 ------------------ 2 files changed, 6 insertions(+), 136 deletions(-) delete mode 100644 web/supabase/functions/email-verification/index.ts diff --git a/web/src/pages/EmailVerification.tsx b/web/src/pages/EmailVerification.tsx index d5a2baa2..17e34ff6 100644 --- a/web/src/pages/EmailVerification.tsx +++ b/web/src/pages/EmailVerification.tsx @@ -10,7 +10,6 @@ export default function EmailVerification() { const [searchParams] = useSearchParams(); const location = useLocation(); const navigate = useNavigate(); - const [verifying, setVerifying] = useState(false); const [verified, setVerified] = useState(false); const [error, setError] = useState(''); const [resending, setResending] = useState(false); @@ -19,7 +18,6 @@ export default function EmailVerification() { // Get email from navigation state (after sign up) const email = location.state?.email; - const token = searchParams.get('token'); const type = searchParams.get('type'); useEffect(() => { @@ -32,29 +30,8 @@ export default function EmailVerification() { setVerified(true); } }); - } else if (token) { - verifyEmail(token); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [token, type]); - - const verifyEmail = async (verifyToken: string) => { - setVerifying(true); - try { - const { error } = await supabase.functions.invoke('email-verification', { - body: { action: 'verify', token: verifyToken } - }); - - if (error) throw error; - - setVerified(true); - } catch (err: unknown) { - const errorMessage = err instanceof Error ? err.message : t('emailVerify.failedToVerify'); - setError(errorMessage); - } finally { - setVerifying(false); - } - }; + }, [type]); const handleResendEmail = async () => { if (!email) return; @@ -80,7 +57,7 @@ export default function EmailVerification() { }; // Show "check your email" screen after sign up - if (email && !token && !type) { + if (email && !type) { return (
@@ -162,18 +139,11 @@ export default function EmailVerification() { {t('emailVerify.title')} - {verifying ? t('emailVerify.verifyingEmail') : t('emailVerify.verificationStatus')} + {t('emailVerify.verificationStatus')} - {verifying && ( -
- -

{t('emailVerify.pleaseWait')}

-
- )} - - {!verifying && verified && ( + {verified && (

{t('emailVerify.success')}

@@ -189,7 +159,7 @@ export default function EmailVerification() {
)} - {!verifying && !verified && error && ( + {!verified && error && (

{t('emailVerify.failed')}

@@ -205,7 +175,7 @@ export default function EmailVerification() {
)} - {!verifying && !verified && !error && !token && !type && ( + {!verified && !error && !type && (

{t('emailVerify.noToken')}

diff --git a/web/supabase/functions/email-verification/index.ts b/web/supabase/functions/email-verification/index.ts deleted file mode 100644 index e2bd0b7c..00000000 --- a/web/supabase/functions/email-verification/index.ts +++ /dev/null @@ -1,100 +0,0 @@ -export const corsHeaders = { - "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Headers": - "authorization, x-client-info, apikey, content-type", -}; - -Deno.serve(async (req) => { - if (req.method === "OPTIONS") { - return new Response("ok", { headers: corsHeaders }); - } - - try { - const { action, token, email } = await req.json(); - - if (action === "send") { - // Generate verification token - const verificationToken = crypto.randomUUID(); - - // Store token in database (expires in 24 hours) - const { createClient } = - await import("https://esm.sh/@supabase/supabase-js@2"); - const supabaseUrl = Deno.env.get("SUPABASE_URL")!; - const supabaseKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!; - const supabase = createClient(supabaseUrl, supabaseKey); - - const { data: user } = await supabase.auth.admin.getUserByEmail(email); - - if (!user?.user) { - throw new Error("User not found"); - } - - await supabase.from("email_verification_tokens").insert({ - user_id: user.user.id, - token: verificationToken, - email: email, - expires_at: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), - }); - - // In production, send email via email service - // For now, return the token - return new Response( - JSON.stringify({ - success: true, - message: "Verification email sent", - token: verificationToken, // Remove in production - }), - { headers: { ...corsHeaders, "Content-Type": "application/json" } }, - ); - } - - if (action === "verify") { - const { createClient } = - await import("https://esm.sh/@supabase/supabase-js@2"); - const supabaseUrl = Deno.env.get("SUPABASE_URL")!; - const supabaseKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!; - const supabase = createClient(supabaseUrl, supabaseKey); - - // Check token validity - const { data: tokenData } = await supabase - .from("email_verification_tokens") - .select("*") - .eq("token", token) - .single(); - - if (!tokenData || new Date(tokenData.expires_at) < new Date()) { - throw new Error("Invalid or expired token"); - } - - // Update user profile - await supabase - .from("profiles") - .update({ - email_verified: true, - email_verified_at: new Date().toISOString(), - }) - .eq("id", tokenData.user_id); - - // Delete used token - await supabase - .from("email_verification_tokens") - .delete() - .eq("token", token); - - return new Response( - JSON.stringify({ - success: true, - message: "Email verified successfully", - }), - { headers: { ...corsHeaders, "Content-Type": "application/json" } }, - ); - } - - throw new Error("Invalid action"); - } catch (error) { - return new Response(JSON.stringify({ error: error.message }), { - headers: { ...corsHeaders, "Content-Type": "application/json" }, - status: 400, - }); - } -});