Replace storefront naming across source, tests, docs, config, and production scripts. Rename the legacy top-level app directory and Carplace component files, remove duplicate storefront startup scripts, and refresh the lockfile.
8.5 KiB
Security Vulnerability Fix Report
Date: 2026-05-22
Scope: API (apps/api) — automated scan + manual review
Branch: develop
Summary
A full security review of the API codebase identified 3 confirmed, high-confidence vulnerabilities. All 3 were fixed in the same session. A separate frontend scan (dashboard, admin, carplace, public-site) found no confirmed vulnerabilities above the reporting threshold.
| # | Severity | Category | Status |
|---|---|---|---|
| VF-01 | High | Credential / Data Exposure | Fixed |
| VF-02 | High | Authentication Bypass | Fixed |
| VF-03 | Medium-High | IDOR / Tenant Isolation | Fixed |
VF-01 — Payment Credential Exposure
Severity: High Confidence: 9/10 Category: Credential / Data Exposure
Description
GET /api/v1/companies/me/brand returned the full BrandSettings database row, including the live payment gateway credentials amanpaySecretKey, amanpayMerchantId, paypalEmail, and paypalMerchantId in the JSON response. The endpoint had no role guard, so any authenticated employee — including the lowest-privilege AGENT role — could call it and receive the secret key in plaintext.
Exploit Scenario
An AGENT-role employee calls GET /api/v1/companies/me/brand with a valid session token. The response body includes amanpaySecretKey. The attacker uses the merchant ID and secret key to interact directly with the AmanPay payment gateway on behalf of the company, initiating fraudulent charges or refunds outside the application.
Root Cause
presentBrand() in company.presenter.ts was a transparent passthrough (return brand). The DB query had no select clause, so Prisma returned every column. The public-facing site module correctly stripped these fields (evidenced by explicit test assertions), but the authenticated dashboard endpoint did not.
Fix
File: apps/api/src/modules/companies/company.presenter.ts
presentBrand() now destructures and drops all four credential fields before returning. The caller receives boolean presence indicators (amanpayConfigured, paypalConfigured) instead of the raw secrets.
export function presentBrand(brand: any) {
- return brand
+ if (!brand) return brand
+ const { amanpaySecretKey, amanpayMerchantId, paypalEmail, paypalMerchantId, ...safe } = brand
+ return {
+ ...safe,
+ amanpayConfigured: !!(amanpayMerchantId && amanpaySecretKey),
+ paypalConfigured: !!(paypalEmail || paypalMerchantId),
+ }
}
VF-02 — Webhook Signature Bypass
Severity: High Confidence: 8/10 Category: Authentication Bypass / Payment Fraud
Description
Both payment and subscription webhook endpoints for AmanPay and PayPal used a short-circuit AND guard for signature verification:
if (amanpay.isConfigured() && !amanpay.verifyWebhookSignature(rawBody, signature)) {
return res.status(401).json({ error: 'invalid_signature' })
}
await service.handleAmanpayWebhook(req.body) // executed unconditionally
When isConfigured() returns false (i.e., credentials are absent or set to placeholder defaults), the entire guard short-circuits and the webhook handler executes on any unauthenticated request. The endpoints had no IP allowlist and were explicitly placed before requireCompanyAuth.
This affected 4 endpoints:
POST /api/v1/payments/webhooks/amanpayPOST /api/v1/payments/webhooks/paypalPOST /api/v1/subscriptions/webhooks/amanpayPOST /api/v1/subscriptions/webhooks/paypal
Exploit Scenario
On any staging or misconfigured production environment where AmanPay credentials are not set, an attacker posts {"transaction_id": "<known_id>", "status": "PAID"} to /api/v1/payments/webhooks/amanpay. The handler finds the pending payment by transactionId, marks it succeeded, and unlocks the reservation — with no money changing hands. A valid amanpayTransactionId can leak through receipts, logs, or API responses visible to renters.
The same attack against /api/v1/subscriptions/webhooks/amanpay activates a paid subscription tier for free.
Fix
Files: apps/api/src/modules/payments/payment.routes.ts, apps/api/src/modules/subscriptions/subscription.routes.ts
Inverted the guard logic: an unconfigured provider now immediately returns 401 instead of passing through. Applied to all 4 endpoints.
-if (amanpay.isConfigured() && !amanpay.verifyWebhookSignature(rawBody, signature)) {
+if (!amanpay.isConfigured() || !amanpay.verifyWebhookSignature(rawBody, signature)) {
return res.status(401).json({ error: 'invalid_signature' })
}
-if (paypal.isConfigured() && !isValid) return res.status(401).json({ error: 'invalid_signature' })
+if (!paypal.isConfigured() || !isValid) return res.status(401).json({ error: 'invalid_signature' })
VF-03 — IDOR on Vehicle Maintenance Logs
Severity: Medium-High Confidence: 9/10 Category: Insecure Direct Object Reference (IDOR) / Tenant Isolation Bypass
Description
GET /api/v1/vehicles/:id/maintenance was protected by requireCompanyAuth and requireTenant, making req.companyId available. However, req.companyId was never forwarded to the service or repository. The database query used only vehicleId with no tenant filter:
// vehicle.repo.ts
prisma.maintenanceLog.findMany({ where: { vehicleId } })
An authenticated employee from Company A could fetch the full maintenance history of any vehicle belonging to any other company by knowing its UUID. The write path (POST /:id/maintenance) already performed the correct ownership check using repo.findById(id, companyId) — the read path did not.
Exploit Scenario
An employee of Company A calls GET /api/v1/vehicles/<vehicleId_from_company_B>/maintenance. Vehicle UUIDs can leak through carplace listings, shared bookings, or support interactions. The response contains the complete maintenance history of Company B's vehicle — including service records, mileage data, and operational schedules — across tenant boundaries.
Fix
Files: apps/api/src/modules/vehicles/vehicle.routes.ts, apps/api/src/modules/vehicles/vehicle.service.ts
req.companyId is now threaded through to getMaintenanceLogs, which performs the same ownership pre-check already used by the write path. A cross-tenant request returns 404.
// vehicle.routes.ts
-const logs = await service.getMaintenanceLogs(id)
+const logs = await service.getMaintenanceLogs(id, req.companyId)
// vehicle.service.ts
-export async function getMaintenanceLogs(id: string) {
- return repo.findMaintenanceLogs(id)
+export async function getMaintenanceLogs(id: string, companyId: string) {
+ const vehicle = await repo.findById(id, companyId)
+ if (!vehicle) throw new NotFoundError('Vehicle not found')
+ return repo.findMaintenanceLogs(id)
}
Frontend Scan Results
A full scan of all four frontend applications (dashboard, admin, carplace, public-site) was performed. No vulnerabilities above the reporting threshold (confidence ≥ 8/10) were confirmed.
Hardening Recommendations (not vulnerabilities)
Two items were identified as best-practice improvements:
-
Admin auth-redirect
nextparam (apps/admin/src/app/auth-redirect/page.tsx) — Validate thenextquery parameter is a relative path (starts with/, does not contain://) before passing it torouter.replace. The router itself does not perform cross-origin navigation, but defense-in-depth is recommended. -
postMessagewildcard origin + missingframe-ancestorsCSP (apps/dashboard/src/app/sign-in/[[...sign-in]]/SignInPageClient.tsx,apps/dashboard/next.config.js) — ReplacetargetOrigin: '*'with the carplace origin, and addContent-Security-Policy: frame-ancestors 'self' <carplace-origin>to prevent the sign-in page from being embeddable by arbitrary third-party domains.
Files Changed
| File | Change |
|---|---|
apps/api/src/modules/companies/company.presenter.ts |
Strip payment credentials in presentBrand() |
apps/api/src/modules/payments/payment.routes.ts |
Fix webhook signature bypass (AmanPay + PayPal) |
apps/api/src/modules/subscriptions/subscription.routes.ts |
Fix webhook signature bypass (AmanPay + PayPal) |
apps/api/src/modules/vehicles/vehicle.routes.ts |
Pass companyId to maintenance log service |
apps/api/src/modules/vehicles/vehicle.service.ts |
Add ownership check in getMaintenanceLogs |