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

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):


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:


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:


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