PHP Security Patterns
When to Use
Reviewing or writing PHP/Laravel code that touches raw SQL, user input, authentication, file uploads, or serialized data.
Core Patterns
Prepared Statements, Never Interpolation
// BAD: string interpolation — classic SQL injection
$pdo->query("SELECT * FROM users WHERE email = '{$email}'");
// GOOD: bound parameters
$stmt = $pdo->prepare('SELECT * FROM users WHERE email = :email');
$stmt->execute(['email' => $email]);
Eloquent/query builder methods bind parameters automatically — the risk appears when developers drop to DB::select()/DB::statement() with raw strings.
// BAD
DB::select("SELECT * FROM orders WHERE status = '$status'");
// GOOD — still parameterized even in raw SQL
DB::select('SELECT * FROM orders WHERE status = ?', [$status]);
Mass-Assignment Protection
// BAD: everything in the request can set model attributes, including is_admin
class User extends Model {
protected $guarded = [];
}
// GOOD: explicit allow-list
class User extends Model {
protected $fillable = ['name', 'email', 'password'];
}
Never call ->fill($request->all()) or User::create($request->all()) — always pass $request->validated() from a FormRequest.
Password Hashing
// Storing
$hashed = password_hash($plainPassword, PASSWORD_BCRYPT);
// Laravel:
$hashed = Hash::make($plainPassword);
// Verifying — constant-time comparison built in, never use ===
if (password_verify($plainPassword, $hashed)) { /* ... */ }
if (Hash::check($plainPassword, $hashed)) { /* ... */ }
Never roll a custom hash (md5, sha1) or store plaintext. Rehash on login if password_needs_rehash() returns true after a cost-factor bump.
Blade Output Escaping
{{-- GOOD: auto-escaped, safe by default --}}
<p>{{ $comment->body }}</p>
{{-- DANGEROUS: raw output, only for trusted/pre-sanitized HTML --}}
<div>{!! $trustedCmsContent !!}</div>
If user input must render as HTML, sanitize it first with a vetted library (e.g. HTMLPurifier) — never {!! !!} a raw user-supplied field.
CSRF Protection
<form method="POST" action="/orders">
@csrf
...
</form>
// API routes using tokens/sessions still need CSRF unless purely stateless
// with a Bearer token — verify VerifyCsrfToken middleware covers all
// state-changing routes that use cookie-based auth.
Safe Deserialization
// BAD: unserialize() on user input can trigger PHP object injection
$data = unserialize($_COOKIE['data']);
// GOOD: use a data-only format
$data = json_decode($_COOKIE['data'], true, flags: JSON_THROW_ON_ERROR);
File Upload Validation
$request->validate([
'avatar' => ['required', 'file', 'image', 'max:2048', 'mimes:jpg,png,webp'],
]);
// Store outside the public webroot when possible, or with a randomized name
$path = $request->file('avatar')->store('avatars', 'private');
Never trust the client-supplied MIME type or extension alone — validate the actual file content (image rule inspects real image data).
Checklist
- No raw SQL string interpolation anywhere in the codebase
- Models use
$fillable, never$guarded = [] - Passwords hashed with
password_hash/Hash::make, never a fast general-purpose hash -
{!! !!}/ raw HTML output only used on sanitized or trusted content - CSRF middleware active on all cookie-authenticated state-changing routes
-
unserialize()never called on user-controlled input - File uploads validated by content type and size, stored outside public path when sensitive
- Secrets loaded from
.env/config(), never hardcoded
Anti-Patterns
// BAD: trusting $_GET/$_POST directly in a redirect (open redirect)
header('Location: ' . $_GET['next']);
// GOOD: validate against an allow-list of known internal paths
$next = in_array($request->get('next'), $allowedRedirects, true)
? $request->get('next')
: '/dashboard';
header("Location: {$next}");
Quick Reference
| Risk | Mitigation |
|---|---|
| SQL injection | Prepared statements / query builder binding |
| Mass assignment | $fillable allow-list + FormRequest validation |
| XSS | Blade {{ }} auto-escaping; sanitize before {!! !!} |
| CSRF | @csrf + VerifyCsrfToken middleware |
| Object injection | json_decode instead of unserialize |
| Weak password storage | password_hash / Hash::make (bcrypt/argon2) |
See Also
skills/php-ecosystem/laravel-patterns.mdskills/php-ecosystem/php-database.mdskills/security/owasp-checklist.md