T05 · Unauthorized Access and Privilege Escalation
Error
- Location
- src/app/api/portfolio/route.ts:17
- Finding
- Unauthenticated Portfolio Disclosure and Modification API<![CDATA[ ## Vulnerability Details **File Location**: `src/app/api/portfolio/route.ts:17-159` **Vulnerability Type**: Missing authentication and authorization **Risk Level**: High ### Vulnerable Code ```ts export async function GET() { const portfolio = getActivePortfolio(); const state = loadPortfolio(); const sellHistory = getSellHistory(); const realizedPL = sellHistory.reduce((sum, s) => sum + s.totalRealizedPL, 0); return NextResponse.json({ portfolio, portfolios: state.portfolios, activePortfolioId: state.activePortfolioId, realizedPL, sellHistory, }); } export async function POST(request: Request) { try { const body = await request.json(); if (body.action === 'createPortfolio') { const portfolio = createPortfolio(body.name); return NextResponse.json(portfolio, { status: 201 }); } if (body.action === 'setActive') { const portfolio = setActivePortfolio(body.id); if (!portfolio) { return NextResponse.json({ error: 'Portfolio not found' }, { status: 404 }); } return NextResponse.json(portfolio); } if (body.action === 'deletePortfolio') { const success = deletePortfolio(body.id); if (!success) { return NextResponse.json({ error: 'Cannot delete last portfolio' }, { status: 400 }); } return NextResponse.json({ success: true }); } // The remainder of this handler also processes sales and adds holdings // without authenticating or authorizing the caller. ``` ```ts export async function PUT(request: Request) { try { const { id, updates } = await request.json(); const holding = updateHolding(id, updates); if (!holding) { return NextResponse.json({ error: 'Holding not found' }, { status: 404 }); } return NextResponse.json(holding); } catch (error) { return NextResponse.json({ error: 'Failed to update holding' }, { status: 500 }); } } export async function DEL ...[truncated 3081 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Add authentication to every portfolio, dividend, price, and export API route. Reject unauthenticated requests with HTTP 401. 2. Add authorization checks that bind each portfolio and holding to an authenticated user. Do not rely only on possession of an object ID. 3. If the web UI is intended exclusively for local use, explicitly bind the production and development servers to loopback and document that restriction. Authentication should still be used if exposure is possible. 4. Add CSRF protection for state-changing requests. Validate `Origin` and `Host` headers and use SameSite cookies where cookie-based authentication is employed. 5. Define strict Zod schemas for each action and reject unknown properties. 6. Replace unrestricted `Partial<Holding>` updates with an allowlist of fields that users are permitted to modify. Never allow an update request to replace the holding ID. 7. Validate symbols, asset types, names, ISO dates, quantities, and prices. Require finite positive numeric values where appropriate and enforce reasonable string and collection limits. 8. Separate POST actions into dedicated endpoints or use a strict discriminated union so that missing or unknown actions cannot fall through to holding creation. 9. Add authorization-focused integration tests covering anonymous GET, POST, PUT, DELETE, and export requests. 10. Maintain backups or an append-only audit log for destructive portfolio operations. ]]>
