| name | secure-coding-patterns |
| description | Secure coding patterns for PHP/Laravel. Input validation, output encoding, database security, error handling. Use when writing new code or reviewing for security. |
Secure Coding Patterns
Write secure code from the start. Prevention is cheaper than remediation.
When to Use
- Writing new features that handle user input
- Creating API endpoints
- Working with database operations
- Handling file uploads
- Implementing error handling
Core Principles
| Principle | Application |
|---|
| Never trust input | Validate everything from users, APIs, even database |
| Encode output | Context-aware encoding prevents XSS |
| Parameterize queries | Never concatenate SQL |
| Fail secure | On error, deny access |
| Least privilege | Minimum required permissions |
1. Input Validation
Laravel Form Requests (Always Use)
class StorePostRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()->can('create', Post::class);
}
public function rules(): array
{
return [
'title' => ['required', 'string', 'max:255'],
'body' => ['required', 'string', 'max:65535'],
'category_id' => ['required', 'exists:categories,id'],
'tags' => ['array', 'max:10'],
'tags.*' => ['string', 'max:50'],
];
}
}
Validation Patterns
'price' => ['required', 'numeric', 'min:0', 'max:999999.99'],
'quantity' => ['required', 'integer', 'min:1', 'max:1000'],
'status' => ['required', Rule::enum(PostStatus::class)],
// โ
Conditional validation
'company' => ['required_if:type,business', 'string', 'max:255'],
// โ
Custom validation with closure
'slug' => [
'required',
'string',
function ($attribute, $value, $fail) {
if (preg_match('/[^a-z0-9\-]/', $value)) {
$fail('The slug may only contain lowercase letters, numbers, and dashes.');
}
},
],
File Upload Validation
'avatar' => [
'required',
'image',
'mimes:jpeg,png,webp',
'max:2048',
'dimensions:min_width=100,min_height=100,max_width=2000,max_height=2000',
],
'document' => [
'required',
'file',
'mimes:pdf,doc,docx',
'max:10240',
],
2. Output Encoding (XSS Prevention)
Blade Escaping
{{ $userInput }}
{!! $trustedHtml !!}
{!! clean($userInput) !!}
Safe Patterns
<p>{{ $post->title }}</p>
<a href="{{ route('posts.show', $post) }}">View</a>
<script>
const data = @json($safeData);
</script>
<a href="{!! $userUrl !!}">
<a href="{{ Str::startsWith($url, ['http://', 'https://']) ? $url : '#' }}">
Livewire Considerations
<div>{{ $this->userInput }}</div>
<div contenteditable wire:model="content"></div>
<textarea wire:model="content"></textarea>
3. Database Security
Eloquent (Safe by Default)
User::where('email', $email)->first();
User::find($id);
Post::whereIn('id', $ids)->get();
DB::select('SELECT * FROM users WHERE email = ?', [$email]);
DB::select("SELECT * FROM users WHERE email = '$email'");
Raw Queries (When Needed)
$results = DB::select(
'SELECT * FROM posts WHERE status = :status AND user_id = :user',
['status' => 'published', 'user' => $userId]
);
Post::whereRaw('LOWER(title) LIKE ?', ['%' . strtolower($search) . '%'])->get();
Post::whereRaw("title LIKE '%$search%'")->get();
Mass Assignment Protection
class User extends Model
{
protected $fillable = [
'name',
'email',
'password',
];
}
User::create($request->validated());
User::create($request->all());
4. Error Handling Security
Production Error Messages
'debug' => env('APP_DEBUG', false),
class Handler extends ExceptionHandler
{
public function render($request, Throwable $e)
{
if ($request->expectsJson()) {
return response()->json([
'message' => 'An error occurred.',
], 500);
}
return parent::render($request, $e);
}
}
Logging Without Leaking
try {
$this->processPayment($order);
} catch (PaymentException $e) {
Log::error('Payment failed', [
'order_id' => $order->id,
'error' => $e->getMessage(),
// Never log: card numbers, CVV, passwords
]);
return back()->withErrors(['payment' => 'Payment could not be processed.']);
}
Never Log Sensitive Data
Log::info('Login attempt', ['password' => $password]);
Log::info('Payment', ['card' => $cardNumber]);
Log::info('Login attempt', ['email' => $email]);
Log::info('Payment', ['last4' => substr($cardNumber, -4)]);
5. Secrets Management
Environment Variables
$apiKey = config('services.stripe.key');
$apiKey = 'sk_live_xxxxx';
'stripe' => [
'key' => env('STRIPE_KEY'),
'secret' => env('STRIPE_SECRET'),
],
Validation at Boot
public function boot(): void
{
if (app()->isProduction()) {
$required = ['STRIPE_KEY', 'MAIL_PASSWORD', 'APP_KEY'];
foreach ($required as $key) {
if (empty(env($key))) {
throw new RuntimeException("Missing required env: $key");
}
}
}
}
6. Session Security
Secure Session Configuration
'secure' => env('SESSION_SECURE_COOKIE', true),
'http_only' => true,
'same_site' => 'lax',
'expire_on_close' => false,
'lifetime' => 120,
Session Regeneration
$request->session()->regenerate();
$request->session()->invalidate();
$request->session()->regenerateToken();
Quick Reference Checklist
Before Committing Code
File Uploads
Remember: Laravel provides excellent security defaults. Your job is to NOT break them.