--- title: Security, DB Performance, and Logic Audit - 2026-06-07 tags: [audit, security, database, performance, logic, findings] created: 2026-06-07 updated: 2026-06-07 status: closed --- # Security, DB Performance, and Logic Audit - 2026-06-07 Fresh post-remediation audit track after the DB Query & Schema Audit was closed. This pass targets security, authorization, sensitive data handling, DB/performance regressions outside the closed report, and business-logic/state-machine correctness. > [!info] Scope > Initial source pass covered mounted backend routes, auth/ownership middleware, uploads/static file serving, dispute controllers/services, delivery-code flows, frontend token handling, and template checkout batch paths. This document is the starting register; expand it as each wave verifies more surfaces. ## Method - Review only code that is mounted or imported by mounted routes, unless the finding explicitly calls out legacy/dead code. - Separate confirmed findings from follow-up scan queue items. - Use the same severity posture as the DB audit: - Critical: cross-tenant privilege bypass, funds/control-plane compromise, or exploitable unauthenticated secret exposure. - High: authenticated cross-tenant mutation/read, sensitive token/code leakage, unbounded user-triggered load, or logic that can corrupt user-visible state. - Medium: broken route behavior, false-deny authorization, latent risk, degraded performance, or cleanup needed before the next refactor. - Low: hardening and maintainability issues with limited user impact. ## Remediation Summary | Severity | Open | Fixed | |---|---:|---:| | Critical | 0 | 0 | | High | 0 | 6 | | Medium | 0 | 4 | | Low | 0 | 0 | | Total | 0 | 10 | All initial findings were remediated in backend `c0e80a7` and frontend `38ff0db`. | Finding | Status | Fixed in | Notes | |---|---|---|---| | H1 | Fixed | backend `c0e80a7` | Dashboard dispute creation now checks canonical purchase-request ownership before creating dispute/chat state. | | H2 | Fixed | backend `c0e80a7` | Delivery-code generation and verification logs no longer print code values or expected values. | | H3 | Fixed | backend `c0e80a7`, frontend `38ff0db` | Google OAuth/bearer logs no longer print token material; frontend logger redacts sensitive object keys. | | H4 | Fixed | backend `c0e80a7` | File delete/info routes use upload-relative paths resolved under the upload root. | | H5 | Fixed | backend `c0e80a7` | Template batch endpoints cap array sizes and reject duplicate payment-completion ids. | | H6 | Fixed | backend `c0e80a7` | Broad legacy user list is admin-only; contacts/search no longer expose email/phone. | | M1 | Fixed | backend `c0e80a7` | Purchase-request dispute-hold routes use canonical `sameUser` checks. | | M2 | Fixed | backend `c0e80a7` | Dashboard dispute participant checks use canonical identity comparison. | | M3 | Fixed | backend `c0e80a7` | Temp/document uploads are blocked from public static serving. | | M4 | Fixed | backend `c0e80a7` | Audited list endpoints use the shared `parsePagination` helper. | ## High Findings ### H1 - Dashboard dispute creation does not verify purchase-request ownership **Files:** `backend/src/routes/disputeRoutes.ts:10-11`, `backend/src/controllers/disputeController.ts:9-31`, `backend/src/services/dispute/DisputeService.ts:96-170` **Status:** Fixed in backend `c0e80a7` `POST /api/disputes` is mounted under `/api/disputes` and only requires authentication. The controller correctly ignores the client-submitted `buyerId` and passes `req.user.id`, but `DisputeService.createDispute` only verifies that `purchaseRequestId` exists. It does not verify that the authenticated user is the purchase request buyer, seller, admin, or resolver before creating a dispute and dispute chat tied to that purchase request. An authenticated user can therefore create a dashboard dispute for another user's purchase request if they know or can infer the request id. The new dispute stores the attacker as `buyerId` and can pull the real selected/preferred seller into a chat. **Impact:** Cross-tenant state mutation, support queue pollution, seller notification/chat creation for unrelated requests, and possible confusion in admin dispute handling. **Fix:** Before creating the chat or dispute, load the purchase request context and require one of: - requester is the request buyer; - requester is the selected/preferred seller, if seller-initiated disputes are intentionally allowed; - requester is `admin` or `resolver`. Add a regression test where user B attempts to dispute user A's purchase request and receives 403. ### H2 - Delivery codes are printed in backend logs **Files:** `backend/src/services/marketplace/marketplaceController.ts:2022-2025`, `backend/src/services/delivery/DeliveryService.ts:133-147`, legacy `backend/src/services/marketplace/routes.ts:2941-2947` **Status:** Fixed in backend `c0e80a7` Delivery codes are sensitive one-time confirmation values. The mounted controller logs the full generated code: ```ts console.log(`Delivery code generated for request ${id}: ${deliveryCode}`); ``` `DeliveryService.verifyDeliveryCode` also logs failed attempts with both the submitted value and the expected code. **Impact:** Anyone with backend log access can recover or validate a delivery code and influence delivery confirmation. The failed-attempt log is especially risky because it turns an invalid guess into the correct code in logs. **Fix:** Never log the code or expected value. Log only request id, actor id, success/failure reason, and an event id. If auditability is needed, store a salted hash or redacted code suffix only. ### H3 - Google OAuth and bearer-token debug logs expose access-token material in development logs **Files:** `frontend/src/auth/services/google-oauth.ts:117-121`, `frontend/src/auth/services/google-oauth.ts:185-193`, `frontend/src/lib/axios.ts:69-76`, `frontend/src/utils/logger.ts:29-34` **Status:** Fixed in backend `c0e80a7`, frontend `38ff0db` The frontend logger is development-enabled by default. Google signup logs the full OAuth `tokenResponse` and then the full `access_token`. Google signin logs token metadata and a token prefix. The axios request interceptor logs a bearer-token preview and localStorage key names. **Impact:** Dev/local screenshots, browser consoles, test logs, or shared debugging sessions can leak usable OAuth or API bearer material. Production `info` logs are disabled, so this is not currently a production console leak, but it violates the "never log tokens" rule and is easy to re-enable accidentally. **Fix:** Remove token value logging entirely. Keep boolean/length-only diagnostics if required, and add a small frontend logger redaction helper for keys containing `token`, `authorization`, `secret`, `password`, or `code`. ### H4 - Generic file delete/info routes are broken and the service path handling is unsafe for a future repair **Files:** `backend/src/services/file/fileRoutes.ts:71-80`, `backend/src/services/file/fileController.ts:247-280`, `backend/src/services/file/fileService.ts:225-268` **Status:** Fixed in backend `c0e80a7` `DELETE /api/files/delete` never supplies `req.params.filename`, while the controller requires it. `GET /api/files/info/:filePath` provides `filePath`, but the controller also reads `filename`. These endpoints currently fail validation rather than doing useful work. The latent security issue is in the service: `deleteFile(filePath)` and `getFileInfo(filePath)` accept arbitrary raw filesystem paths when the input does not start with `/uploads`. A future route-param fix could accidentally expose arbitrary file stat/delete behavior to any authenticated caller. **Impact:** Broken file-management functionality now; high-risk latent arbitrary file access/delete if the route is repaired by simply passing request params through. **Fix:** Replace path-based delete/info with file identifiers or strict allow-listed upload-relative paths. Resolve with `path.resolve`, require the final path to stay under the configured upload directory, reject `..`, reject absolute paths, and add route tests for traversal inputs. ### H5 - Template batch endpoints accept unbounded arrays and perform per-id/per-item work **Files:** `backend/src/services/marketplace/requestTemplateRoutes.ts:267-325`, `backend/src/services/marketplace/RequestTemplateService.ts:472-663` **Status:** Fixed in backend `c0e80a7` `batch-convert` validates that `items` is an array with `min: 1`, but no maximum. Each item can request `quantity` up to 100 and the service loops items sequentially, creates requests/offers, increments usage, re-fetches populated purchase requests, and schedules notifications. `complete-payment` validates `requestIds` with `min: 1`, but no maximum, then runs: ```ts await Promise.all(requestIds.map((id) => this.marketplaceRepo.findPurchaseRequestById(id))) ``` before the bulk status update. **Impact:** An authenticated caller can submit large arrays within the request body limit and fan out many DB reads/writes or a large concurrent read burst. This is a DB/performance and abuse-control issue. **Fix:** Add explicit max lengths for `items` and `requestIds` (for example 50 or the intended cart maximum), reject duplicate request ids, and move the ownership check to a single repository method that fetches all requested rows by id. ### H6 - User-facing legacy user list exposes broad user data to any authenticated account **Files:** `backend/src/app.ts:591-592`, `backend/src/services/user/userRoutes.ts:830-879`, `backend/src/services/user/userRoutes.ts:988-1006` **Status:** Fixed in backend `c0e80a7` The legacy `/api/users` route is still mounted. `GET /api/users` requires authentication but no role restriction, allows `limit` up to 1000, and returns email, role, status, profile fields, and last login metadata for matching active users. The search endpoint also returns email and profile contact metadata to any authenticated caller with a two-character search term. **Impact:** Broad authenticated user enumeration and privacy exposure. This also creates avoidable DB load because the route still uses Mongoose `User.find`/`countDocuments` rather than the newer repository path. **Fix:** Decide whether non-admin user discovery is still a product requirement. If not, make `/api/users` admin-only. If yes, narrow returned fields, cap limits much lower, remove email unless explicitly needed, and move it behind a purpose-specific contacts/search endpoint. ## Medium Findings ### M1 - Purchase-request dispute-hold route uses raw ID string comparisons **Files:** `backend/src/services/dispute/disputeRoutes.ts:44-50`, `backend/src/services/dispute/disputeRoutes.ts:146-153` **Status:** Fixed in backend `c0e80a7` The `/api/disputes/pr/:purchaseRequestId/...` route compares `request.buyerId.toString()` and preferred seller ids directly against `req.user.id`. In this codebase, many repositories now resolve UUID and legacy ObjectId forms. Raw comparison can false-deny legitimate users when one side is UUID and the other is legacy id. **Impact:** Compatibility/logic failure, especially during mixed legacy/UUID flows. This looks more like false-deny than privilege bypass. **Fix:** Reuse the existing `sameUser`/canonical-id helper or expose canonical participant checks from the repository. ### M2 - Dashboard dispute participant checks assume populated `_id` shapes **Files:** `backend/src/controllers/disputeController.ts:125-130`, `backend/src/controllers/disputeController.ts:253-258`, `backend/src/db/repositories/drizzle/DrizzleDisputeRepo.ts:271-278` **Status:** Fixed in backend `c0e80a7` The controller checks `dispute.buyerId._id.toString()` and `dispute.sellerId?._id.toString()`. Drizzle dispute records currently map these references into `{ _id: displayId }`, so this works today, but the controller is tightly coupled to a Mongoose-like shape rather than using canonical participant helpers. **Impact:** Future repository shape changes can break dispute reads/evidence with 500s or false denials. **Fix:** Add a small `isDisputeParticipant(dispute, user)` helper using `toIdString` plus canonical user comparison, and use it for details/evidence access. ### M3 - Public `/uploads` serving exposes all non-chat uploaded files cross-origin **Files:** `backend/src/app.ts:527-551`, `backend/src/services/file/fileRoutes.ts:21-69`, `backend/src/services/file/fileService.ts:75-109` **Status:** Fixed in backend `c0e80a7` Chat attachments are explicitly blocked from static serving, but all other uploads are served through `/uploads` with `Cross-Origin-Resource-Policy: cross-origin`. Generic upload accepts images, PDFs, and Word documents by MIME type and returns a public URL. **Impact:** This may be intended for avatars, blog images, and product/request-template images, but generic temp/documents uploads become public once uploaded. There is no ownership check, no signed URL, no magic-byte validation for generic documents, and no malware/content scanning. **Fix:** Split public media uploads from private documents. Keep only intentional public media under static `/uploads`; serve private documents through authenticated file routes with ownership checks. Validate file signatures, not only MIME. ### M4 - Several list endpoints parse user-supplied limits without consistent normalization **Files:** `backend/src/controllers/disputeController.ts:70-71`, `backend/src/controllers/pointsController.ts:46-78`, `backend/src/services/payment/paymentRoutes.ts:171-174`, `backend/src/services/chat/chatController.ts:301-355` **Status:** Fixed in backend `c0e80a7` The DB audit fixed many concrete query caps, but the current codebase still has route handlers that call `parseInt(limit)` and pass the result onward without a shared normalizer. Some downstream repos cap values, some do not, and malformed/negative values can produce inconsistent behavior. **Impact:** Performance variability and route-specific edge cases. This is lower priority than H5 because it requires endpoint-specific downstream confirmation. **Fix:** Add a shared `parsePagination` helper with min/max/defaults and replace local ad hoc parsing. ## Follow-up Scan Queue 1. Security wave: fix/verify H1-H4 first, then re-scan authz on mounted marketplace/payment routes. 2. DB/performance wave: quantify H5 with expected max cart size; inspect remaining route-level `limit` parsing and user-route Mongoose paths. 3. Logic wave: review delivery-code state transitions, dispute creation/resolution side effects, and payment status mutation routes. 4. Privacy wave: decide intended visibility for `/api/users`, generic uploaded documents, and dispute evidence URLs. 5. Regression tests: add at least one failing test per high finding before code fixes where practical. ## Already Checked in This Pass | Area | Result | |---|---| | Payment callback auth | `paymentCallbackAuth` fails closed when secret is unset and uses timing-safe comparison. | | Chat attachment serving | Static `/uploads/chat` is blocked; authenticated chat attachment route checks membership and path traversal. | | Dashboard dispute body `buyerId` | Controller ignores body `buyerId` and uses `req.user.id`; the missing check is purchase-request ownership, not body trust. | | Legacy marketplace route module | `backend/src/services/marketplace/routes.ts` still contains old route code, but `app.ts` mounts `marketplaceControllerRouter`, not the legacy router. | | Taskmaster | `task-master next` reported no available tasks. |