# Form Security Audit — Contact & Newsletter Endpoints **Auditor:** security-auditor agent **Date:** 2026-03-21 **Scope:** `/api/contact` (POST), `/api/newsletter` (POST), and their frontend forms **Status:** ✅ No critical or high vulnerabilities. 1 medium, 2 low, 1 informational. --- ## Summary Both form endpoints were audited for: CSRF protection, rate limiting, input validation, output sanitization, injection risks, and secret exposure. The codebase demonstrates solid security practices overall. Findings are documented below by severity. --- ## Security Controls — Status | Control | Contact Form | Newsletter Form | Notes | |---------|-------------|-----------------|-------| | CSRF Protection | ✅ Origin/Referer check in middleware | ✅ Same | `validateCsrfOrigin()` in `middleware.ts:19-42` | | Rate Limiting | ✅ 5 req/hr/IP | ✅ 3 req/hr/IP | In-memory sliding window | | Input Validation | ✅ Server-side for all fields | ✅ Email validated | Allowlist for subject field | | Output Sanitization | ✅ `escapeHtml()` in email HTML | N/A | `contact.ts:181-188` | | Honeypot (Bot Detection) | ✅ `website` field check | ❌ No honeypot | Contact only | | Payload Size Limit | ✅ 16KB max | ✅ 4KB max | Checked via `Content-Length` header | | CORS | ✅ Origin allowlist in production | ✅ Same | Wildcard only in dev | | XSS Prevention | ✅ `textContent` used on frontend | ✅ Same | No `innerHTML` with server data | | Secret Exposure | ✅ Secrets server-only via `import.meta.env` | ✅ Same | No `PUBLIC_` prefix on sensitive keys | | Error Info Disclosure | ✅ Generic error messages to client | ✅ Same | Full errors go to logger/Sentry only | --- ## Findings ### MEDIUM — M01: In-Memory Rate Limiter Lost on Server Restart **File:** `src/pages/api/contact.ts:12`, `src/pages/api/newsletter.ts:12` **OWASP:** A07:2021 Identification and Authentication Failures **Description:** Both endpoints use a module-level `Map` for rate limiting: ```typescript const rateLimitStore = new Map(); ``` This map is stored in process memory. On any server restart, crash, or deployment, the rate limit counters reset to zero. An attacker who knows this can circumvent rate limits by triggering restarts, or simply timing attacks to coincide with deploys. **Risk:** Spam/abuse bursts become possible after any restart event. **Recommendation:** For production, migrate to a persistent store (Redis, Upstash, or a database-backed counter). For low-traffic sites, the current approach is acceptable with the understanding of this limitation. Consider adding a note to deployment runbooks that rate limit state is not persisted. --- ### LOW — L01: Honeypot Field CSS Positioning May Be Detected **File:** `src/pages/contact.astro:158` **OWASP:** A05:2021 Security Misconfiguration **Description:** The honeypot field uses CSS positioning to hide it from users: ```html