Known Vulnerabilities & Security Assessment
This document covers the current security posture of the Evergrn codebase as of June 2026. Issues are ranked by severity.
Critical
1. GodMode admin route exposes full impersonation capability [GODMODE]
Files: src/routes/admin.js, client/src/pages/GodMode.jsx, client/src/App.jsx
The /admin/impersonate endpoint issues valid, signed JWTs for any user or provider account with no audit trail and no notification to the impersonated account. The tokens it issues are indistinguishable from real user tokens โ downstream code cannot tell impersonation from normal login. If the godmode_admin_token leaks from the browser's localStorage (via XSS or physical access), an attacker can impersonate any account, read all their jobs and messages, and take actions (accept quotes, cancel jobs, charge their saved card) on their behalf.
Staging/production status: โ Excluded from staging and production
- API:
/adminroutes only registered whenNODE_ENVis notstagingorproduction(src/app.js) - Web:
/godmodeReact route andgodmode_admin_tokenUI gated byimport.meta.env.DEVโ stripped from all production builds by Vite tree-shaking - Staging is additionally protected by IP restriction (allowlist-only; Easy Auth disabled due to B1 tier VNETFailure โ
set-staging-ip.ps1)
Mitigation before production: Remove entirely. All marked with // [GODMODE] โ use grep -r "\[GODMODE\]" . to find every line.
2. JWT secret exposed in .env with no rotation mechanism
File: .env
JWT_SECRET is a static string loaded from .env at startup. If it leaks (committed to git, logged, or visible in a crash dump), every token ever issued must be considered compromised. There is no mechanism to revoke individual tokens โ once issued, a 7-day token is valid for its full lifetime even after a password change or account deletion.
Mitigation (before production):
- Ensure
.envis in.gitignoreโ verify withgit check-ignore -v .env - Add a token blocklist (Redis or DB table) checked in the
authenticatemiddleware - Consider shorter token lifetimes (1 day) with refresh tokens
- Rotate the secret immediately if it ever leaks
3. localStorage token storage vulnerable to XSS (web)
File: client/src/context/AuthContext.jsx
The JWT is stored in localStorage, which is accessible to any JavaScript running on the page. A single XSS vector (malicious ad, npm dependency attack, unsanitized content rendered as HTML) can silently read the token and make authenticated API calls from any machine.
Mitigation:
- Move to
httpOnlycookies set by the server โ JavaScript cannot access these - Implement Content Security Policy (CSP) headers to reduce XSS attack surface
- The mobile app already uses Expo SecureStore (device keychain) which is immune to this
High
4. No rate limiting on auth endpoints โ
RESOLVED
Resolution: loginLimiter (10 attempts per 15 minutes per IP via express-rate-limit) is applied to POST /auth/login, POST /auth/provider/login, and the admin login in src/routes/auth.js.
5. No input validation or sanitization library โ
RESOLVED
Resolution: Zod validation middleware implemented in src/middleware/validate.js and applied to all 18 body-receiving endpoints across auth.js, jobs.js, quotes.js, payments.js, users.js, reviews.js. Schemas are in src/schemas/index.js.
All invalid payloads now return 400 { error: "<field>: <message>" } before reaching route logic. Previously unvalidated fields like rating (1โ5 integer), amount (positive number), serviceZips (array of 5-digit ZIPs), email (format + lowercase normalization), and content lengths are now enforced at the API boundary.
6. File uploads stored on local disk โ
RESOLVED (MIME spoofing remains open)
Resolution: All file uploads now stream directly to Azure Blob Storage via src/config/blob.js (@azure/storage-blob + multer memoryStorage). Files never touch the container filesystem. Storage is durable across server restarts and scales horizontally.
Remaining open: MIME type is still client-declared (Multer fileFilter checks mimetype from headers, not actual file content). A malicious upload with a spoofed MIME header is technically possible. Mitigation: add file-type package to validate actual file magic bytes before uploading to blob.
7. No HTTPS in dev โ credentials sent in plaintext
Files: server.js, mobile/src/api.js
The API server runs plain HTTP on port 3000. In development, passwords and tokens are sent unencrypted. The localhost.run tunnel does add TLS for mobile-to-API traffic, but the local hop from Expo Metro to the device and the web frontend's communication with the Vite dev server are both HTTP.
Mitigation (before production): Terminate TLS at a reverse proxy (nginx, Caddy, or a cloud load balancer). Never run the API on plain HTTP in production. Ensure all Stripe webhooks use verified signatures (already implemented).
Medium
8. Stripe payment authorization is non-blocking โ silent failures
File: src/routes/quotes.js
When a customer accepts a quote, the Stripe PaymentIntent creation is wrapped in a try/catch that logs the error but resolves successfully:
} catch (e) {
console.error('Stripe auth failed:', e)
// quote still accepted, job still ACCEPTED
}
This means a job can be marked ACCEPTED and a provider dispatched without any payment being secured. If the customer's card is declined or invalid, the provider may complete work and never get paid.
Mitigation: Decide on a policy โ either block quote acceptance when payment fails (return 402 to the customer) or implement a separate payment-required gate before the provider can start the job.
9. Message thread has no content length limit โ
RESOLVED
Resolution: Zod validation enforces content min 1 / max 2000 characters on POST /jobs/:id/messages. Additionally, a 250-message cap per job thread was added โ the endpoint returns 429 if the thread already has 250 messages.
10. Provider can quote any PENDING job โ no business verification โ
RESOLVED
Resolution: Server-side check added to POST /quotes โ returns 403 if the job's ZIP is not in provider.serviceZips or the job's service is not in provider.services.
11. No CORS policy configured โ
RESOLVED
Resolution: CORS allowlist implemented in src/app.js as custom middleware (not the cors package). Allows: localhost:5173, https://evergrn.co, https://www.evergrn.co, and the Azure Static Web Apps default URL. Credentials allowed. OPTIONS preflight handled. All other origins receive no CORS headers and browsers will block cross-origin requests.
12. Seed scripts and admin account left in codebase
Files: scripts/seedAdmin.js, scripts/seedAvailableJobs.js, scripts/seedSchedule.js, scripts/seedConflicts.js
Seed scripts create accounts with known passwords (Test1234!, GodMode1!). If these scripts are accidentally run against a production database, they create exploitable accounts. The scripts have no environment guard.
Mitigation: Add a guard at the top of every seed script:
if (process.env.NODE_ENV === 'production') { console.error('Refusing to seed in production'); process.exit(1) }
Low / Informational
13. Client-side JWT decoding for role determination
Files: client/src/context/AuthContext.jsx, mobile/src/context/AuthContext.js
The client reads the role field by base64-decoding the JWT payload. This is safe for display purposes (the server re-verifies on every request), but if any client-side logic gates access based on role without a server round-trip, a user who crafts a fake token can bypass the client-side gate (though not the server). Currently the risk is cosmetic โ wrong dashboard shown โ but worth noting.
14. Audit log โ partially implemented
Status: The AuditEvent model and auditLog middleware (src/middleware/auditLog.js) are implemented. Every API request made during an active staff elevation session is logged to AuditEvent (elevationId, method, path, body). The internal_audit staff role can query all sessions and events via GET /staff/audit/sessions.
Still missing:
- Business-level audit trail (quote accepted, job cancelled, job completed, payment captured) โ these are not currently logged outside of PostgreSQL
updatedAttimestamps - Azure Application Insights captures request-level logs automatically, but without business-event context (which user accepted which quote, etc.)
15. avgTurnaroundMs in insights exposes internal timing data
File: src/routes/providers.js
The insights endpoint returns avgTurnaroundMs โ average milliseconds between job creation and completion. This is low-risk but exposes raw timing data that could be used to profile system behavior.
Security Measures Already in Place
| Measure | Status |
|---|---|
| bcrypt password hashing (12 rounds) | โ Implemented |
| JWT signature verification on every request | โ Implemented |
Staff token type claim (type: 'staff') โ prevents user tokens on staff routes |
โ Implemented |
| Stripe webhook signature verification | โ Implemented |
Stripe capture_method: 'manual' for payment holds |
โ Implemented |
| Admin login rate limiting (10/15min) | โ Implemented |
| Forgot-password rate limiting (5/15min) | โ Implemented |
| Multer MIME type filtering for uploads | โ Implemented (client-declared only) |
| Azure Blob Storage for all file uploads (no local disk) | โ Implemented |
| Expo SecureStore for mobile token storage | โ Implemented |
| Same error message for wrong email vs wrong password | โ Implemented |
| One-review-per-job constraint (DB unique) | โ Implemented |
| One-payment-per-job constraint (DB unique) | โ Implemented |
| HTTPS via App Service managed cert (Azure) | โ Implemented |
| CORS allowlist (localhost:5173, evergrn.co) | โ Implemented |
| Input validation with Zod (all body-receiving routes) | โ Implemented |
| Staff elevation audit log (AuditEvent + auditLog middleware) | โ Implemented |
| Staff elevation: 15-min auto-deactivation + admin approval gate | โ Implemented |
| Headshot + ID doc verification gate before provider can bid | โ Implemented |
| Rate limiting on user auth endpoints | โ Implemented โ loginLimiter (10/15 min) in src/routes/auth.js |
| httpOnly cookie token storage (web) | โ Missing |
| Token revocation / blocklist | โ Missing |
| Content-Security-Policy headers | โ Missing |
| MIME content verification for uploads (file-type package) | โ Missing |
| Business-level audit trail (quote accepted, job completed, etc.) | โ Missing |
| Environment guard on seed scripts | โ Missing |