Files
nick-doc/09 - Audits/Platform Logical Audit - 2026-05-24.md
Siavash Sameni 5b93b2d23e docs: add comprehensive logical audit report
Adds a full cross-document audit covering:
- Data Models (broken refs, ghost states, missing constraints)
- API Reference (unauthenticated endpoints, field mismatches, missing pagination)
- Architecture (fictitious deps, statelessness claims vs reality)
- Flows (race conditions, missing failure paths, auth bypasses)
- Security (passkey stubs, JWT storage, webhook verification)

32 findings organized by severity with recommended fixes.
2026-05-24 08:03:20 +04:00

20 KiB
Raw Blame History

Platform Logical Audit

Date: 2026-05-24
Scope: Data Models, API Reference, Architecture, Flows, Security
Method: Cross-document consistency check, state-machine validation, authorization gap analysis, dependency verification


Executive Summary

This audit identifies critical logical contradictions, security holes, and cross-document inconsistencies across the Amn platform documentation. The most severe findings are:

  1. Dispute/escrow race condition allowing fund release while a dispute is active.
  2. Three mutually incompatible Dispute status/action enums across the Data Model, API Reference, and Flow documents — the documented API cannot be implemented against the documented schema.
  3. Passkey (WebAuthn) implementation is cryptographically broken and unusable in production.
  4. Multiple financial endpoints require no authentication, allowing unauthenticated payment record injection and private data exfiltration.
  5. Web3 payment verification only checks receipt.status, not recipient address or amount, making payment fraud trivial.

🔴 Critical Issues

1. Dispute Does Not Auto-Pause Escrow (Funds-at-Risk)

Files: 04 - Flows/Dispute Flow.md, 03 - API Reference/Dispute API.md

Finding: The Dispute Flow explicitly states: "Today, opening a dispute does not flip Payment.escrowState away from funded. An admin could theoretically still release the escrow before resolving the dispute."

At the same time, the Dispute API claims dispute creation "Pauses any in-flight payout (sets a hold flag)."

Impact: A buyer opens a dispute; an admin (or malicious/compromised admin) releases the escrow before resolution. The seller receives funds while the dispute is still open.

Required Fix: Introduce a disputed escrow state (or disputeHold flag) that blocks all release/refund operations until the dispute is resolved. The hold must be enforced in PaymentCoordinator, not just in controller logic.


2. Passkey Authentication Is Completely Broken

Files: 04 - Flows/Passkey (WebAuthn) Flow.md, 02 - Data Models/User.md

Findings:

  • Attestation is stubbed: The publicKey field is stored as the literal string 'simulated-public-key'. A malicious client can register any credential ID under any user account.
  • Refresh tokens are not persisted: Passkey-issued refresh tokens are never appended to user.refreshTokens[]. Standard /api/auth/refresh-token will reject them, breaking session continuity.
  • In-memory challenge store: storedChallenges is a Map in process memory. In a horizontally scaled deployment, the challenge created on instance A can only be verified on instance A. Load-balancer round-robin breaks authentication entirely.

Impact: Passkey auth is trivially bypassable and non-functional at scale.

Required Fix: Replace stub with @simplewebauthn/server (or equivalent), persist real public keys, store refresh tokens in the user record, and move challenges to Redis with TTL.


3. Unauthenticated State-Mutation Endpoints

Files: 03 - API Reference/Payment API.md, 03 - API Reference/AI API.md, 03 - API Reference/Notification API.md

Findings:

Endpoint Risk
POST /api/payment/decentralized/save Anyone can persist a Web3 payment record
POST /api/payment/decentralized/update Anyone can update decentralized payment status/confirmations
GET /api/payment/decentralized/history/:userId Anyone can read any user's payment history (privacy breach)
POST /api/payment/shkeeper/create-test-payment Anyone can inject test payment records into production data
POST /api/payment/decentralized/verify/:paymentId Anyone can trigger chain re-verification
POST /api/payment/decentralized/verify-all-pending Anyone can trigger a global batch verification job
All AI endpoints (/generate, /analyze, /translate, /assist) No caller identity; unlimited OpenAI cost abuse
Legacy notification router (notification/routes.ts) Mounted without auth; accepts ?userId= query parameter allowing notification read/modify for any user

Impact: Data poisoning, privacy violations, and unbounded cost exposure.

Required Fix: Add Bearer JWT middleware to all endpoints above. Enforce ownership or admin-role checks on :userId parameterized endpoints.


4. Web3 Payment Verification Trusts the Wrong Data

Files: 04 - Flows/Payment Flow - DePay & Web3.md

Finding: The verifier checks only receipt.status === '0x1'. It does not decode the Transfer event to verify:

  • to address == ESCROW_WALLET_ADDRESS
  • value >= expectedAmount (accounting for decimals)

