44 documents
Operations & Runbooks⏱️10 min read

Production security audit

Audited against this repository on **2026-07-30**. Findings are evidence-based from current code; update this file when remediations land.

Production security audit

Audited against this repository on 2026-07-30. Findings are evidence-based from current code; update this file when remediations land.

Never paste secrets here — no webhook URLs, API keys, tokens, or customer PII. Reference variable names and file paths only.

Verdict

Not production-ready for multi-tenant / CRM-backed lead capture without hardening. Public GEO/MCP exposure is partly intentional (see ../decisions/0003-lead-capture-via-mcp-and-openapi.md), but multi-tenant routing, Apps Script auth, rate-limit bypass, and PII logging are concrete abuse paths.

Attack surface (summary)

Surface Path Auth Rate limit CORS
REST lead capture POST /api/leads/submit None (X-API-Key is not validated) In-memory, 10/min/IP *
MCP Streamable HTTP /api/mcp/sse, /api/mcp/mcp None None *
On-site contact POST /api/contact None In-memory, 5/min/IP Same-origin form
OpenAPI docs /docs/openapi.json, /api/openapi.json None N/A *
CRM webhook (Apps Script) CRM_WEBHOOK_URL target URL-as-secret only N/A N/A

Related integration notes: ../integrations/lead-capture.md, ../integrations/mcp-server.md. Multi-tenant ADR: ../decisions/0008-multi-tenant-lead-routing-and-rate-limiting.md.


Critical

1. Google Apps Script webhook has no authentication

Files scripts/google-apps-script/Code.gs (approx. L37–86), scripts/google-apps-script/SETUP.md
Evidence doGet / doPost accept any caller; no shared secret, signature, or token check. Setup requires Who has access: Anyone.
Impact Anyone holding the Web App URL (logs, leaked env, error surfaces, referrers) can append arbitrary rows — sheet spam, data pollution, formula payloads.
Fix Require a header secret (e.g. X-Webhook-Secret) checked in doPost; rotate the URL on leak; optionally restrict by known server IPs if hosting allows.

High

2. Multi-tenant client_id is attacker-controlled (tenant spoofing)

Files app/api/leads/submit/route.ts (approx. L76–82), lib/leads.ts (resolveClientWebhookUrl, submitConsultationLead), lib/mcp-server.ts, lib/openapi-spec.ts
Evidence client_id comes from body, X-Client-ID, or X-API-Key with no proof of tenancy. That ID maps to tenant webhooks via CLIENT_WEBHOOK_MAP / CLIENT_WEBHOOK_<ID> / CRM_WEBHOOK_URL.
Impact Knowing or guessing a tenant ID routes junk into that client’s CRM/Sheet. Unknown IDs fall back to the default webhook (spam the owner). OpenAPI documents the client_id pattern.
Fix Bind routing to a server-side API key → tenant map. Never accept raw public client_id as routing authority.

3. X-API-Key is not authentication — it is treated as client_id

Files app/api/leads/submit/route.ts (CORS headers + header extraction)
Evidence x-client-id and x-api-key are OR’d into the same clientId string. Neither is validated against a secret store. CORS advertises Authorization / X-API-Key.
Impact False sense of security; “keys” become tenant identifiers and may leak in browser/agent configs.
Fix Implement real API-key auth (constant-time compare against hashed secrets) or remove the header until auth exists.

4. MCP lead tool is public, unauthenticated, and unrate-limited

Files lib/mcp-server.ts, app/api/mcp/[transport]/route.ts, ../integrations/mcp-server.md
Evidence CORS Access-Control-Allow-Origin: *; no auth; no checkRateLimit. Advertised in public/llms.txt, .well-known/llms.txt, and JSON-LD.
Impact Unlimited webhook/Sheet writes via MCP, bypassing the 10/min limit on /api/leads/submit.
Fix Shared rate limiter (Redis/Upstash), optional MCP auth for non-demo use, bot/abuse controls. Keep fully public only if product requires it and residual spam risk is accepted.

5. Rate limiting is bypassable via client-controlled IP headers

Files lib/rate-limit.ts (getClientIp)
Evidence Uses leftmost x-forwarded-for, then x-real-ip, then cf-connecting-ip.
Impact Attackers can rotate spoofed IPs and evade the 10/min and 5/min limits unless the platform overwrites these headers.
Fix Trust only the platform’s canonical client IP (after trusted proxy rewrite). Never trust raw leftmost XFF without a trusted hop count.

6. In-memory rate limiter is ineffective on multi-instance / serverless

Files lib/rate-limit.ts (process-local Map), ADR ../decisions/0008-multi-tenant-lead-routing-and-rate-limiting.md
Evidence Tracker is in-process; resets per instance / cold start. ADR already notes Redis as a follow-up.
Impact Effective limit scales with instance count — weak anti-abuse in production.
Fix Distributed limiter (Redis / Upstash / edge).

Medium

7. Webhook URL / upstream detail leak into server errors and logs

Files lib/leads.ts (network failure path embeds webhookUrl in LeadWebhookError), app/api/leads/submit/route.ts (console.error of full error)
Evidence HTTP JSON responses to clients are sanitized (good), but logs/APM retain the secret Web App URL.
Note Webhook targets are env-configured — classic user-driven SSRF from the request body is not present. Risk is credential disclosure via logs.
Fix Log status/error code only; never log full webhook URL or upstream body; redact in error constructors.

8. Full PII logged when webhook unset or on contact submit

