Files
classroomio/supabase/migrations/20241203000001_email_verification_security.sql
T
Rotimi Best 25d1a8d710 Security Release v0.2.6: Email Verification Bypass Vulnerabilities Fixed
Critical security fixes addressing email verification bypass vulnerabilities:

Security Issues Fixed:
- Email verification status manipulation through client-side profile updates
- Base64 token forgery allowing arbitrary email verification
- DOM manipulation bypasses of email verification modal

Backend Security Enhancements:
- Secure verification tokens table with cryptographic token generation
- RLS policies preventing direct email verification status changes
- Server-side validation functions with expiration and ownership checks
- Database triggers blocking unauthorized verification status updates
- IP address logging and audit trails for security monitoring

Frontend Security Protections:
- Multi-layered DOM protection preventing UI manipulation bypasses
- Continuous monitoring and re-application of interaction restrictions
- CSS-based backup protection even if JavaScript is disabled
- Mutation observer to protect dynamically added elements
- Enhanced CSP configuration for Senja widget support

API Security Updates:
- Secure token-based verification replacing insecure Base64 method
- Comprehensive error handling with security-focused user messages
- Token validation with signature verification and expiration checks
- Legacy verification method blocking with security warnings
- Enhanced verification email generation with secure tokens

Files Modified:
- Database: New email_verification_tokens table and security functions
- API: Secure verification endpoints with token validation
- Frontend: DOM protection and verification modal enhancements
- CSP: Updated Content Security Policy configuration
- Blog: Security advisory for CVE-2025-65675 disclosure

This release patches critical authentication bypass vulnerabilities and
implements defense-in-depth security measures across the entire platform.

Security researchers and users should upgrade immediately.
2025-12-03 15:33:04 +01:00

281 lines
8.7 KiB
PL/PgSQL

