Skip to content

Security: Taskly-ca/taskly.ca

Security

SECURITY.md

Security Policy

Taskly is a live marketplace handling real payments (Stripe) and personal data (Supabase, RLS-enforced). We take reports seriously.

Reporting a vulnerability

  • Email: tasklyapp.ai@gmail.com with subject [SECURITY]
  • Please include: the affected URL/route or file, reproduction steps, and impact. A proof-of-concept is welcome; do not access other users' data while demonstrating.
  • You'll get an acknowledgement within 72 hours.

Scope

  • taskly.ca (and regional domains) — the app in this repo, including /api/* and the community app under /community.
  • Out of scope: volumetric DoS, findings requiring a stolen device/session, social engineering, and reports from automated scanners without a demonstrated impact.

Please don't

  • Publicly disclose an issue before we've had a chance to fix it.
  • Run destructive tests against production (data deletion, spam bookings, payment abuse). Escrow money moves are real.

No bug-bounty program exists yet; good-faith reports are appreciated and credited if you want.

Admin two-factor (TOTP)

The admin console supports Supabase's native MFA (TOTP — 1Password/Authy/Google Authenticator). No schema, no extra dependency, no migration.

  • Enroll / manage: /admin/security — always available, regardless of the flag.
  • Step-up challenge: /admin-verify — deliberately outside the (admin) route group, so the admin layout's redirect can never loop.
  • Enforcement flag: NEXT_PUBLIC_ADMIN_MFA_REQUIRED (default false).

Enroll first, enforce second

Do these in order. Never flip the flag first.

  1. Deploy with NEXT_PUBLIC_ADMIN_MFA_REQUIRED unset (or false).
  2. Go to /admin/security, enroll, scan the QR (or paste the setup key), and verify a 6-digit code. The status row should read Enabled.
  3. Visit /admin-verify and confirm a code raises the session to aal2 and returns you to the console.
  4. Only then set NEXT_PUBLIC_ADMIN_MFA_REQUIRED=true and redeploy (it's a NEXT_PUBLIC_* var — it's inlined at build time, so a redeploy is required).

The enforcement decision (shouldChallengeAdmin in lib/admin/mfa.ts) fails open by design: an admin with no verified factor is never challenged, even with the flag on, and an unreadable assurance level lets the request through. Flipping the flag therefore cannot lock out an admin who hasn't enrolled.

Locked out? Recovery

If an authenticator is lost, delete the factor row with the service-role key (SQL editor in the Supabase dashboard runs as service role). Supabase stores factors in auth.mfa_factors; deleting the row drops the account back to aal1 and, per the rule above, enforcement then stops applying to that admin.

-- 1. Confirm what's enrolled for that admin.
select f.id, f.friendly_name, f.factor_type, f.status, f.created_at
from auth.mfa_factors f
join auth.users u on u.id = f.user_id
where u.email = 'admin@example.com';

-- 2. Delete it (challenges cascade). Sessions are cleared so the next sign-in is clean.
delete from auth.mfa_factors
where user_id = (select id from auth.users where email = 'admin@example.com');

delete from auth.sessions
where user_id = (select id from auth.users where email = 'admin@example.com');

Belt-and-braces: setting NEXT_PUBLIC_ADMIN_MFA_REQUIRED=false and redeploying also restores access immediately without touching the database.

Enrollment and removal are recorded in admin_audit_log as admin.mfa_enrolled / admin.mfa_unenrolled.

There aren't any published security advisories