Secure Public Forms in Laravel: A Production Threat Model
Secure Public Forms in Laravel: A Production Threat Model
Form security does not end when validation passes. A harmless-looking answer can become stored XSS in an admin panel, a spreadsheet formula in a CSV export, sensitive data in a log, or a payload sent to an attacker-controlled webhook destination.
The useful model is to follow one hostile submission across six trust boundaries and attach a regression test to every control.
1. Form definition to runtime schema
Treat a database-backed form schema as configuration, never executable code. Allowlist field types, rule identifiers, option-source keys, and condition operators. Bound field count, nesting, string length, option count, uploads, and expensive pattern matching.
Do not store arbitrary PHP, Blade, JavaScript, class names, SQL, or free-form regular expressions for ordinary form authors. The server should compile a small reviewed language. Build validation from the published trusted schema, not from field definitions submitted by the visitor.
2. Browser to submission
Client validation improves feedback but is bypassable. Validate and authorize on the server, allowlist request keys, and avoid mass-assigning the raw request into a model.
CSRF and spam solve different problems. Laravel request-forgery protection defends the authority attached to a browser/session; it does not prove an anonymous visitor is human. Use layered bot controls, reasonable per-form/actor rate limits, and risk-based CAPTCHA or Turnstile where needed. Per-IP-only limits can punish offices or carrier NAT while distributed bots rotate addresses.
The Laravel spam-control guide covers that layer in depth. Laravel's current CSRF documentation explains the session boundary.
3. Stored answers to every output context
Blade escaping should remain the default. A stored <script> string must display as text in the admin panel and email preview unless rich HTML is an explicit, sanitized feature.
HTML, email, Slack/Teams markup, PDF, logs, and CSV are different output contexts. Encode for the destination.
CSV/formula injection deserves special attention: spreadsheet applications can interpret cells beginning with formula markers such as =, +, -, or @. There is no universal neutralization compatible with every spreadsheet and downstream import. Define your export contract, escape or prefix dangerous values according to it, and test separators, tabs, newlines, and quote tricks. Review OWASP's CSV injection guidance.
Do not log raw form payloads, tokens, signatures, or uploaded document contents. Log identifiers, outcomes, durations, and redacted error classes.
4. Upload to private retrieval
Allowlist extensions and detected media types, generate storage names, cap count/size/total request bytes, and store outside the public webroot or on a private disk by default. Browser Content-Type and MIME validation do not prove a document is safe.
Higher-risk PDF, DOCX, image, or archive workflows may need malware scanning or content disarm/reconstruction. Protect parsers from decompression bombs and resource exhaustion. Authorize every download and prefer short-lived temporary access.
Use the full file-upload checklist for implementation details and OWASP's upload guidance for the broader threat model.
5. Form configuration to outbound webhooks
User-configured destinations create server-side request forgery risk. Restrict schemes, resolve and reject loopback/private/link-local/metadata addresses for IPv4 and IPv6, disable redirects or revalidate every redirect, and account for DNS rebinding between validation and connection.
Use short connect/total timeouts, response-size caps, and queues so a destination cannot hold the visitor request open. Encrypt and mask integration secrets; keep them out of audit diffs and logs. OWASP maintains a dedicated SSRF prevention guide.
6. Availability and privileged workflows
Enforce active state, publishing windows, tenant ownership, and capacity on the server. Hiding the form or button is not a lifecycle rule.
Put cheap bounds before expensive parsing and storage. Queue notifications, exports, and integrations. Protect individual submissions, bulk exports, secret changes, and retention actions with separate policies and audit events. An authenticated insider or compromised account can cause more damage than a spam bot.
Threat/control/test matrix
| Threat | Control | Regression test |
|---|---|---|
| Unknown payload keys | Allowlisted validated data | Extra key never persists |
| Stored XSS | Context escaping | Script displays as text |
| Spreadsheet formula | Explicit CSV contract | Formula markers stay inert |
| Malicious/oversized file | Bounds, private storage, scanning | Mismatch and size cases fail |
| Cross-tenant selection | Scoped query and validation | Forged tenant ID fails |
| Webhook SSRF | Public-address validation and egress controls | Local/metadata/redirect cases fail |
| Duplicate/replayed submit | Idempotency ledger | Two requests create one effect |
| Log disclosure | Redaction | Payload and secrets absent |
What not to promise
No checklist makes an application “secure” or “OWASP compliant.” Controls depend on deployed versions, infrastructure, data, and operations. Self-hosting changes who owns security and incident response; it does not automatically improve them.
The strongest article—or implementation—pairs every boundary with evidence: a policy, a deliberately hostile input, and a test proving the result. Validation is the beginning of that chain, not the end.