Impact: A malicious actor can submit the hash of any successful transaction (e.g., a 0.01 USDT transfer to their own wallet) and the system will mark the payment as completed.

Required Fix: Decode the event logs; verify recipient, token contract address, and amount match the intent.


5. Three Mutually Incompatible Dispute Enum Sets

Files: 02 - Data Models/Dispute.md, 03 - API Reference/Dispute API.md, 04 - Flows/Dispute Flow.md

Finding: The three documents describe three different Dispute systems:

Source Status Enum Resolution Action Enum
Data Model pending, in_progress, waiting_response, resolved, rejected, closed refund, replacement, compensation, warning_seller, ban_seller, no_action
API Reference open, under_review, resolved_buyer, resolved_seller, closed buyer, seller, split
Flow pending, in_progress, resolved, closed refund, partial, release, reject

Impact: The API cannot be implemented as documented against the data model. The request/response schemas for POST /api/disputes/:id/resolve are completely different between API (uses decision) and Flow (uses action).

Required Fix: Create a single source-of-truth enum set. Align all three documents (or generate API docs from the model). ban_seller also has no corresponding User.status value (User.status is active | suspended | deleted).


🟠 High Severity Issues

6. Payment Model Uses Mixed for Core Foreign Keys

File: 02 - Data Models/Payment.md

purchaseRequestId, sellerOfferId, and sellerId are Schema.Types.Mixed (ObjectId or String) to support the template flow. This sacrifices all type safety, referential integrity, and indexing efficiency for the normal (non-template) payment path, which is the vast majority of transactions.

Fix: Use strict ObjectId refs for the standard path; store template linkage in a separate metadata field.


7. Broken Foreign Key References

Files: 02 - Data Models/PointTransaction.md, 02 - Data Models/Chat.md

  • PointTransaction.order references an Order model that does not exist in any documented schema.
  • Chat.relatedTo.type includes Transaction, but no Transaction model exists.

Fix: Remove or rename orphaned references. If Order is an internal alias, document it.


8. Delivery Confirmation Code Is Brute-Forceable

Files: 04 - Flows/Delivery Confirmation Flow.md

The 6-digit code (Math.floor(100000 + Math.random()*900000)) has only 900,000 combinations. There is no rate limiting documented on verification attempts.

Fix: Add Redis-backed rate limiting (e.g., 5 attempts per 15 minutes per request). Consider increasing entropy or adding time-based invalidation.


9. Purchase Request Status Machine Is Inconsistent

Files: 02 - Data Models/PurchaseRequest.md, 03 - API Reference/Marketplace API.md, 04 - Flows/Purchase Request Flow.md

Three different status enums:

Source Unique Values
Data Model pending_payment, active, seller_paid
API draft
Flow finalized, archived

The flow also claims a backward transition in_negotiation → received_offers is valid, but states elsewhere that "progression is forward-only except into terminal status."

Fix: One canonical enum. Remove ghost states (draft, finalized, archived, pending_payment, active) or implement them consistently.


10. JWTs in localStorage with 7-Day Expiry

Files: 01 - Architecture/Security Architecture.md

Access tokens are stored in localStorage (XSS theft vector) and expire in 7 days. For a financial escrow platform, this is far too long. The docs admit the risk but only list secondary mitigations (CSP, audits).

Fix: Move tokens to httpOnly cookies with 1560 minute expiry and implement a secure refresh-token rotation mechanism.


11. SHKeeper Webhook Swallows All Errors

File: 04 - Flows/Payment Flow - SHKeeper.md

The webhook returns 202 Accepted even on signature failures, DB errors, and unknown payments. Failures are silently swallowed with no dead-letter queue, retry mechanism, or alerting.

Fix: Differentiate 400 (client error, do not retry) from 500 (server error, retry). Log structured webhook failures to a DLQ or alerting channel.


12. Socket.IO Room Joining Is Client-Controlled

Files: 01 - Architecture/Real-time Layer.md

Clients explicitly emit join-user-room {userId} with their own ID. The docs include a warning that userId arguments "are NOT trusted blindly" and add "(cite: needs verification in socketService.ts)." This is an explicit admission the authorization check may not be implemented. If missing, any authenticated user can subscribe to another user's private notifications.

Fix: Remove client-driven join-user-room events. The server should automatically join the socket to user-{decoded.id} immediately after handshake auth.


13. Rate Limiting Is Effectively Disabled

Files: 01 - Architecture/Backend Architecture.md, 03 - API Reference/API Overview.md

express-rate-limit is disabled by default. Protected paths are limited to auth OTP/resend and AI endpoints. Marketplace, payment, chat, file upload, and notification endpoints have no documented rate limiting.

