# AgentForms Comprehensive Audit Report
**Date:** 2026-06-30
**Scope:** Full frontend + backend audit — Security, Architecture, UI/UX, Performance, Code Quality
**Stack:** Flask + SQLite + Redis + RQ + Stripe, Python 3.11, Docker
**Positioning:** API-First Form & Document Platform
---
## Executive Summary
| Area | Severity | Count |
|------|----------|-------|
| **Backend** | Critical | 4 |
| | High | 9 |
| | Medium | 15 |
| | Low | 8 |
| **Frontend** | Critical | 1 |
| | High | 5 |
| | Medium | 10 |
| | Low | 6 |
| **TOTAL** | **Critical** | **5** |
| | **High** | **14** |
| | **Medium** | **25** |
| | **Low** | **14** |
---
## 🔴 CRITICAL (Must Fix — Production Risk)
### C1 — Stripe Webhook Replay Vulnerability (Backend)
- **Files:** `app/routes/billing.py`
- **Issue:** No dedup protection on Stripe webhooks. No `stripe_events_seen` table to prevent replay attacks.
- **Risk:** Attacker replays webhook to grant free tier upgrades or manipulate subscription state.
- **Fix:** Add `CREATE TABLE IF NOT EXISTS stripe_events_seen (event_id TEXT PRIMARY KEY, processed_at TIMESTAMP)` and check `event.id` before processing.
### C2 — SQLite Cross-Thread Corruption (Backend)
- **File:** `app/db.py:1`
- **Code:** `sqlite3.connect(DB_PATH, check_same_thread=False, timeout=30)`
- **Issue:** `check_same_thread=False` allows concurrent writes without locking. Combined with WAL mode + RQ workers = silent data corruption risk.
- **Fix:** Wrap all DB operations in `threading.Lock()`.
### C3 — Rate Limiter Fail-Open on Redis Failure (Backend)
- **File:** `app/services/ratelimit.py:74-75, 100-102`
- **Issue:** When Redis is down, auth endpoints (`/api/login`, `/api/register`) allow ALL requests. No local fallback.
- **Fix:** Add in-memory rate limiter fallback for auth endpoints.
### C4 — Password Timing Oracle (Backend)
- **File:** `app/routes/auth.py:50-64`
- **Issue:** Early return on "user not found" vs bcrypt timing (~300ms) allows email enumeration via response timing.
- **Fix:** Always execute bcrypt.checkpw even for non-existent users with a dummy salt.
### C5 — Inline Scripts Violate CSP (Frontend)
- **Files:** `app/templates/documents_dashboard.html`, `edit_site.html`, `hosted_form.html`, `referral.html`, `site_analytics.html`, `user_settings_api_keys.html`, `teams/manage.html`
- **Code:** `window.__DATA__ = {...}` inline scripts (no CSP nonce)
- **Issue:** These inline scripts inject server-side data (including `site.token` — the API key) into the page. They violate CSP `script-src` policy, meaning either:
1. CSP is not enforced (security hole), OR
2. These scripts fail silently (functionality broken)
- **Risk:** If CSP is enforced → functionality breaks. If not enforced → XSS via inline scripts. The `site.token` being rendered directly in templates is especially dangerous.
- **Fix:** Use CSP nonces or move data to `<script type="application/json" id="page-data">` elements read by external JS.
---
## 🟠 HIGH (Should Fix — Significant Risk)
### H1 — Password Reset Token Reuse (Backend)
- **File:** `app/models_user.py`
- **Issue:** Token not cleared after successful password reset. Old reset link remains valid.
- **Fix:** `UPDATE users SET password_reset_token = NULL, password_reset_sent_at = NULL WHERE id = ?`
### H2 — Missing Rate Limit on Auth Endpoints (Backend)
- **File:** `app/routes/auth.py`
- **Issue:** `/api/password-reset` and `/api/magic-login` have no rate limiting.
- **Fix:** `@limiter.limit("3 per hour", key_func=get_client_ip)`
### H3 — CSRF Exempt on Public Submissions (Backend)
- **File:** `app/app.py:501-520`
- **Issue:** All submission endpoints exempt from CSRF. Any page can POST to your submission endpoint.
- **Mitigation:** Honeypot exists but add IP rate limiting on `/submit` endpoints.
### H4 — No File Upload Size Limit (Backend)
- **File:** `app/app.py`
- **Issue:** `MAX_CONTENT_LENGTH` not set → unlimited uploads.
- **Fix:** `app.config["MAX_CONTENT_LENGTH"] = 16 * 1024 * 1024 # 16 MB`
### H5 — Stripe Price IDs Default to Empty String (Backend)
- **File:** `app/models.py:98-103`
- **Issue:** Missing env vars → empty price IDs → checkout silently fails.
- **Fix:** Validate at startup, raise `RuntimeError` if missing.
### H6 — Circular Import Between Models (Backend)
- **File:** `app/models_user.py` ↔ `app/models.py`
- **Issue:** `models.py` line 172 re-exports from `models_user.py`, which imports back from `models.py`. Fragile import ordering.
- **Fix:** Move `try_decrypt_user_value` to `app/crypto_utils.py`.
### H7 — Duplicate Function Definitions (Backend)
- **Files:** `app/models.py:234` + `app/models_user.py`
- **Issue:** `get_user_tier` and `get_site_owner_tier` defined in BOTH modules. Shadowing risk.
- **Fix:** Remove duplicates from `models.py`.
### H8 — innerHTML with User Data (Frontend)
- **Files:** `app/static/js/form_builder.js:1287,1322`, `form_editor.js:1212,1240`
- **Issue:** `innerHTML` used with data that could contain user-submitted content. While current usage is template-based, the pattern is risky.
- **Fix:** Use `textContent` or `document.createElement()` for dynamic content.
### H9 — No File Upload Size Validation (Frontend)
- **Files:** `app/static/js/form_builder.js`, `form_editor.js`
- **Issue:** File upload fields have no client-side size validation. User can select multi-GB files → sent to server → waste bandwidth.
- **Fix:** Add `File.size` check before FormData submission with user-facing error.
### H10 — CSS Not Compiled from Source (Frontend)
- **File:** `app/static/css/style.css`
- **Issue:** 2,360+ lines of raw Tailwind utility classes. No PostCSS/Tailwind source file. No `tailwind.config.js`. Every class is manually written — no JIT compilation, no purge, no autocomplete.
- **Fix:** Set up `tailwind.config.js` + source `input.css` with `@tailwind` directives. Run `npx tailwindcss -i input.css -o style.css --minify`.
---
## 🟡 MEDIUM (Good to Fix — Maintainability/Performance)
### M1 — `models.py` Is 3,429-Line God Module (Backend)
- **Issue:** Despite 9 domain modules, `models.py` still has ~3,000 lines of active code. C2 refactor incomplete.
- **Fix:** Move remaining functions to domain modules. `models.py` should be < 50 lines.
### M2 — `print()` Instead of Logger (Backend)
- **Files:** `app/models.py:336`, `app/helpers.py:2102,2580` + more
- **Issue:** Production errors use `print()` instead of structured logging.
- **Fix:** Replace with `logging.getLogger(__name__).error(...)`.
### M3 — Dynamic SQL UPDATE Fragility (Backend)
- **File:** `app/models.py:484`
- **Issue:** Column names from `allowed` set injected into SQL. Fragile against future changes.
- **Fix:** Use `frozenset` constant for allowed fields with explicit validation.
### M4 — Missing DB Indexes (Backend)
- **Tables:** `api_keys.user_id`, `submissions.customer_email`, `form_actions.chain_id`
- **Fix:** Run CREATE INDEX statements.
### M5 — No Cascading Delete (Backend)
- **Issue:** Related records not cleaned on user/site deletion. GDPR non-compliance.
- **Fix:** Add `ON DELETE CASCADE` to FK constraints.
### M6 — No Database Backup (Backend)
- **Issue:** SQLite in Docker volume with no automated backup.
- **Fix:** Scheduled `sqlite3 .dump` to timestamped files.
### M7 — N+1 Query on Submission List (Backend)
- **File:** `app/routes/api.py`
- **Issue:** Each submission decrypted individually in Python loop.
- **Fix:** Batch decryption or decrypt only on render.
### M8 — No Transaction Boundaries (Backend)
- **File:** `app/routes/api.py`, `app/models_site.py`
- **Issue:** Multi-step operations have no transaction rollback on failure.
- **Fix:** Wrap in `BEGIN`/`COMMIT`/`ROLLBACK`.
### M9 — No Migration Version Tracking (Backend)
- **File:** `app/models_initdb.py:109-140`
- **Issue:** 37 migrations run sequentially every restart. Non-idempotent migrations will re-execute.
- **Fix:** Add `migration_versions` table.
### M10 — `helpers.py` Is 2,595-Line God Module (Backend)
- **Issue:** Spam detection, PII helpers, geo, email templates — all in one file.
- **Fix:** Split into domain modules.
### M11 — No API Versioning (Backend)
- **Issue:** Routes at `/api/` without version prefix. Breaking changes affect all clients.
- **Fix:** Prefix with `/api/v1/`.
### M12 — Inline Styles in Templates (Frontend)
- **Files:** `app/templates/sites.html:12`, `teams/analytics.html:15`
- **Issue:** Inline `<style>` blocks should be moved to `style.css`.
- **Fix:** Extract to CSS classes.
### M13 — No Client-Side Validation (Frontend)
- **Files:** `app/templates/settings.html`, `app/templates/edit_site.html`
- **Issue:** Form fields lack HTML5 validation attributes (`required`, `pattern`, `minlength`). All validation is server-side only.
- **Fix:** Add native validation attributes + JS validation for better UX.
### M14 — Missing Form Labels (Frontend)
- **Files:** `app/templates/login.html` (1 label), `register.html` (1 label)
- **Issue:** Login/register forms have minimal label coverage. Screen readers rely on labels.
- **Fix:** Wrap each input in `<label for="id">` or add `aria-label`.
### M15 — No Dark Mode Support (Frontend)
- **Issue:** CSS has no `prefers-color-scheme` support. Growing number of users expect dark mode.
- **Fix:** Add `@media (prefers-color-scheme: dark)` with inverted palette.
### M16 — CSP Delegates External Scripts but No Nonce (Frontend)
- **File:** `app/static/js/csp-delegate.js`
- **Issue:** CSP delegate script handles nonce-based execution, but the inline data injection scripts don't use it.
- **Fix:** Pass nonce from Flask via `csp_nonce` template variable to all inline scripts.
### M17 — fetch Without Error Handling (Frontend)
- **Files:** `app/static/js/api_keys.js`, `billing.js`, `form_analytics.js`, `hosted_submissions.js`
- **Issue:** Multiple `fetch()` calls have no `.catch()` handlers. Failed requests silently fail.
- **Fix:** Add `.catch(err => showError(err.message))` pattern.
### M18 — Console.log in Production (Frontend)
- **Files:** `api_keys.js`, `billing.js`, `form_builder.js`, `hosted_submissions.js`, `site_analytics.js`, `templates_browser.js`
- **Issue:** Debug `console.log` statements left in production JS.
- **Fix:** Remove or wrap in `if (DEBUG)` guard.
### M19 — No Loading States (Frontend)
- **Files:** `app/templates/hosted_form.html`
- **Issue:** Form submission button not disabled during submit. Double-click = double submission.
- **Fix:** Disable button on submit, show spinner.
### M20 — No Responsive Testing on Hosted Forms (Frontend)
- **File:** `app/templates/hosted_form.html`
- **Issue:** Hosted forms rendered via inline styles and Tailwind. No mobile viewport testing. Form fields may not be usable on small screens.
- **Fix:** Test hosted forms on 320px viewport, add `@media` breakpoints.
---
## 🟢 LOW (Nice to Have — Polish/Standards)
### L1 — No Test API Key Prefix (Backend)
- **File:** `app/models.py:105`
- **Issue:** Only `afk_live_` prefix. No `afk_test_` for test environments.
- **Fix:** Support both prefixes.
### L2 — OpenAPI Typo (Backend)
- **File:** `app/routes/api.py:226`
- **Issue:** `content-type` should be `Content-Type`.
- **Fix:** Capitalization fix.
### L3 — Missing Content-Type Validation (Backend)
- **Issue:** API endpoints accept POST without validating `Content-Type: application/json`.
- **Fix:** Add `@require_json` decorator.
### L4 — Migration Re-exports in `models.py` (Backend)
- **File:** `app/models.py:108-124`
- **Issue:** 37+ migration functions re-exported but never called from `models.py`.
- **Fix:** Remove dead re-exports.
### L5 — Dockerfile Python Version Mismatch (Backend)
- **File:** `Dockerfile:1`
- **Issue:** `FROM python:3.11-slim` but pip resolves to python3.13.
- **Fix:** Align base image with runtime.
### L6 — No Graceful Shutdown (Backend)
- **Files:** `worker.py`, `app/app.py`
- **Issue:** No `SIGTERM`/`SIGINT` handlers. Docker stop may drop in-flight tasks.
- **Fix:** Add signal handlers.
### L7 — `app/__init__.py` Empty (Backend)
- **Issue:** No package metadata or factory pattern.
- **Fix:** Add `__version__`, `__author__`.
### L8 — No Error Boundary on Form Builder (Frontend)
- **File:** `app/static/js/form_builder.js`
- **Issue:** 2,000+ line JS file with no error boundaries. Single runtime error = dead page.
- **Fix:** Wrap form builder init in try/catch with user-facing error message.
### L9 — No Keyboard Navigation on Form Builder (Frontend)
- **File:** `app/static/js/form_builder.js`
- **Issue:** Drag-and-drop form builder has no keyboard alternative. Screen reader users cannot use it.
- **Fix:** Add `tabindex`, `role="listbox"`, keyboard handlers (Enter/Space to add, Arrow to move).
### L10 — No Image Optimization (Frontend)
- **Issue:** No lazy loading, no WebP conversion, no responsive images.
- **Fix:** Add `loading="lazy"` to below-fold images, serve WebP.
### L11 — CSS !important Usage (Frontend)
- **File:** `app/static/css/style.css`
- **Issue:** 18 `!important` declarations — indicates specificity wars.
- **Fix:** Refactor to use proper specificity instead of `!important`.
### L12 — No Print Stylesheet (Frontend)
- **Issue:** No `@media print` styles. Print previews of forms/documents look broken.
- **Fix:** Add print stylesheet for form preview pages.
---
## Priority Matrix
| # | ID | Effort | Impact | Priority |
|---|-----|--------|--------|----------|
| 1 | C5 | Small | Critical | P0 |
| 2 | C2 | Medium | Critical | P0 |
| 3 | C3 | Small | Critical | P0 |
| 4 | C4 | Small | Critical | P0 |
| 5 | C1 | Medium | Critical | P0 |
| 6 | H10 | Medium | High | P1 |
| 7 | H8 | Small | High | P1 |
| 8 | H1 | Small | High | P1 |
| 9 | H2 | Small | High | P1 |
| 10 | H9 | Small | High | P1 |
| 11 | M13 | Small | Medium | P2 |
| 12 | M14 | Small | Medium | P2 |
| 13 | M17 | Small | Medium | P2 |
| 14 | M18 | Small | Medium | P2 |
| 15 | M19 | Small | Medium | P2 |
| 16 | M11 | Medium | Medium | P2 |
| 17 | M1 | Large | Medium | P3 |
| 18 | L9 | Medium | Low | P3 |
---
## Frontend Architecture Summary
### Template Structure
- **31 templates** extend `base.html` — ✅ Good inheritance pattern
- **admin_login.html**, **admin_templates.html** — standalone (no session/nav) — ✅ Correct
- **field_advanced.html** — partial (included) — ✅ Correct
- **hosted_form.html** — standalone (public, no auth) — ✅ Correct
### CSS
- Single file: `app/static/css/style.css` (2,360 lines, 77.2 KB)
- Tailwind utility classes (not compiled) — no `tailwind.config.js`
- 18 `!important` declarations — specificity debt
- No dark mode support
- No print stylesheet
### JavaScript
- **11 externalized JS files** in `app/static/js/` (41.4 KB total)
- Vanilla JS — no framework ✅
- CSP delegate pattern for nonce-based execution ✅
- `console.log` in production — ❌
- `fetch()` without `.catch()` — ❌
- `innerHTML` with user data — ⚠️
### Security
- CSP enforced via `Content-Security-Policy` header ✅
- `X-Frame-Options: SAMEORIGIN` ✅
- `X-Content-Type-Options: nosniff` ✅
- Inline scripts for data injection — CSP violation ❌
- `site.token` (API key) rendered in template — ⚠️
---
## Key Recommendations
### Immediate (This Week)
1. **Fix CSP inline script violation (C5)** — Use nonce or data attributes
2. **Add threading lock for SQLite (C2)** — Prevent data corruption
3. **Add Redis fail-open fallback (C3)** — Prevent auth bypass
4. **Fix timing oracle (C4)** — Prevent email enumeration
5. **Add Stripe webhook dedup (C1)** — Prevent replay attacks
### Short Term (This Sprint)
1. **Set up Tailwind compilation pipeline (H10)** — Proper `tailwind.config.js` + PostCSS
2. **Add client-side validation (H9, M13)** — HTML5 + JS
3. **Fix fetch error handling (M17)** — `.catch()` on all API calls
4. **Remove console.log from production (M18)** — Cleanup
5. **Add form labels for accessibility (M14)** — Screen reader support
### Medium Term (Next Release)
1. **Complete `models.py` extraction (M1)** — Split to domain modules
2. **Add API versioning (M11)** — `/api/v1/` prefix
3. **Add keyboard nav to form builder (L9)** — Full accessibility
4. **Add dark mode (M15)** — `prefers-color-scheme`
5. **Set up database backups (M6)** — Automated SQLite dumps
---
## Appendix A: Frontend File Inventory
| File | Size | Purpose |
|------|------|---------|
| `static/css/style.css` | 77.2 KB | Main stylesheet |
| `static/js/form_builder.js` | 16.3 KB | Drag-and-drop form builder |
| `static/js/form_editor.js` | 11.6 KB | Field configuration editor |
| `static/js/site_analytics.js` | 3.7 KB | Charts and metrics |
| `static/js/csp-delegate.js` | 1.4 KB | CSP nonce handling |
| `static/js/hosted_submissions.js` | 1.1 KB | Public form submission |
| `static/js/api_keys.js` | 1.1 KB | API key management |
| `static/js/billing.js` | 0.7 KB | Stripe checkout |
| `static/js/form_analytics.js` | 0.6 KB | Form-level analytics |
| `static/js/templates_browser.js` | 0.3 KB | Template gallery |
| `static/js/templates.js` | 0.3 KB | Template management |
---
## Appendix B: Rate Limit Coverage
| Endpoint | Rate Limited | Config |
|----------|-------------|--------|
| POST /api/register | ✅ Yes | 5/hour |
| POST /api/login | ✅ Yes | 5/minute |
| POST /api/password-reset | ❌ No | — |
| POST /api/magic-login | ❌ No | — |
| POST /api/api-keys | ❌ No | — |
| POST /submit | Partial | IP-based |
| GET /api/sites | ❌ No | — |
| GET /api/submissions | ❌ No | — |
| POST /api/stripe/checkout | ❌ No | — |
---
## Appendix C: Database Index Audit
### Existing
| Table | Index |
|-------|-------|
| submissions | `idx_submissions_site_id` |
| submissions | `idx_submissions_created` |
| sites | `idx_sites_user_id` |
| api_keys | `idx_api_keys_key_hash` |
| usage | `idx_usage_user_month` |
| form_analytics | `idx_form_analytics_site_date` |
| rate_limits | `idx_rate_limits_key_window` |
### Missing
| Table | Column | Priority |
|-------|--------|----------|
| api_keys | `user_id` | High |
| submissions | `customer_email` | Medium |
| form_actions | `chain_id` | Medium |
| invoice_schedules | `email_encrypted` | Medium |
| email_campaigns | `user_id` | Medium |
| teams | `user_id` | Medium |
| webhook_destinations | `site_id` | Medium |
---
## Appendix D: Auth Flow
```
Register: POST /api/register → validate → create user (bcrypt) → send verification email
Verify: GET /api/verify-email/{token} → validate → set email_verified=1
Login: POST /api/login → find_user → bcrypt.checkpw → create_session
Magic Login: POST /api/magic-login → find_user → generate token → send email
Magic Verify: GET /api/verify-magic/{token} → validate → create_session
Password Reset: POST /api/password-reset → find_user → generate token → send email
Password Reset Apply: POST /api/reset-password/{token} → validate → update password
API Key Auth: GET request → parse Bearer → validate_api_key() → set g.current_user
```