Skip to content

Security: Movalabs-crew/mova-store

Security

SECURITY.md

Security Policy

Supported Versions

Version Supported
1.x.x ✅

Reporting a Vulnerability

We take security seriously at Mova Store. If you discover a security vulnerability, please report it responsibly.

How to Report

  1. Do NOT open a public GitHub issue for security vulnerabilities
  2. Email security concerns to: security@movalabs.dev
  3. Include a detailed description of the vulnerability
  4. Provide steps to reproduce if possible

What to Expect

  • Acknowledgment within 48 hours
  • Regular updates on our progress
  • Credit in the security advisory (if desired)

Security Best Practices

Environment Variables

All sensitive configuration is stored in environment variables. Never commit secrets to the repository.

Required environment variables are documented in .env.local.example. Copy this file to .env.local and fill in your values.

cp .env.local.example .env.local

Sensitive Data Handling

Data Type Storage Notes
API Keys Environment variables Never hardcode
Supabase Config Environment variables Client-side anon key is safe; never commit service role key
EmailJS Credentials Environment variables Required for email functionality
Stellar Contract IDs Environment variables Public but environment-specific
User Passwords Supabase Auth Handled by Supabase, never stored locally
Payment Data On-chain (Stellar) Non-custodial, no card data stored

Smart Contract Security

The Soroban escrow contract follows security best practices:

  1. Non-custodial: Funds flow directly from buyer to contract to merchant
  2. Access Control: Only authorized parties can dispatch/refund orders
  3. Token Whitelist: Only approved tokens can be used for payment
  4. Event Emission: All state changes emit events for transparency
  5. Error Handling: Typed errors prevent unexpected failures

Frontend Security

  1. Input Validation: All user inputs are validated and sanitized
  2. XSS Prevention: HTML entities are escaped before rendering
  3. HTTPS Only: All API calls use HTTPS
  4. No Sensitive Data in URLs: Sensitive data is sent via POST body
  5. Content Security Policy: Configured in Next.js headers

Authentication & Access Control

  • Supabase Authentication handles user sessions.
  • Client-side admin UI access is controlled solely by the is_admin JWT claim in app_metadata. There is no client-side email allow-list: NEXT_PUBLIC_* values are compiled into the browser bundle, so an allow-list published that way both leaks the admin addresses and lets anyone edit their local bundle to pass the check. The UI gate is convenience only; the real boundary is the server.
  • Server-side database and storage writes are enforced via PostgreSQL Row Level Security (RLS) policies in supabase/schema.sql.
  • No passwords or private secrets are stored in the application repository.

Authorizing Administrators in Supabase

To authorize an admin user for product mutations and storage uploads in Supabase, use one of the following methods:

  1. Method 1: public.admin_users table (Recommended) Insert the admin's email address into the admin_users table:

    insert into public.admin_users (email)
    values ('admin@example.com')
    on conflict (email) do nothing;
  2. Method 2: Custom JWT App Metadata (is_admin: true) Assign the is_admin: true flag to the user's app_metadata in Supabase Auth:

    update auth.users
    set raw_app_meta_data = coalesce(raw_app_meta_data, '{}'::jsonb) || '{"is_admin": true}'::jsonb
    where email = 'admin@example.com';

    Or via the Supabase Admin API / Dashboard:

    await supabase.auth.admin.updateUserById(userId, {
      app_metadata: { is_admin: true },
    });

    For server routes that must decide on an address rather than a claim, use the server-only ADMIN_EMAILS variable. It is read exclusively in server code, is never prefixed with NEXT_PUBLIC_, and must never be exposed to the browser.

Dependencies

  • Dependencies are regularly updated for security patches
  • Use npm audit to check for vulnerabilities
  • CI pipeline fails on high-severity vulnerabilities

Security Checklist for Contributors

Before submitting a PR, ensure:

  • No secrets or API keys are committed
  • User inputs are validated and sanitized
  • No eval() or dangerouslySetInnerHTML usage
  • Error messages don't expose sensitive information
  • Dependencies are from trusted sources
  • No console.log with sensitive data

Known Security Considerations

Checkout OTP

Checkout OTPs are generated, stored and verified server-side by app/api/otp/route.ts using lib/otp.ts. The browser only asks for a code to be sent and asks for a submitted code to be checked:

  • Codes are 6 digits drawn from a CSPRNG (crypto.randomInt), never Math.random().
  • Only a SHA-256 hash of the code is kept; the plaintext string is returned once so the server can email it, and never reaches the client bundle.
  • Codes expire after 5 minutes and are single-use.
  • A wrong guess consumes one of 5 attempts, after which the code is discarded.
  • Issue requests are throttled to 3 per minute per email address.

EmailJS is called from the server route using the EMAILJS_* (server-only) names, falling back to the legacy NEXT_PUBLIC_EMAILJS_* names. Prefer the server-only names so the credentials stay out of the client bundle.

The OTP store is in-process; a deployment with more than one serverless instance should back OtpStore with a shared store (e.g. Redis/Upstash).

Card Payment Fields (threat model)

The card number, expiry and CVV fields on /checkout are a UI-only demo affordance. No card data may be transmitted, persisted or processed by this application:

  • Card values live only in React state for the lifetime of the tab. They are never sent to an API, logged, written to localStorage, placed in a URL, or handed to a third party.
  • Because no card value leaves the browser, Mova Store stays outside PCI DSS scope for cardholder data.
  • The UI labels the fields as demo-only so a shopper cannot mistake them for a real payment form; the sanctioned path is the Stellar (Soroban) checkout.

Do not "finish" this form in place. If real card payments are ever required, integrate a PCI-compliant processor (Stripe, etc.) and let it collect card data in its own iframe/tokenization flow — never handle raw card data on our servers.

Supabase Row Level Security

Apply supabase/schema.sql. The shipped policies are already the hardened, admin-only model — there is no outstanding "tighten later" step:

  • Products — public.products and the products storage bucket are publicly readable; insert, update and delete on both require public.is_admin() ("Admins can insert/update/delete products", "Admins can upload/update/delete product images").
  • Orders — buyers may read only their own rows (auth.uid() = user_id or auth.email() = user_email); status changes and deletions are admin-only.
  • Allowlist — public.admin_users, which backs public.is_admin(), is readable by admins only.

public.is_admin() (defined next to the admin_users table in supabase/schema.sql) is the single source of truth for admin access: it returns true when the caller's JWT carries app_metadata.is_admin = true, or when the caller's email is in public.admin_users. Grant admin rights through one of those two mechanisms; never widen a policy to work around a missing admin entry.

Never expose the Supabase service role key in the browser.

Stellar/Soroban Security

Testnet vs Mainnet

  • Testnet: Used for development, tokens have no real value
  • Mainnet: Real value transactions, requires additional auditing

Before mainnet deployment:

  1. Smart contract security audit
  2. Penetration testing
  3. Rate limiting implementation
  4. Monitoring and alerting setup

Wallet Security

  • Users connect their own Freighter wallet
  • Private keys never leave the user's browser
  • Transactions require explicit user approval

Contact

For security-related inquiries:

There aren't any published security advisories