Fix: Enable global rate limiting with tiered limits (stricter for auth/financial, moderate for chat/marketplace).


🟡 Medium Severity Issues

14. Architecture Claims Stateless, But Isn't

Files: 01 - Architecture/System Architecture.md, 01 - Architecture/Backend Architecture.md, 01 - Architecture/Real-time Layer.md

  • Claims backend is stateless (JWT-only, no sessions).
  • Admits Redis stores login-attempt lockout counters (session state).
  • Socket.IO requires sticky sessions for multi-node, but current infra is a single Docker host. Running N=2 backend replicas would break real-time events.

Fix: Remove the "stateless" claim until auth state and Socket.IO are node-agnostic (Redis adapter + cookie-based sticky sessions).


15. Fictitious Dependencies

Files: 01 - Architecture/Frontend Architecture.md, 01 - Architecture/Infrastructure.md

  • Next.js 16 does not exist (latest stable is 15). Using an unverified framework version for a financial platform is high-risk.
  • Redis 8 (redis:8-alpine) does not exist (latest stable is 7.x).

Fix: Update docs to reflect actual tested versions.


16. Blog Post viewsCount Incremented on GET

File: 03 - API Reference/Blog API.md

GET /api/blog/posts/:slug atomically increments viewsCount. GET requests should be idempotent and safe.

Fix: Move view counting to a POST /api/blog/posts/:slug/view beacon endpoint, or use an async analytics pipeline.


17. API Request/Response Fields Do Not Match Data Models