Files lib/leads.ts (unconfigured webhook → console.info of full payload), app/api/contact/route.ts (console.info("[contact] Lead received", lead))
Impact Name, email/phone, notes in host logs (retention, access, compliance).
Fix Log lead_id + non-PII metadata only; redact contact fields.

9. Input length / size gaps (DoS / oversized sheet cells)

Field Current limit
client_name min 2, no max
budget_range_usd (string) min 1, no max
client_email_or_phone min 5, no max
contact name / email / scope min only; no max
requirements_notes max 5000 (correct)
Files lib/leads.ts, app/api/contact/route.ts
Impact Large JSON bodies → memory/CPU, huge Sheet cells, log bloat.
Fix Add max lengths (e.g. name 200, contact 100, budget string 100, scope 5000); reject oversized bodies early.

10. Google Sheets formula / CSV injection

Files scripts/google-apps-script/Code.gs (appendRow)
Evidence User strings written without neutralizing leading =, +, -, @.
Impact Malicious requirements_notes / names can become formulas (IMPORTDATA, etc.) when opened in Sheets.
Fix Prefix dangerous values with ' or escape before append.

11. No security headers / no middleware

Files next.config.ts (OpenAPI CORS only); no middleware.ts
Evidence Missing CSP, HSTS, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, frame-ancestors / X-Frame-Options.
Impact Weaker XSS / clickjacking / MIME defenses for the marketing site.
Fix Add baseline headers in next.config.ts or middleware (tighten CSP carefully around JSON-LD).

12. Contact form accepts leads but does not deliver them

Files app/api/contact/route.ts
Evidence Resend path is a stub; success returned after logging only.
Impact Silent lead loss; users believe submission succeeded.
Fix Wire delivery, or return an explicit “not configured” / disable the form in production until ready.

Low

13. Public OpenAPI + llms.txt expand attack surface (by design)

Files lib/openapi-spec.ts, OpenAPI routes, public/llms.txt, public/.well-known/llms.txt, components/GeoSchema.tsx
Impact Easy discovery of lead schema, endpoints, and client_id multi-tenancy — amplifies High findings #2–#4.
Fix Accept for GEO, or split a “demo” surface from authenticated tenant APIs; omit tenant-routing fields from public specs.

14. Knowledge base documents insecure design as accepted

Files ../decisions/0003-lead-capture-via-mcp-and-openapi.md, ../integrations/mcp-server.md, ../architecture/geo-mcp-gap-analysis.md, ../integrations/lead-capture.md
Evidence Explicitly: public, unauthenticated MCP. lead-capture.md may still say “No rate limiting” in places that are stale vs the REST limiter.
Fix Keep ADRs accurate; track MCP rate limit/auth as open production blockers.

15. Confidential playbook PDF committed to git

Files knowledge/source/JSGlobtech_GEO_Agency_Playbook.pdf, ../source/README.md
Evidence Marked confidential business blueprint; not served by Next.js routes (not web-exposed at runtime).
Impact Pricing/GTM leak if repo access widens or goes public.
Fix Remove from git; store outside the repo; use history rewrite if already pushed and exposure matters.

Info

16. Dependency advisories (npm audit --omit=dev, inspected 2026-07-30)

Package Severity Notes
@modelcontextprotocol/sdk via @hono/node-server Moderate Path traversal in serve-static on Windows; lower relevance if only web-standard transport on Linux is used
next → postcss / sharp High (transitive) Upgrade Next when patched releases land

Do not run npm audit fix --force — it may attempt a breaking Next downgrade. Prefer intentional version bumps when fixes are available.

17. robots.ts allows broad crawl

app/robots.ts — allow: "/" for *. Intentional for GEO; API abuse still depends on auth/rate limits above.

18. JSON-LD via dangerouslySetInnerHTML

components/GeoSchema.tsx — low XSS risk given controlled/static data. Prefer standard patterns if data ever becomes user-influenced.


SSRF assessment

Question Finding
User-controlled webhook URL? No — only CRM_WEBHOOK_URL / CLIENT_WEBHOOK_* / CLIENT_WEBHOOK_MAP
Classic SSRF from request body? Not evidenced
URL leakage? Yes, in server errors/logs — not in sanitized HTTP JSON responses

Production blockers (priority order)

  1. Shared secret on Apps Script + stop treating the Web App URL as the sole auth.
  2. Real API-key → tenant mapping; remove public client_id routing authority.
  3. Rate-limit MCP with a distributed store; fix IP trust in getClientIp.
  4. Stop logging PII and webhook URLs.
  5. Max-length validation + Sheets formula escaping.
  6. Baseline security headers.

Remediation tracking

ID Severity Status
#1 Apps Script auth Critical Open
#2 Tenant spoofing High Open
#3 Fake X-API-Key High Open
#4 MCP unauth / unrate-limited High Open
#5 Spoofable IP High Open
#6 In-memory limiter High Open
#7 Webhook URL in logs Medium Open
#8 PII in logs Medium Open
#9 Missing max lengths Medium Open
#10 Sheet formula injection Medium Open
#11 Security headers Medium Open
#12 Contact form no delivery Medium Open
#13 Public discovery surface Low Accepted risk (GEO) unless tenants ship
#14 Stale KB claims Low Open
#15 Confidential PDF in git Low Open
#16 Dep advisories Info Monitor
#17 Broad robots Info Accepted (GEO)
#18 JSON-LD innerHTML Info OK while data is static

When a finding is fixed in code, flip its status here and note the PR/commit in the same change that lands the fix.