Security & compliance
The mental model before you execute
Security on a CodeCanyon Laravel app is not one switch you flip — it is layers that each fail safely, plus the habit of never letting a secret touch your code. This guide builds that mental model so the security playbook phases make sense when you run them. It does not run commands for you; the playbook does that.
Why it matters
Section titled “Why it matters”A vendor app ships installed and “working”, which makes it easy to forget that you — not the vendor — own its production security posture. The three failure modes that actually bite solo founders are: a leaked secret (an API key in git, a .env exposed over HTTP), a single point of failure (you trusted one layer and it was misconfigured), and an unenforced compliance promise (your privacy policy says “GDPR compliant” but there’s no delete button). Each section below is the mental model that prevents one of those.
Defense in depth — never trust one layer
Section titled “Defense in depth — never trust one layer”The core principle: never rely on a single security layer. If one is bypassed or misconfigured, the others still protect you. Three layers wrap a Laravel app, from the outside in:
graph TD Browser["Client browser"] Browser -->|HTTPS| L3["Layer 3 — Edge / CDN<br/>(Cloudflare, optional)<br/>WAF · DDoS · rate limit · bot mgmt"] L3 --> L2["Layer 2 — Server<br/>(Apache/Nginx + .htaccess)<br/>SSL/TLS · HSTS · security headers · file protection"] L2 --> L1["Layer 1 — Application<br/>(Laravel + middleware)<br/>auth · CSRF · XSS · SQL-injection · encryption"] L1 --> App["Laravel app → database / files"]The order is deliberate — Layer 1 is the most reliable because it always runs with your PHP code, no matter how the server or CDN is configured. Layer 3 is an enhancement, not a replacement: if Cloudflare is bypassed by a direct-IP request, it gives zero protection. So the rule is always implement Layers 1 and 2; add Layer 3 when you need DDoS protection or global performance.
| Layer | What it does | If misconfigured |
|---|---|---|
| 1 — Application | CSRF tokens, Blade escaping, Eloquent, RBAC, encryption | Form hijacking, XSS, mass-assignment exploit |
| 2 — Server | HTTPS enforcement, HSTS, headers, .env protection | Downgrade attacks, leaked credentials |
| 3 — Edge/CDN | WAF, DDoS absorption, rate limiting, bot management | No edge protection (but app still safe) |
Layer 1 — application security (Laravel’s defaults, used correctly)
Section titled “Layer 1 — application security (Laravel’s defaults, used correctly)”Laravel ships most of this for free; the bugs come from bypassing the defaults. The OWASP Top 10 maps cleanly onto Laravel mechanisms:
| OWASP risk | Laravel protection |
|---|---|
| Broken access control | Gates, Policies, middleware (spatie/laravel-permission) |
| Cryptographic failures | encrypt(), Hash::make(), encrypted casts |
| Injection | Eloquent ORM / Query Builder (parameterized) |
| Security misconfiguration | APP_DEBUG=false in production |
| Vulnerable components | composer audit |
| Auth failures | request throttling, strong password rules |
| Logging failures | activity logging (spatie/laravel-activitylog) |
The non-negotiables a beginner most often gets wrong:
- Mass assignment. Always set
$fillable(whitelist) or$guarded. Never$guarded = []— that lets an attacker set any field, includingis_admin. - XSS. Use
{{ $var }}(escaped). Reserve{!! $var !!}(raw) for trusted content only, and never with user input. - SQL injection. Use Eloquent or Query Builder bindings. Never concatenate user input into a raw query string.
- Rate limiting. Throttle public endpoints, and throttle auth endpoints harder (
throttle:5,1on login/reset) to blunt brute-force. - Encryption. Cast sensitive columns as
encryptedso the value is never stored in plaintext — this matters before the first secret is ever written, because plaintext leaks into binlogs and backups permanently. - Sessions. In production:
SESSION_SECURE_COOKIE=true,http_onlyon, andsame_siteset tolax(the Laravel default ofnullships no CSRF protection and trips security scanners).
Layer 2 — SSL, HSTS, and security headers
Section titled “Layer 2 — SSL, HSTS, and security headers”Layer 2 protects requests before they reach Laravel. Two ideas do most of the work: forcing HTTPS, and sending the right response headers.
HSTS — teach the browser “HTTPS only”
Section titled “HSTS — teach the browser “HTTPS only””HSTS (HTTP Strict Transport Security) is a header that tells the browser to always use HTTPS for your domain, even if the user types http://. After the first visit, the browser rewrites http to https itself — no server round-trip, so a man-in-the-middle on coffee-shop WiFi can’t downgrade the connection and steal a login.
Two facts that confuse people:
- It’s browser-side, not server-side. The
max-agetimer resets on every visit, so a regular visitor effectively never loses protection. And you can always “unlock” users by sendingmax-age=0— they are never permanently locked out as long as your server is reachable. includeSubDomainsis a foot-gun if rushed. It forces HTTPS on every subdomain — and any subdomain without a valid certificate becomes unreachable.
The safe rollout is a progression, not a one-shot:
graph LR P1["Phase 1 · main domain only<br/>max-age=31536000 (1yr)"] --> P2["Phase 2 · add includeSubDomains<br/>after every subdomain has SSL"] P2 --> P3["Phase 3 · max-age=63072000 (2yr)<br/>+ submit to hstspreload.org"]When to add includeSubDomains | When to wait |
|---|---|
| Every subdomain has a valid, verified SSL certificate | Any docs. / api. / dev. subdomain still lacks SSL |
| You’ve run stable HTTPS for a few months | You’re deploying HSTS for the very first time |
Browsers cap max-age at 2 years (63072000); larger values are ignored. The preload directive is permanent and very hard to undo — only submit to hstspreload.org after 12+ months of stable HTTPS.
The security-header set
Section titled “The security-header set”These response headers each block a specific attack class. Set them all at once (in .htaccess mod_headers, or a Laravel middleware), not as a “future enhancement”:
| Header | Stops | Typical value |
|---|---|---|
Strict-Transport-Security | Protocol-downgrade / MITM | max-age=31536000 |
X-Frame-Options | Clickjacking (iframe overlay) | SAMEORIGIN |
X-Content-Type-Options | MIME-sniffing (image run as JS) | nosniff |
Referrer-Policy | Leaking sensitive URLs | strict-origin-when-cross-origin |
Permissions-Policy | Rogue camera/mic/geolocation | camera=(), microphone=(), geolocation=() |
Content-Security-Policy | XSS | start in -Report-Only, then enforce |
CSP is the one to add last and carefully — it can break a working site. Run it in report-only mode for a week or two, watch the violation reports, then switch to enforcing.
Verify with two different tools
Section titled “Verify with two different tools”These check different things — you need both:
| Tool | Checks | Doesn’t check |
|---|---|---|
SSL Labs (ssllabs.com/ssltest) | Certificate, TLS versions, ciphers, HSTS value | .htaccess syntax, whether the redirect works, .env exposure |
| SecurityHeaders.com | The application headers above (grade A–F) | TLS/cipher details |
A quick curl -I https://yourdomain.com confirms the HSTS header is actually being sent, and curl -I https://yourdomain.com/.env should return 403 Forbidden — if it returns 200, your secrets are exposed and that is a stop-everything incident.
Secrets & password management — keep keys out of code
Section titled “Secrets & password management — keep keys out of code”The highest-frequency real-world breach isn’t a clever exploit — it’s a key committed to git. Two habits prevent it.
Never commit secrets. .env and every .env.* is gitignored; .env.example holds placeholders only. Config reads secrets via env() at config-load time, never as inline literals in app code. Before every commit, scan your own diff for the give-away tokens — APP_KEY=, DB_PASSWORD, sk_, pk_live_, *_SECRET, AKIA, Bearer . If one ever ships, treat the credential as compromised: rotate it at the provider first, then scrub history.
Use a real password/secrets manager rather than a notes file or browser autofill. For developer work the must-haves are zero-knowledge encryption (the provider can never see your data), passkey + 2FA support, and — critically for a deploy pipeline — SSH-key storage, a CLI, and CI/CD secret injection. The practical picks:
| Pick when you want… | Tool | Why |
|---|---|---|
| Best developer workflow (SSH agent, CLI, CI/CD) | 1Password | Built-in SSH agent (keys never touch disk), op CLI, official GitHub/GitLab actions, IDE op:// references |
| Budget + open-source + self-hosting | Bitwarden | Free unlimited tier, self-host via Vaultwarden, bw CLI — but no native SSH agent |
| Beginner + real-time breach alerts | NordPass | XChaCha20, real-time dark-web monitoring — but no SSH keys or dev CLI, so not for deploy workflows |
The pattern that pays off: store secrets in the manager, reference them by path in .env (e.g. API_KEY=op://Development/API/key), and run the app with the manager injecting them at runtime (op run -- npm run dev). The committed .env then contains references, not values — safe to share.
Compliance — what each framework actually requires
Section titled “Compliance — what each framework actually requires”Compliance is mostly policy + evidence + a few technical features, not a single piece of code. Don’t pursue a framework you don’t need. Pick by your data and your customers:
graph TD Q1{"Do you handle medical<br/>or health data (PHI)?"} Q1 -->|Yes| HIPAA["HIPAA<br/>(+ BAAs, safeguards)"] Q1 -->|No| Q2{"Do you process data<br/>of EU residents?"} Q2 -->|Yes| GDPR["GDPR<br/>(rights + consent + DPAs)"] Q2 -->|No| Q3{"Do enterprise clients<br/>ask for a security report?"} Q3 -->|Yes| SOC2["SOC 2 Type II"] Q3 -->|No| Base["Baseline security only<br/>(Layers 1 + 2 + secrets hygiene)"]| Framework | Trigger | Core technical asks |
|---|---|---|
| SOC 2 | Enterprise clients want assurance | MFA, RBAC, centralized logging, tested backups, incident-response plan, 3–12 months of evidence (Type II) |
| GDPR | You process EU residents’ data | Data export + account deletion + consent logging, encryption, ROPA, signed DPAs with every vendor, 72-hour breach notice |
| HIPAA | You handle PHI (health data) | PHI encrypted at rest + transit, unique user IDs, auto-logoff, full PHI-access audit logging, signed BAAs with every vendor |
A few orienting facts so you size the effort correctly:
- SOC 2 has five Trust Service Criteria; only Security is always required. SaaS typically adds Availability and Confidentiality. Type II (controls working over 3–12 months) is what enterprises actually ask for — Type I is just a point-in-time design snapshot. Automation platforms (Drata, Vanta, Secureframe, Sprinto) cut the evidence-collection grind dramatically.
- GDPR is the one most solo apps genuinely need, and it’s mostly buildable: a data-export endpoint (Right to Portability), an account-deletion/anonymize flow (Right to Erasure), consent logged with a timestamp + IP, and a
ROPAtable documenting what you collect and why. You can only truthfully claim “GDPR compliant” once those features and the privacy policy actually exist. - HIPAA is significant extra cost and work — only pursue it if you handle PHI. If your SaaS has no health data and no healthcare clients, you don’t need it. When you do, the BAA (Business Associate Agreement) with every vendor that touches PHI is mandatory and non-optional.
Where you actually execute this
Section titled “Where you actually execute this”This guide is the why. The hands-on steps — adding the headers, wiring activity logging, setting up backups, and the GDPR/SOC 2/HIPAA tracks — live in the security playbook phase, run in this order so secrets are encrypted before any secret is written: