GDPR Article 20 (Right to Data Portability) and CCPA Section 1798.110 both require that users can receive their personal data, including transaction history, in a machine-readable format. CFPB Rule 1033 (Personal Financial Data Rights) additionally requires that financial institutions make consumer financial data available for download upon request. The failure to provide user-facing transaction export is simultaneously a privacy compliance gap, a regulatory compliance gap, and a usability failure — users who want to reconcile their own records or switch financial providers cannot do so without this feature. An export scoped only to admin or compliance roles does not satisfy these requirements; the user must be able to retrieve their own data directly.
Low because the absence of user export does not expose data to unauthorized parties, but it creates regulatory non-compliance with GDPR Article 20 and CFPB 1033 and prevents legitimate user data access.
Add a user-scoped export endpoint at src/app/api/user/transactions/export/route.ts that is strictly limited to the authenticated user's own records:
export async function GET(req: Request) {
const session = await getServerSession();
if (!session) return new Response('Unauthorized', { status: 401 });
const { startDate, endDate, format = 'csv' } =
Object.fromEntries(new URL(req.url).searchParams);
const rows = await db('transaction_logs')
.where({ user_id: session.user.id }) // never accept userId from query params
.whereBetween('timestamp', [startDate, endDate])
.orderBy('timestamp');
if (format === 'csv') {
const csv = [Object.keys(rows[0]).join(','),
...rows.map(r => Object.values(r).join(','))].join('\n');
return new Response(csv, {
headers: {
'Content-Type': 'text/csv',
'Content-Disposition': 'attachment; filename=transactions.csv'
}
});
}
return Response.json(rows);
}
Use the session to derive user_id server-side — never read it from request parameters, which can be tampered with to access other users' records.
ID: finserv-audit-trail.tamper-evidence.user-export
Severity: low
What to look for: Count all user-facing export endpoints or UI buttons for transaction history. Enumerate supported formats (CSV, PDF, JSON) and filter options (date range, amount range, status). Quote the actual endpoint paths found. Verify exports are scoped to the authenticated user's own transactions only.
Pass criteria: At least 1 transaction history export feature is available to end users. Exports support at least 1 format (CSV or PDF) and at least 1 filter (date range). Report the count even on pass (e.g., "1 export endpoint at /api/user/transactions/export with CSV + PDF formats, date range filter").
Fail criteria: No user export feature (0 endpoints), or only admin/compliance can export (not end users), or exports lack date filtering.
Skip (N/A) when: The application is B2B (does not serve individual users) or is internal-only — cite the actual project type detected.
Detail on fail: "No transaction export feature — 0 user-facing export endpoints found. Users cannot download their history.".
Remediation: Add user export endpoint (in src/app/api/user/transactions/export/route.ts):
app.get('/api/user/transactions/export', authenticate, async (req, res) => {
const { startDate, endDate, format } = req.query; // format: 'csv' or 'pdf'
const userId = req.user.id;
const transactions = await db.transactionLogs
.find({
userId,
timestamp: { $gte: new Date(startDate), $lte: new Date(endDate) }
})
.lean();
if (format === 'csv') {
const csv = convertToCSV(transactions);
res.setHeader('Content-Type', 'text/csv');
res.setHeader('Content-Disposition', 'attachment; filename=transactions.csv');
res.send(csv);
} else if (format === 'pdf') {
const pdf = await generatePDF(transactions);
res.setHeader('Content-Type', 'application/pdf');
res.setHeader('Content-Disposition', 'attachment; filename=transactions.pdf');
res.send(pdf);
}
});