Skip to content

Security Posture Overview

Prism authenticates a diverse set of callers — web/mobile staff, customer-facing loyalty apps, POS/3rd-party integrators, and internal WhatsApp services — through six distinct auth layers, each validating a different credential against a different datastore. This page is the security map: who is trusted, how tenants are isolated, what the request pipeline monitors, and — most importantly — the known security findings that must be remediated.

Validation legend: ✅ verified against source · ❔ representative, verify · ⚠ security finding / risk

1. The six auth layers

Every protected route attaches exactly one auth middleware. All layers fail closed (401/403/500 on any error — never fall through unauthenticated).

# Middleware File Credential Validates against Sets on req Failure
1 authMiddleware middlewares/authMiddleware.js token header (primary) / password body (fallback, bcrypt) MySQL users + auth_tokens (joined w/ roles, addo_account_mapping, addo_business, business_category) req.body.created_by 403 / 500
2 adminMiddleware middlewares/adminMiddleware.js token header / password body MySQL (single conn) — hardcoded business_id == 1 ⚠ req.body.created_by 403 / 500
3 appMiddleware middlewares/appMiddleware.js contactMappingId + token + parent_id (body) MySQL addo_contacts_mapping (status=1) 403 / 500
4 crm_offer_middleware middlewares/crm_offer_middleware.js Authorization header MongoDB authorization_token (exact match) req.parent_business_id 401 (uE_101)
5 jwtMiddleware / jwtWhatsappMiddleware middlewares/jwtMiddleware.js, middlewares/jwtWhatsappMiddleware.js Authorization: Bearer <jwt> JWT HS256 (hardcoded secret) ⚠ req.user 401 / 403
6 injestion_middleware / thirdPartyMiddleware middlewares/injestion_middleware.js, middlewares/thirdPartyMiddleware.js token header Static string compare (hardcoded token) ⚠ 401

Deep dive on each layer (headers, queries, decision table, flowchart): Authentication.

Two cross-cutting middlewares run before any auth, wired once in index.js: - loggerService.traceIdMiddleware() — sets a request-scoped trace_id for log correlation. - requestLogger (middlewares/requestLogger.js) — the plaintext-password monitor (see §3).

2. Multi-tenancy isolation

Prism is a shared multi-tenant platform. Isolation is application-enforced, not database-enforced (there is no per-tenant DB/schema and no row-level security).

  • parent_business_id — the brand/chain (the parent account). crm_offer_middleware derives it from the matched authorization_token document (req.parent_business_id) so a POS integrator can only act within its own brand.
  • child_business_id — the individual outlet/store under a parent (in MySQL: addo_business.id, mapped via addo_account_mapping).
  • authMiddleware scopes a staff user to a specific outlet by requiring both userId and business_id in the join (WHERE u.userId=? AND b.id=?), and by binding tokens to userId (a token cannot be replayed against another account).
  • Socket.IO sessions carry parent_business_id + child_business_id on whatsapp_chat_sessions.

⚠ Isolation depends on every controller filtering by tenant. Because middleware sets req.parent_business_id/req.body.business_id but does not force controllers to use it, a controller that queries without a tenant filter can leak cross-tenant data. Treat tenant-scoping as a per-controller invariant, not a solved problem.

flowchart LR
    A[parent_business_id<br/>brand / chain] --> B[child_business_id<br/>outlet 1]
    A --> C[child_business_id<br/>outlet 2]
    A --> D[child_business_id<br/>outlet N]
    B --> E[users / staff<br/>scoped by userId + business_id]
    C --> F[customers / orders<br/>crm_queue, customer 360]

3. The plaintext-password monitor (requestLogger)

middlewares/requestLogger.js is a detective control, not a preventive one. It fires on every request that carries a non-empty password in the JSON/form body or the query string, and emits a warn via loggerService.get("security", "plaintext-password-monitor") with error code INCOMING_PASSWORD_PRESENT.

Key properties (verified ✅): - Never logs the password value. Only its presence. It even strips the query string from the referer header before logging (referer.split("?")[0]) so a ?password= leaked via referer is not written. - Logs metadata only: endpoint (method path), origin/referer (path-stripped), has_token, capped user_agent (≤512 chars), and unverified/spoofable user_id + tenant.business_id (client-supplied, pre-auth — do not treat as identity). - Wrapped in try/catch — logging never breaks the request. - Naming note: despite the "plaintext" name, it is presence-based — it fires even for correctly-bcrypted values. Read the signal as "a password was transmitted".

⚠ This monitor tells you passwords are flowing through request bodies/query strings (see the query-string finding in §4), but it does not block them. It is an observability signal to drive remediation, not a fix.


4. Security Findings & Risks

Each finding references file:line only. Secret values are never reproduced here — rotate the secret and read it from the source file if you need the current value. Every item is marked ⚠.