-- Drop existing objects first to ensure clean migration
DROP TABLE IF EXISTS "public"."email_verification_tokens" CASCADE;
DROP TRIGGER IF EXISTS profile_email_verification_protection ON "public"."profile";
DROP FUNCTION IF EXISTS prevent_email_verification_manipulation();
-- Drop all versions of verify_email_with_token function
DROP FUNCTION IF EXISTS "public"."verify_email_with_token"(text, text);
DROP FUNCTION IF EXISTS "public"."verify_email_with_token"(text, inet);
DROP FUNCTION IF EXISTS "public"."verify_email_with_token"(text);
-- Drop all versions of create_email_verification_token function
DROP FUNCTION IF EXISTS "public"."create_email_verification_token"(uuid, text, text);
DROP FUNCTION IF EXISTS "public"."create_email_verification_token"(uuid, text, inet);
DROP FUNCTION IF EXISTS "public"."create_email_verification_token"(uuid, text);
DROP FUNCTION IF EXISTS "public"."cleanup_expired_verification_tokens"();
CREATE TABLE IF NOT EXISTS "public"."email_verification_tokens" (
"id" uuid DEFAULT gen_random_uuid() PRIMARY KEY,
"profile_id" uuid REFERENCES "public"."profile"("id") ON DELETE CASCADE,
"token" text UNIQUE NOT NULL,
"email" text NOT NULL,
"created_at" timestamp with time zone DEFAULT timezone('utc', now()),
"expires_at" timestamp with time zone NOT NULL,
"used_at" timestamp with time zone,
"created_by_ip" inet,
"used_by_ip" inet
);
-- Drop existing policies and create secure profile update policy
DROP POLICY IF EXISTS "Users can update own profile." ON "public"."profile";
DROP POLICY IF EXISTS "Users can update own profile safely" ON "public"."profile";
-- Create a restrictive policy that prevents email verification manipulation
CREATE POLICY "Users can update own profile safely" ON "public"."profile"
FOR UPDATE
USING (auth.uid() = id)
WITH CHECK (auth.uid() = id);
-- Create a trigger to prevent direct email verification status changes
CREATE OR REPLACE FUNCTION prevent_email_verification_manipulation()
RETURNS TRIGGER
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
BEGIN
-- Only allow email verification changes via our secure function
IF (OLD.is_email_verified != NEW.is_email_verified OR
OLD.verified_at IS DISTINCT FROM NEW.verified_at) THEN
-- Check if this is being called by our secure verification function
IF current_setting('app.verification_context', true) != 'secure_verify' THEN
RAISE EXCEPTION 'Email verification status can only be changed through secure verification process';
END IF;
END IF;
RETURN NEW;
END;
$$;
CREATE TRIGGER profile_email_verification_protection
BEFORE UPDATE ON "public"."profile"
FOR EACH ROW
EXECUTE FUNCTION prevent_email_verification_manipulation();
CREATE OR REPLACE FUNCTION "public"."verify_email_with_token"(
token_input text,
user_ip text DEFAULT NULL
)
RETURNS jsonb
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
DECLARE
token_record RECORD;
result jsonb;
ip_addr inet;
BEGIN
-- Safely convert IP string to inet, handle invalid IPs
BEGIN
IF user_ip IS NOT NULL AND user_ip != 'unknown' AND user_ip != '' THEN
ip_addr := user_ip::inet;
ELSE
ip_addr := NULL;
END IF;
EXCEPTION WHEN others THEN
ip_addr := NULL;
END;
SELECT
evt.id,
evt.profile_id,
evt.email,
evt.expires_at,
evt.used_at,
p.email as current_email
INTO token_record
FROM email_verification_tokens evt
JOIN profile p ON p.id = evt.profile_id
WHERE evt.token = token_input;
-- Check if token exists
IF NOT FOUND THEN
RETURN jsonb_build_object(
'success', false,
'error', 'INVALID_TOKEN',
'message', 'Verification token not found or invalid'
);
END IF;
-- Check if token already used
IF token_record.used_at IS NOT NULL THEN
RETURN jsonb_build_object(
'success', false,
'error', 'TOKEN_ALREADY_USED',
'message', 'This verification link has already been used'
);
END IF;
-- Check if token expired
IF token_record.expires_at < NOW() THEN
RETURN jsonb_build_object(
'success', false,
'error', 'TOKEN_EXPIRED',
'message', 'Verification link has expired. Please request a new one.'
);
END IF;
-- Verify email matches current profile email
IF token_record.email != token_record.current_email THEN
RETURN jsonb_build_object(
'success', false,
'error', 'EMAIL_MISMATCH',
'message', 'Email address has been changed. Please request a new verification link.'
);
END IF;
-- Mark token as used
UPDATE email_verification_tokens
SET
used_at = NOW(),
used_by_ip = ip_addr
WHERE id = token_record.id;
-- Set context for secure verification
PERFORM set_config('app.verification_context', 'secure_verify', true);
-- Update profile as verified
UPDATE profile
SET
is_email_verified = true,
verified_at = NOW()
WHERE id = token_record.profile_id;
-- Reset context
PERFORM set_config('app.verification_context', '', true);
-- Clean up old/expired tokens for this profile
DELETE FROM email_verification_tokens
WHERE
profile_id = token_record.profile_id
AND id != token_record.id;
RETURN jsonb_build_object(
'success', true,
'message', 'Email successfully verified',
'profile_id', token_record.profile_id
);
END;
$$;
CREATE OR REPLACE FUNCTION "public"."create_email_verification_token"(
profile_id_input uuid,
email_input text,
creator_ip text DEFAULT NULL
)
RETURNS jsonb
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
DECLARE
new_token text;
token_id uuid;
profile_exists boolean;
ip_addr inet;
BEGIN
-- Safely convert IP string to inet, handle invalid IPs
BEGIN
IF creator_ip IS NOT NULL AND creator_ip != 'unknown' AND creator_ip != '' THEN
ip_addr := creator_ip::inet;
ELSE
ip_addr := NULL;
END IF;
EXCEPTION WHEN others THEN
ip_addr := NULL;
END;
-- Verify profile exists and email matches
SELECT EXISTS(
SELECT 1 FROM profile
WHERE id = profile_id_input
AND email = email_input
) INTO profile_exists;
IF NOT profile_exists THEN
RETURN jsonb_build_object(
'success', false,
'error', 'INVALID_PROFILE',
'message', 'Profile not found or email mismatch'
);
END IF;
-- Generate cryptographically secure token
new_token := encode(gen_random_bytes(32), 'base64');
new_token := replace(replace(replace(new_token, '/', '-'), '+', '_'), '=', '');
-- Clean up any existing tokens for this profile
DELETE FROM email_verification_tokens
WHERE profile_id = profile_id_input;
-- Insert new token
INSERT INTO email_verification_tokens (
profile_id,
token,
email,
expires_at,
created_by_ip
) VALUES (
profile_id_input,
new_token,
email_input,
NOW() + INTERVAL '1 hour', -- 1 hour expiration
ip_addr
) RETURNING id INTO token_id;
RETURN jsonb_build_object(
'success', true,
'token', new_token,
'token_id', token_id,
'expires_at', NOW() + INTERVAL '1 hour'
);
END;
$$;
-- Step 5: Add indexes for performance
CREATE INDEX idx_email_verification_tokens_token ON email_verification_tokens(token);
CREATE INDEX idx_email_verification_tokens_profile_id ON email_verification_tokens(profile_id);
CREATE INDEX idx_email_verification_tokens_expires_at ON email_verification_tokens(expires_at);
-- Step 6: Add RLS for verification tokens table
ALTER TABLE "public"."email_verification_tokens" ENABLE ROW LEVEL SECURITY;
-- Only allow service role to manage verification tokens
CREATE POLICY "Service role can manage verification tokens" ON "public"."email_verification_tokens"
FOR ALL
TO service_role
USING (true)
WITH CHECK (true);
-- Step 7: Clean up expired tokens periodically (via cron job or manual cleanup)
CREATE OR REPLACE FUNCTION "public"."cleanup_expired_verification_tokens"()
RETURNS integer
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
DECLARE
deleted_count integer;
BEGIN
DELETE FROM email_verification_tokens
WHERE expires_at < NOW() - INTERVAL '24 hours';
GET DIAGNOSTICS deleted_count = ROW_COUNT;
RETURN deleted_count;
END;
$$;
-- Step 8: Grant necessary permissions
GRANT EXECUTE ON FUNCTION "public"."verify_email_with_token"(text, text) TO authenticated, anon;
GRANT EXECUTE ON FUNCTION "public"."create_email_verification_token"(uuid, text, text) TO authenticated, service_role;
GRANT EXECUTE ON FUNCTION "public"."cleanup_expired_verification_tokens"() TO service_role;
-- Security audit log
COMMENT ON TABLE "public"."email_verification_tokens" IS 'Secure email verification tokens - CVE fix for email verification bypass';
COMMENT ON FUNCTION "public"."verify_email_with_token" IS 'Securely verify email with cryptographic token validation';
COMMENT ON FUNCTION "public"."create_email_verification_token" IS 'Generate secure verification tokens with expiration';