This document summarizes the security measures implemented in TaniChain and known limitations, written for hackathon judges and anyone evaluating the codebase.
- Passwords are hashed with bcrypt (cost factor 12), never stored or logged in plaintext.
- Sessions use JWT signed with
JWT_SECRET, with a configurable expiry (JWT_EXPIRES_IN, default 7 days). - All mutating endpoints require a valid
Authorization: Bearer <token>header, verified by theauthenticatemiddleware. - Role-based access control (
authorize(...roles)) restricts farmer-only, buyer-only, and admin-only endpoints at the route level - not just in the UI. - Ownership checks are enforced in the service layer (e.g. a farmer can only edit/delete their own products; a buyer can only cancel their own orders) so a valid token for User A can never be used to act on User B's resources, even if the role matches.
- Every Stellar secret key is encrypted at rest with AES-256-GCM before being stored in Postgres (
src/utils/crypto.ts). The encryption key is derived fromWALLET_ENCRYPTION_KEYvia SHA-256. - Decrypted secret keys exist only in memory, only for the duration of signing a single transaction, and are never returned in any API response or logged.
- Only the Stellar Testnet is used - there is no path to real-world funds.
- The platform escrow wallet uses the same encryption-at-rest approach as user wallets.
- Every request body is validated with Zod schemas before it reaches business logic (
middleware/validate.ts). Invalid input returns a 422 with field-level error messages rather than reaching the database layer. - Prisma's parameterized queries prevent SQL injection by construction - the codebase contains no raw SQL string concatenation.
- Helmet sets standard security headers (
X-Content-Type-Options,X-Frame-Options, HSTS in production, etc.). - A scoped Content-Security-Policy is applied (
default-src 'self'), permissive only where Swagger UI genuinely needs inline styles/scripts to render. - CORS is restricted to the configured frontend origin (
CORS_ORIGIN), not a wildcard, and credentials are only allowed for that origin. - Rate limiting is applied globally (300 req/15min per IP by default) and more strictly on auth endpoints (20 req/15min) to slow down credential-stuffing and brute-force attempts.
- Nginx terminates all external traffic and proxies internally over the Docker network; only ports 80 (Nginx) and optionally 3000/4000 (for direct debugging) are exposed.
passwordHashis stripped from every user object before it's returned from the API (sanitizeUserinauth.service.ts/user.service.ts).- Wallet secret keys are never included in any API response, including admin endpoints.
- Error responses only include stack traces when
NODE_ENV=development; production responses return a generic message.
- Single platform escrow key: the escrow simulation uses one Stellar account for all in-flight orders. This is appropriate for a hackathon demo of the payment flow, but a production system would want per-order or sharded escrow accounts, or a Soroban smart contract instead of a custodial key, to reduce blast radius if that one key were ever compromised.
- No email verification / password reset flow: registration trusts the provided email address. Out of scope for this build; would be required before any real-money deployment.
- JWT has no revocation list: a stolen token remains valid until it expires. A production deployment should add a short-lived access token + refresh token pattern, or a revocation store.
- Rate limiting is in-memory (
express-rate-limitdefault store): it resets on restart and doesn't share state across multiple backend replicas. A multi-instance production deployment should back it with Redis. - Default secrets in
.env.exampleare placeholders and must be replaced before any non-local deployment - seedocs/DEPLOYMENT.md.