Corrective Action: My Value Path Portal — 6 Security Vulnerabilities Patched

Corrective Action: My Value Path Portal — 6 Security Vulnerabilities Patched

Date: March 15, 2026 Category: Security / Authorization Failure Impact: 6 Tier 1 security vulnerabilities across the authenticated portal experience. Any authenticated user could impersonate others, access unauthorized data, and bypass admin controls. Public staging sitemap exposed full URL structure. Presentation analytics API was unauthenticated. Resolution: All 6 patched in commit 63bb1453. tsc passes clean.


Incident

What Happened

A 5-agent audit of the My Value Path portal (139 routes under /my-value-path/) uncovered 20 critical, 36 major, 30 minor, and 25 informational findings. Six findings were classified as Tier 1 security issues requiring immediate remediation.

These vulnerabilities existed since the portal's initial implementation. They were not caught because:

  1. The portal was built incrementally across many sessions without a security review
  2. Silent error swallowing (.catch(() => [])) masked API failures that would have surfaced authorization gaps
  3. localStorage-first patterns gave functional feedback that hid broken HubSpot integrations

The 6 Vulnerabilities

# Vulnerability Severity File
1 Impersonation cookie not bound to session — any user can set vf_impersonate via dev tools CRITICAL src/lib/auth.ts
2 Onboarding endpoint has no ownership verification — any user can update any service record CRITICAL src/pages/api/onboarding/complete.ts
3 Planning admin uses email-substring check (email.includes('valuefirst')) CRITICAL src/pages/my-value-path/planning/admin/index.astro
4 Open redirect in auth/verify — unvalidated redirect parameter CRITICAL src/pages/auth/verify.astro
5 Staging sitemap: prerender = true, zero auth — full URL structure publicly exposed CRITICAL src/pages/my-value-path/command-center/staging/sitemap.astro
6 Presentation analytics API — zero authentication on GET and POST MAJOR src/pages/api/presentations/analytics/

Root Cause

1. No Security Review Process for Authenticated Routes

The portal grew organically. Each page was built to "work" (fetch data, render UI) without systematic authorization review. The middleware detects 7 roles but only sets variables — individual pages must check them. Most don't.

2. Impersonation Was a Dev Convenience That Shipped to Production

The vf_impersonate cookie was likely added for testing ("see what this person's portal looks like"). It was never gated behind admin role verification or session binding. Any user who discovers it can impersonate any other user by setting a cookie with a HubSpot contact ID.

3. Email-Substring Auth Is Not Auth

The admin page checked email.includes('valuefirst') || email.includes('admin'). This matches any email containing those substrings — including admin@malicious.com or myvaluefirst@gmail.com. This is pattern matching, not authorization.

4. Open Redirect Is a Known Attack Vector

The auth/verify page accepted a redirect query parameter and redirected to it without validation. An attacker could craft a magic link URL that redirects to a phishing site after successful authentication.


Fixes Applied (commit 63bb1453)

Fix 1: Impersonation Cookie Bound to Session + Role

Added getContactIdFromCookieAsync() in auth.ts:

  • Validates the auth token FIRST (confirms the user is authenticated)
  • Fetches the authenticated user's contact record from HubSpot
  • Checks if the user has Head Coach role via isHeadCoachRole()
  • Only then reads the impersonation cookie
  • Non-Head-Coach users: impersonation cookie is silently ignored

Fix 2: Onboarding Ownership Verification

complete.ts now:

  • Fetches all services associated to the authenticated contact
  • Verifies the requested serviceId is in that association list
  • Returns 403 if the user doesn't own the service record

Fix 3: Role-Based Admin Check

planning/admin/index.astro now:

  • Uses canAccessCommandCenter(unifiedRole) instead of email substring
  • Properly detects unified role from the middleware context
  • Added export const prerender = false (was missing, needed for SSR auth)

Fix 4: Open Redirect Validation

auth/verify.astro now:

  • Validates redirect starts with / (relative path)
  • Rejects // prefix (protocol-relative URLs that bypass origin)
  • Defaults to /my-value-path if validation fails

Fix 5: Staging Sitemap Auth

staging/sitemap.astro now:

  • Changed prerender = true to prerender = false (SSR, not static)
  • Added full auth block: cookie check, contact fetch, canAccessCommandCenter() gate
  • Unauthenticated/unauthorized users get 403

Fix 6: Presentation Analytics Auth

Both index.ts and [slug].ts now:

  • Check getContactIdFromCookie() on all handlers (GET and POST)
  • Return 401 JSON response if unauthenticated

Systemic Issues Identified (Not Yet Fixed)

The audit revealed three systemic patterns beyond these 6 fixes:

Pattern Scope Status
localStorage masking HubSpot failures 9 of 12 planning tools Properties now created (Ledger reconciliation). Code wiring needed.
Silent error swallowing (.catch(() => [])) 25+ pages Tracked in audit synthesis. Progressive remediation.
Authentication without authorization ~60% of routes Middleware has role detection. Pages need to use it. Route migration blueprint maps all 139 routes.

Prevention

Immediate

  1. Security audit is now a proven capability — 5-agent parallel audit pattern documented. Can be repeated for any experience vertical.
  2. Route migration blueprint created at /mnt/d/Leadership/audits/2026-03-15-my-value-path/route-migration-blueprint.md — maps every route with its auth status.
  3. HubSpot property reconciliation completed — 10 missing properties created, 11 undocumented properties indexed, 3 enum mismatches flagged.

Structural

  1. Every new authenticated route MUST call canAccessPath() or equivalent role check. The middleware function exists but was never called by individual pages.
  2. No more email-substring checks anywhere in the codebase. Role-based authorization only.
  3. Impersonation requires session-bound, role-verified access. The getContactIdFromCookieAsync() pattern established in this fix is the template.

Verification

  • tsc --noEmit passes clean (these are SSR-only pages — no full build required per build-verification skill)
  • All 6 files compile without type errors
  • Auth patterns verified against existing canAccessCommandCenter() implementation
  • Property-index updated with 21 newly documented/created properties

Audit Reports

Report Path
Synthesis (111 findings) /mnt/d/Leadership/audits/2026-03-15-my-value-path/synthesis.md
Auth & Infrastructure /mnt/d/Leadership/audits/2026-03-15-my-value-path/auth-infrastructure.md
Command Center /mnt/d/Leadership/audits/2026-03-15-my-value-path/command-center.md
Contributor & Media /mnt/d/Leadership/audits/2026-03-15-my-value-path/contributor-media.md
Commerce & Objects /mnt/d/Leadership/audits/2026-03-15-my-value-path/commerce-objects.md
Planning & Self-Service /mnt/d/Leadership/audits/2026-03-15-my-value-path/planning-selfservice.md
Route Migration Blueprint /mnt/d/Leadership/audits/2026-03-15-my-value-path/route-migration-blueprint.md
HubSpot Property Reconciliation /mnt/d/Leadership/audits/2026-03-15-my-value-path/hubspot-property-reconciliation.md

Corrective action documented by V. Commit 63bb1453. March 15, 2026.