Authenticated users can download all personal data in portable format
Why it matters
GDPR Art. 20 gives data subjects the right to receive their personal data in a structured, commonly used, machine-readable format and to transmit it to another controller. CCPA §1798.110 and LGPD Art. 18 impose equivalent portability rights. Without a working data export endpoint, you cannot fulfill these rights — which means any user who files a data portability request triggers a regulatory obligation you cannot meet programmatically. The practical consequence is manual database queries under time pressure, which introduces error risk and compliance delay. This is also a competitive signal: users who can export their data are less anxious about vendor lock-in.
Severity rationale
Medium because failure to provide portability on request triggers a mandatory regulatory response timeline (one month under GDPR Art. 12) and constitutes a standalone violation of Art. 20.
Remediation
Create an authenticated export endpoint at app/api/user/export/route.ts that gathers all user data and returns it as a downloadable JSON file.
// app/api/user/export/route.ts
import { getServerSession } from 'next-auth'
import { db } from '@/lib/db'
export async function GET() {
const session = await getServerSession()
if (!session?.user?.id) return new Response('Unauthorized', { status: 401 })
const userId = session.user.id
const [profile, activity, purchases] = await Promise.all([
db.user.findUnique({ where: { id: userId },
select: { id: true, email: true, name: true, createdAt: true } }),
db.activityLog.findMany({ where: { userId }, orderBy: { createdAt: 'desc' } }),
db.order.findMany({ where: { userId }, include: { items: true } }),
])
return new Response(JSON.stringify({ exportedAt: new Date().toISOString(), profile, activity, purchases }, null, 2), {
headers: {
'Content-Type': 'application/json',
'Content-Disposition': `attachment; filename="my-data-${userId}.json"`,
},
})
}
Add a "Download my data" button to the user settings page. Rate-limit the endpoint to prevent abuse (e.g., one export per 24 hours per user).
Detection
-
ID:
data-export -
Severity:
medium -
What to look for: Enumerate every relevant item. Look for a user settings or profile page with a "Download My Data," "Export Data," or "Data Portability" option. Check API routes for a data export endpoint (common paths:
/api/user/export,/api/account/download,/api/me/data). Inspect what the export includes: does it cover all data types (profile information, activity history, uploaded files, messages, purchase history)? Is the format machine-readable (JSON or CSV)? Does the export require authentication to access? Is there a rate limit on export requests to prevent abuse? -
Pass criteria: At least 1 of the following conditions is met. Authenticated users can trigger a download of all their personal data from a settings or profile page. The export is in a standard, machine-readable format (JSON or CSV). The export covers all major data categories held about the user. The endpoint requires authentication and has reasonable rate limiting.
-
Fail criteria: No data export feature exists. Export exists but is incomplete (e.g., only profile info, missing activity history or purchases). Export is in a proprietary or non-machine-readable format.
-
Skip (N/A) when: The application collects no personal data beyond what is necessary to authenticate (e.g., email and hashed password with no other data stored per user).
-
Detail on fail: Example:
"No data export feature found. No /api/user/export route or equivalent exists. Users cannot exercise their right to data portability."or"Export endpoint exists but only returns profile fields; purchase history and activity logs are excluded.". -
Remediation: Create a data export API endpoint and wire it to a settings page:
// app/api/user/export/route.ts import { getServerSession } from 'next-auth' import { db } from '@/lib/db' export async function GET() { const session = await getServerSession() if (!session?.user?.id) return new Response('Unauthorized', { status: 401 }) const userId = session.user.id // Gather all user data const [profile, activity, purchases, preferences] = await Promise.all([ db.user.findUnique({ where: { id: userId }, select: { id: true, email: true, name: true, createdAt: true } }), db.activityLog.findMany({ where: { userId }, orderBy: { createdAt: 'desc' } }), db.order.findMany({ where: { userId }, include: { items: true } }), db.userPreferences.findUnique({ where: { userId } }), ]) const exportData = { exportedAt: new Date().toISOString(), formatVersion: '1.0', profile, activityHistory: activity, purchases, preferences, } return new Response(JSON.stringify(exportData, null, 2), { headers: { 'Content-Type': 'application/json', 'Content-Disposition': `attachment; filename="my-data-${userId}.json"`, }, }) }Add a "Download my data" button to the user settings page that links to this endpoint.
External references
- gdpr · Art. 20 — Right to data portability
- gdpr · Art. 15 — Right of access by the data subject
- ccpa · §1798.110 — Consumer right to know specific pieces of personal information
- lgpd · Art. 18 — Rights of the data subject including portability
- gdpr · Art. 12 — Transparent information, communication and modalities
Taxons
History
- 2026-04-18·v1.0.0·Initial import from data-protection·automated