Installer avec Codex ou Claude Copiez ce prompt, collez-le dans Codex, Claude ou un autre assistant, puis laissez-le vérifier la page du skill et l'installer pour vous.
Une commande directe contourne le prompt de vérification. Examinez la source avant de l'exécuter.
Laravel security best practices — authentication, authorization, Eloquent safety, CSRF, XSS prevention, API security, and secure deployment configurations.
origin
Multiversal
Laravel Security Best Practices
Comprehensive security guidelines for Laravel applications to protect against common vulnerabilities.
When to Activate
Setting up Laravel authentication and authorization (Sanctum, Passport, Jetstream, Breeze)
Implementing user roles, permissions, and policies
Configuring production security settings and environment variables
Reviewing Laravel applications for security vulnerabilities
Deploying Laravel applications to production
Writing secure Eloquent queries and migrations
Production Configuration
Essential Production Settings
// config/app.php'env' => env('APP_ENV', 'production'),
'debug' => (bool) env('APP_DEBUG', false), // CRITICAL: Never true in production'key' => env('APP_KEY'), // Must be set: php artisan key:generate
=> (, ),
=> ,
=> ,
((())) {
();
}
// config/session.php
'secure'
env
'SESSION_SECURE_COOKIE'
true
'http_only'
true
'same_site'
'lax'
// Verify APP_KEY is set at boot
// bootstrap/app.php or a service provider
if
empty
config
'app.key'
throw
new
RuntimeException
'APP_KEY is not set. Run: php artisan key:generate'
Environment File Security
# NEVER commit .env to version control# .gitignore already includes .env by default# Use .env.example with placeholders instead
DB_PASSWORD=
APP_KEY=
SANCTUM_TOKEN_PREFIX=
# Validate required variables at boot
// In AppServiceProvider::boot()
$requiredKeys = ['app.key', 'database.connections.mysql.database', 'database.connections.mysql.username'];
foreach ($requiredKeys as $key) {
if (empty(config($key))) {
throw new RuntimeException("Missing required config key: {$key}");
}
}
HTTPS Enforcement
// AppServiceProvider::boot() or middlewareif (app()->environment('production')) {
URL::forceScheme('https');
request()->server->set('HTTPS', 'on');
}
// config/app.php for trusted proxies (load balancers)// Use specific IP ranges — * trusts all, allowing X-Forwarded-* spoofing// AWS: '10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16''trusted_proxies' => ['10.0.0.0/8', '172.16.0.0/12'],
// Force HTTPS in production via middleware// app/Http/Middleware/ForceHttps.phppublicfunctionhandle($request, Closure$next)
{
if (!$request->secure() && app()->environment('production')) {
returnredirect()->secure($request->getRequestUri());
}
return$next($request);
}
Authentication
Sanctum (API Token Authentication)
// config/sanctum.php'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
'%s%s',
'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
env('APP_URL') ? ',' . parse_url(env('APP_URL'), PHP_URL_HOST) : ''
)));
'expiration' => 60 * 24, // Token expiration in minutes (null = never)'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''),
// Issuing tokens with abilities$token = $user->createToken('api-token', ['read', 'write'])->plainTextToken;
// Validate abilities on routesRoute::middleware('auth:sanctum')->group(function () {
Route::get('/orders', function () {
// User must have 'read' abilityabort_unless(Auth::user()->tokenCan('read'), 403);
// ...
})->middleware('abilities:read');
Route::post('/orders', function () {
// User must have 'write' abilityabort_unless(Auth::user()->tokenCan('write'), 403);
// ...
})->middleware('abilities:write');
});
// BAD: $guarded = [] allows ALL columns to be mass-assigned// NEVER use $guarded = [] in production// GOOD: Whitelist fillable attributesfinalclassUserextendsAuthenticatable{
protected$fillable = [
'name',
'email',
'phone',
'avatar',
];
// NEVER add 'role', 'is_admin', 'is_verified' here
}
// GOOD: Explicitly control which fields can be filled in requestspublicfunctionstore(StoreUserRequest $request): RedirectResponse{
$user = User::create($request->safe()->only([
'name', 'email', 'phone', 'avatar'
]));
// $request->safe() uses validated data only// $request->only() is NOT safe on its own without validation rules
}
// BAD: Creating a user with request data directlyUser::create($request->all()); // VULNERABLE to mass assignment!// BETTER: Use DTOs for creation$user = User::create($request->validated()); // Only validated fields
SQL Injection Prevention
// GOOD: Eloquent automatically parameterizes queriesUser::where('email', $userInput)->first();
User::whereRaw('email = ?', [$userInput])->first();
// GOOD: Query Builder also parameterizes
DB::table('users')->where('email', $userInput)->first();
DB::select('SELECT * FROM users WHERE email = ?', [$userInput]);
// BAD: Raw string interpolation
DB::select("SELECT * FROM users WHERE email = '{$userInput}'"); // VULNERABLE!User::whereRaw("email = '{$userInput}'")->first(); // VULNERABLE!// BAD: whereRaw/orderByRaw with unescaped inputUser::orderByRaw($userInput); // VULNERABLE!User::groupByRaw($userInput); // VULNERABLE!// BAD: DB::statement with concatenation
DB::statement("INSERT INTO users (email) VALUES ('{$userInput}')"); // VULNERABLE!
finalclassUserextendsAuthenticatable{
// Hide sensitive attributes from JSON/API responsesprotected$hidden = [
'password',
'remember_token',
'two_factor_secret',
'two_factor_recovery_codes',
];
// Append only safe computed attributesprotected$appends = ['full_name']; // safe// NEVER append sensitive computed data
}
finalclassPostextendsModel{
// Global scope to filter soft deleted recordsuseSoftDeletes;
// Prevent N+1 by restricting lazy loading (optional strict mode)// AppServiceProvider::boot()// Model::preventLazyLoading(!app()->isProduction());
}
CSRF Protection
Default Protection
// Laravel CSRF is enabled by default via VerifyCsrfToken middleware// app/Http/Kernel.php (protected $middlewareGroups['web'])// All POST/PUT/PATCH/DELETE forms must include @csrf
<form method="POST" action="/posts">
@csrf
<input type="text" name="title">
<button type="submit">Create</button>
</form>
Excluding Routes (Carefully)
// app/Http/Middleware/VerifyCsrfToken.phpclassVerifyCsrfTokenextendsMiddleware{
// Only exclude routes that have external CSRF protection (webhooks, etc.)protected$except = [
'stripe/*', // Stripe webhooks use their own signature verification// Avoid blanket 'api/*' — stateful Sanctum routes need CSRF.// Exclude only specific stateless webhook/endpoint routes.
];
}
{{-- SAFE: Auto-escaped by Blade --}}
{{ $userInput }}
{{-- DANGEROUS: Raw output — NEVER use with user input --}}
{!! $userInput !!}
{{-- SAFE: Only use {!! !!} with trusted content you control --}}
{!! $trustedHtmlFromYourServer !!}
{{-- GOOD: Use specific escaping directives --}}
@js($data) {{-- JSON encode for JavaScript --}}
@json($data) {{-- JSON encode in templates --}}
{{-- BAD: Direct user input in raw HTML --}}
<div>{!! $user->bio !!}</div> {{-- VULNERABLE if user provides bio --}}
Safe HTML Handling
// When you must allow some HTML, use a whitelist approachuseHTMLPurifier; // Requires: composer require ezyang/htmlpurifierpublicfunctionsanitizeHtml(string$dirty): string{
$config = \HTMLPurifier_Config::createDefault();
$config->set('HTML.Allowed', 'p,b,i,a[href],ul,ol,li,br');
$config->set('URI.AllowedSchemes', ['http', 'https', 'mailto']);
$purifier = new\HTMLPurifier($config);
return$purifier->purify($dirty);
}
// In blade:
<div>{!! $sanitizedContent !!}</div> {{-- Safe after purification --}}
JavaScript Context Escaping
{{-- SAFE: Blade @js escapes for JavaScript context --}}
<script>
const user = @js($user); // JSON + escaped for JS context
const settings = @json($settings); // Direct JSON encode
</script>
{{-- DANGEROUS: Manual JSON in JS context --}}
<script>
const user = {{ json_encode($user) }}; // NOT escaped for JS!
</script>
// Store files outside public directory$path = $request->file('document')->store('documents', 'local');
// Never use 'public' disk for sensitive documents// Use signed URLs for temporary file accessuseIlluminate\Support\Facades\Storage;
publicfunctiondownload(Request $request, string$path)
{
// Generate temporary signed URL (expires in 15 minutes)$url = Storage::temporaryUrl($path, now()->addMinutes(15));
// Validate user has permission$this->authorize('download', $path);
returnredirect($url);
}
// Storage configuration for cloud with encryption// config/filesystems.php's3' => [
'driver' => 's3',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION'),
'bucket' => env('AWS_BUCKET'),
'url' => env('AWS_URL'),
'endpoint' => env('AWS_ENDPOINT'),
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
'throw' => false,
'server_side_encryption' => 'AES256', // Encrypt at rest
],
Dependencies and Secrets
Composer Security
# Always audit dependencies in CI
composer audit
# Pin major versions in composer.json"laravel/framework": "^11.0",
"spatie/laravel-permission": "^6.0"# Check for abandoned packages
composer why-not
# Keep lock file in version control (it pins exact versions)# Run `composer update` deliberately, never in CI/CD
Secret Management
# .env file (NEVER commit)# .gitignore includes .env by default
APP_KEY=base64:abc123...
DB_PASSWORD=secure_password
STRIPE_KEY=sk_live_...
SANCTUM_TOKEN_PREFIX=myapp_
# For production: Use a secret manager# Deploy with: env $(aws secretsmanager get-secret-value --secret-id prod/db | jq ...) php artisan serve# Validate secrets at boot (AppServiceProvider::boot)$secrets = ['services.stripe.key', 'services.stripe.webhook_secret'];
foreach ($secrets as $key) {
if (empty(config($key))) {
Log::critical("Missing secret: {$key}");
}
}
Queue Security
// Define a named rate limiter (typically in AppServiceProvider::boot())RateLimiter::for('payments', fn () =>Limit::perMinute(5));
// Encrypt sensitive job data by implementing the interfacefinalclassProcessPaymentJobimplementsShouldQueue, ShouldBeEncrypted{
useDispatchable, InteractsWithQueue, Queueable, SerializesModels;
publicfunction__construct(privatereadonlystring$paymentIntentId, // Public IDs are fine
privatereadonlystring$cardFingerprint, // Encrypted via ShouldBeEncrypted
) {}
publicfunctionhandle(): void{
// Process payment
}
// Limit retries and delay between attemptspublicfunctionretryUntil(): Carbon{
returnnow()->addMinutes(5);
}
// Rate limit how many jobs of this type can runpublicfunctionmiddleware(): array{
return [
newRateLimited('payments'),
];
}
}