File: 03 - API Reference/*

API Uses Model Has
Address (POST /api/addresses) fullName, street, postalCode, isPrimary name, fullAddress, zipCode, primary
Blog (POST /api/blog/posts) excerpt, isFeatured, videoUrl, metadata description, featured, videos[], no metadata
Chat (POST /api/chat) title name
Chat message (POST /api/chat/:id/messages) type, replyToMessageId messageType, replyTo
Payment (multiple) flat amount: number nested amount: { amount, currency }
Notification (POST /api/notifications) body, type (domain) message, type (severity), category (domain)
User profile (PUT /api/user/profile) name (alias for profile.name) No profile.name field exists
Points (POST /api/points/admin/add) reason description (required)

Fix: Align API specs to the data models, or vice versa, with a single source of truth.


18. Seller Can Update Price After Offer Acceptance

File: 04 - Flows/Negotiation Flow.md

The flow admits: "updateOffer does not enforce status... Current code allows the price change, which is dangerous post-payment."

Fix: Reject updateOffer if status !== 'pending'.


19. Buyer Can Confirm Delivery Before Seller Ships

File: 04 - Flows/Delivery Confirmation Flow.md

Preconditions list payment, processing, or delivery. The "manual fast-track" lets the buyer skip the code and confirm receipt at any time — including immediately after payment, before the seller acknowledges.

Fix: Restrict fast-track confirmation to status delivery only, or require admin override.


20. Payment confirmed Status Is a Ghost State

Files: 02 - Data Models/Payment.md, 04 - Flows/Payment Flow - SHKeeper.md

The model includes confirmed in status. The SHKeeper webhook maps PAID directly to completed, skipping confirmed. No automated flow appears to set it.

Fix: Remove confirmed from the enum, or document the transition that sets it.


21. partial Escrow State Does Not Exist in Model

Files: 04 - Flows/Escrow Flow.md, 04 - Flows/Payment Flow - SHKeeper.md, 02 - Data Models/Payment.md

Both flows use escrowState: "partial" for overpayments. The Payment model enum is funded | releasable | released | refunded | releasing | failedpartial is absent.

Fix: Add partial to the model enum, or map overpayments to funded with a overpaidAmount metadata field.


22. BlogPost.comments Is Just a Number

File: 02 - Data Models/BlogPost.md

There is no Comment model. The system counts comments but cannot store or retrieve actual comment content.

Fix: Implement a Comment collection, or remove the comments counter.


23. No Pagination on List-Oriented Endpoints

File: 03 - API Reference/*

  • GET /api/marketplace/sellers
  • GET /api/marketplace/purchase-requests/my
  • GET /api/marketplace/purchase-requests/:id/offers
  • GET /api/blog/posts/featured
  • GET /api/points/leaderboard
  • GET /api/users/contacts

Fix: Add cursor- or offset-based pagination to all unbounded list endpoints.


24. Missing Array Defaults

Files: 02 - Data Models/*

Across 5+ models, array fields (offers[], tags[], attachments[], evidence[], timeline[], participants[], etc.) have no default value. In Mongoose they default to undefined, causing runtime errors on .push() or .length.

Fix: Add default: [] to all array fields.


25. Inconsistent Soft-Delete Patterns

Files: 02 - Data Models/*

Model Pattern
User status: active / suspended / deleted
BlogPost status: draft / published / archived
RequestTemplate, Category, LevelConfig isActive boolean
Dispute status enum (no deleted state)
SellerOffer status enum

Fix: Standardize on one pattern (e.g., status enum with deleted or archived, plus deletedAt timestamp).


26. Frontend Docker Image Cannot Be Promoted Across Environments

File: 01 - Architecture/Frontend Architecture.md

The frontend uses process.env.NEXT_PUBLIC_API_URL, which is baked at build time. The same Docker image cannot be promoted from dev → staging → prod without rebuilding.

Fix: Inject the API URL at runtime via a server-side config endpoint or an env-replacement script in the container entrypoint.


27. Watchtower Auto-Deploys with Zero Staging Gate

File: 01 - Architecture/Infrastructure.md

Production auto-updates every 5 minutes on latest tag change with no health-check gate, blue/green rollout, or smoke test.

Fix: Implement a staging promotion pipeline: build → deploy to staging → smoke test → tag promote to stable → Watchtower watches stable, not latest.


🟢 Lower Severity / Polish Issues

28. Naming Inconsistencies

Files: 02 - Data Models/*

  • PointTransaction.user vs *Id suffix convention used everywhere else
  • User.referredBy vs referredById
  • User.profile.phone vs phoneNumber used in Address and PurchaseRequest
  • Chat.messages[].senderType defaults to User (PascalCase) while 95% of enums use lowercase
  • Address.addressType uses Home / Office / Other (PascalCase)
  • Chat.metadata.createdBy lacks Id suffix
  • Dispute.resolution.resolvedBy lacks Id suffix

29. Redundant / Derived Fields

Files: 02 - Data Models/PurchaseRequest.md, 02 - Data Models/User.md

  • PurchaseRequest.deliveryConfirmed boolean + deliveryConfirmedAt — boolean is derivable
  • PurchaseRequest.deliveryInfo.deliveryCodeUsed boolean — derivable from deliveryCodeUsedAt
  • User.points.total — derivable from available + used; risks arithmetic drift
  • PurchaseRequest.rating + feedback — duplicate the dedicated Review model (two sources of truth)

30. ER Diagram Errors

File: 02 - Data Models/Data Model Overview.md

  • PURCHASE_REQUEST ||--o| REVIEW implies max one review; the model allows many
  • CHAT ||--o{ DISPUTE direction is reversed; a chat belongs to at most one dispute

31. Missing Audit Timestamps

Files: 02 - Data Models/*

  • User has status: deleted but no deletedAt
  • PurchaseRequest has terminal states (completed, seller_paid, cancelled) but no completedAt / cancelledAt
  • Review has pending → published → rejected workflow but no transition timestamps
  • Dispute has adminId assignment but no assignedAt

32. Missing Constraints

Files: 02 - Data Models/*

  • Dispute.purchaseRequestId has no unique constraint (allows duplicate active disputes per request)
  • PurchaseRequest.deliveryInfo.deliveryCode is not unique or sparse-unique across requests
  • Category.parentId has no circular-reference guard
  • User.referredBy can self-reference with no prevention
  • LevelConfig.discountPercent has no upper bound (max: 100)
  • RequestTemplate.expiresAt can be created in the past

Cross-Cutting Themes

  1. Documentation Drift: The four primary document layers (Models, API, Flows, Architecture) describe different versions of the same system. The most egregious example is the Dispute entity, which has three incompatible definitions.
  2. Security Theater: Several sections describe security measures (CSP, audits, future ClamAV) while fundamental flaws (localStorage tokens, unauthenticated endpoints, stubbed crypto) remain unaddressed.
  3. Ghost States: Multiple status enums contain values that no automated flow sets (confirmed, partial, finalized, archived, active for SellerOffer).
  4. Operational Fragility: The production topology is a single Docker host with automatic latest deployment. There is no HA, no staging gate, and no upload storage that can survive horizontal scaling (uploads are host bind-mounts).

Recommendations (Priority Order)

  1. Fix the Dispute ↔ Escrow race condition immediately. A disputed state must block all releases.
  2. Align all status enums across Models, API, and Flows. Create a single source-of-truth document.
  3. Harden Passkey authentication before any production use.
  4. Add authentication and ownership checks to all financial endpoints.
  5. Fix Web3 verification to decode and validate Transfer event parameters.
  6. Enable rate limiting globally, with stricter tiers for auth and financial paths.
  7. Implement idempotency keys for dispute creation, payment verify, and payout creation.
  8. Protect /uploads with signed URLs or session-based auth for sensitive attachments.
  9. Fix SHKeeper webhook error handling — return proper status codes and log to a DLQ.
  10. Sanitize logs to remove plaintext verification/reset codes.