Hardcoded secrets in source (CRITICAL)

  • ⚠ JWT signing secret (CRM)middlewares/jwtMiddleware.js:5. HS256 secret committed to the repo; anyone with source access can forge valid req.user tokens.
  • ⚠ JWT signing secret (WhatsApp)middlewares/jwtWhatsappMiddleware.js:5. Separate hardcoded HS256 secret; same forgery risk for WhatsApp login routes.
  • ⚠ Ingestion static tokenmiddlewares/injestion_middleware.js:8. A single shared string guards /injestion/ls_center and /sync/crm/token; leak = full order-injection + token-sync access.
  • ⚠ Third-party API tokenmiddlewares/thirdPartyMiddleware.js:6. Single shared string; no per-partner keys, no revocation.
  • ⚠ New Relic license keynewrelic.js (license field). Committed monitoring credential.
  • ⚠ Redis host hardcodedutilities/redis.js (ElastiCache host + rejectUnauthorized:false). Not env-driven; endpoint exposed and TLS verification disabled.
  • ⚠ Prism-services Mongo URI (with password)serverless-orders.yml:51, serverless-whatsapp.yml:67, serverless-whatsapp.yml:89. Plaintext MongoDB Atlas connection string including password committed to version control (prism-services repo). Highest-severity secret in the fleet — grants direct DB access to the shared uengage database.
  • ⚠ Prism-services Kafka brokers hardcodedserverless-orders.yml / serverless-whatsapp.yml (KAFKA_BROKER_URL). Broker endpoints baked into YML rather than env/SSM.

Committed credentials file

  • ⚠ .env.dev checked into the repo — contains plaintext dev credentials (SQL password, commented Redis password). Should be .gitignored. .env.prod.example as a template is acceptable; .env.prod is correctly not in the repo.

Transport & network exposure

  • ⚠ Open CORSindex.js (and socket.js) use wildcard origin *. Any origin can call the API from a browser; combined with credentials-in-body this widens CSRF/abuse surface.
  • ⚠ No HTTPS enforcement — the app creates a plain http.createServer(app) (index.js). TLS termination is assumed upstream but not enforced; there is no HSTS / redirect.
  • ⚠ No rate limiting — no throttling on any route. OTP (/api/generateOTP), login, and ingestion endpoints accept unlimited requests (brute-force / DoS / OTP-bombing exposure).

Authorization weaknesses

  • ⚠ Admin check hardcodedmiddlewares/adminMiddleware.js:9 gates admin purely on business_id == 1. Not role-based; any actor who can authenticate as business 1 is admin, and there is no defence-in-depth beyond the numeric check.
  • ⚠ Password acceptable in query stringauthMiddleware/requestLogger both read password from the query string. Passwords in URLs land in access logs, proxies, and browser history. requestLogger detects this but does not block it.
  • ⚠ Static tokens in plaintext headersinjestion_middleware, thirdPartyMiddleware compare a shared string with no rotation, no per-caller identity, and no expiry.

Remediation guidance (applies to all secret findings)

  1. Move secrets to env vars / a secrets manager (AWS Secrets Manager or SSM Parameter Store for the serverless side; env-injected config for the monolith). Never read secrets from committed files.
  2. Rotate every exposed secret listed above — assume all are compromised because they are in git history. Purge from history where feasible.
  3. Issue per-caller credentials (per-partner API keys with revocation) instead of single shared static tokens.
  4. Enforce TLS at the edge (HTTPS + HSTS), restrict CORS to known origins, and enable rejectUnauthorized for Redis/Mongo TLS.
  5. Add rate limiting (per-IP + per-tenant) on auth, OTP, and ingestion routes.
  6. Replace the business_id==1 admin gate with role-based authorization.

Remediation priority

Priority Finding Location Impact Suggested action
P0 Mongo URI + password in YML serverless-orders.yml:51, serverless-whatsapp.yml:67,89 Direct DB compromise of shared uengage DB Rotate password now; move to Secrets Manager; purge history
P0 JWT secrets hardcoded jwtMiddleware.js:5, jwtWhatsappMiddleware.js:5 Token forgery / auth bypass Rotate secrets; env vars; invalidate live tokens
P0 .env.dev committed repo root Credential exposure Remove + .gitignore; rotate dev creds
P1 Static ingestion / 3rd-party tokens injestion_middleware.js:8, thirdPartyMiddleware.js:6 Order injection / integration abuse Per-partner keys + rotation
P1 New Relic license newrelic.js Monitoring account exposure Rotate; env var
P1 No rate limiting app-wide Brute-force / OTP-bombing / DoS Add per-IP + per-tenant limits
P1 Admin gate business_id==1 adminMiddleware.js:9 Weak authZ Role-based authorization
P2 Open CORS * index.js, socket.js CSRF / browser abuse Allowlist origins
P2 No HTTPS enforcement index.js Cleartext transport Enforce TLS + HSTS at edge
P2 Redis host hardcoded + TLS off utilities/redis.js Endpoint exposure Env-drive; enable cert verification
P2 Kafka brokers hardcoded serverless YMLs Config drift / exposure Env / SSM
P2 Password in query string authMiddleware, requestLogger Creds in logs/proxies Reject query-string passwords