ASAASA Standard
Not in Phase 1Production Foundation

Data Export Controls

Admin Safety · ADM-20 · Priority: P1

Why It Matters

Admin panels in AI-built apps often include "Export" buttons that dump entire database tables into CSV or JSON — with no size limits, no access logging, and no approval workflow. A compromised admin account — or a disgruntled employee — can export your entire user database in one click.

Data export without controls violates GDPR's data minimization principle, SOC 2 access control requirements, and basic security hygiene. If you ever need to demonstrate to a customer or auditor who exported what data and when, you need export controls.

Priority: P1 — Bulk data exfiltration risk. Compliance requirement for enterprise customers.

Affected Stack: Any framework with admin functionality


The Problem

// ❌ Unrestricted bulk export
export async function GET(req: Request) {
  const allUsers = await supabase
    .from('users')
    .select('*'); // All fields, all rows, no limit

  return new Response(JSON.stringify(allUsers.data), {
    headers: { 'Content-Type': 'application/json' },
  });
}

This endpoint returns every user record with every field — emails, billing info, metadata — to anyone with admin access, with no record that the export happened.


The Fix

1. Limit export scope

// ✅ Controlled export with limits and field filtering
export async function POST(req: Request) {
  const admin = await getVerifiedAdmin(req);
  if (!admin) return new Response('Forbidden', { status: 403 });

  const { fields, limit = 100, offset = 0 } = await req.json();

  // Allowlist of exportable fields
  const ALLOWED_FIELDS = ['id', 'email', 'created_at', 'plan', 'status'];
  const safeFields = fields.filter((f: string) => ALLOWED_FIELDS.includes(f));

  const { data } = await supabase
    .from('users')
    .select(safeFields.join(','))
    .range(offset, offset + Math.min(limit, 1000)); // Max 1000 per request

  return Response.json(data);
}

2. Log every export

// ✅ Audit log for exports
await supabaseAdmin.from('admin_audit_log').insert({
  admin_id: admin.id,
  action: 'data.export',
  target_type: 'users',
  details: { fields: safeFields, count: data?.length, offset },
  ip_address: req.headers.get('x-forwarded-for'),
});

3. Require MFA for bulk exports

For exports exceeding a threshold (e.g., >100 rows or sensitive fields), require MFA re-verification before allowing the export.

Key controls:

  • Field-level allowlist (never export password hashes, tokens, secrets)
  • Row limits per request (max 1000, paginated)
  • Audit logging for every export (who, what, when, how many)
  • MFA challenge for bulk or sensitive exports
  • Rate limiting on export endpoints

References


Related Checks


Is your app safe? Run Free Scan →