Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Laravel security best practices for authn/authz, validation, CSRF, mass assignment, file uploads, secrets, rate limiting, and secure deployment.
.claude/skills/loulanyue-laravel-security/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 49% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 137% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 38% | 0% |
| case-04 | ✓→✓ | = Same ✓ | 93% | 0% |
| case-16 | ✓→✓ | = Same ✓ | 58% | 0% |
Comprehensive security guidance for Laravel applications to protect against common vulnerabilities.
VerifyCsrfToken, security headers via SecurityHeaders).auth:sanctum, $this->authorize, policy middleware).UploadInvoiceRequest) before it reaches services.RateLimiter::for('login')) alongside auth controls.URL::temporarySignedRoute + signed middleware).APP_DEBUG=false in productionAPP_KEY must be set and rotated on compromiseSESSION_SECURE_COOKIE=true and SESSION_SAME_SITE=lax (or strict for sensitive apps)SESSION_HTTP_ONLY=true to prevent JavaScript accessSESSION_SAME_SITE=strict for high-risk flowsExample route protection:
phpuse Illuminate\Http\Request; use Illuminate\Support\Facades\Route; Route::middleware('auth:sanctum')->get('/me', function (Request $request) { return $request->user(); });
Hash::make() and never store plaintextphpuse Illuminate\Support\Facades\Hash; use Illuminate\Validation\Rules\Password; $validated = $request->validate([ 'password' => ['required', 'string', Password::min(12)->letters()->mixedCase()->numbers()->symbols()], ]); $user->update(['password' => Hash::make($validated['password'])]);
php$this->authorize('update', $project);
Use policy middleware for route-level enforcement:
phpuse Illuminate\Support\Facades\Route; Route::put('/projects/{project}', [ProjectController::class, 'update']) ->middleware(['auth:sanctum', 'can:update,project']);
$fillable or $guarded and avoid Model::unguard()phpDB::select('select * from users where email = ?', [$email]);
{{ }}){!! !!} only for trusted, sanitized HTMLVerifyCsrfToken middleware enabled@csrf in forms and send XSRF tokens for SPA requestsFor SPA authentication with Sanctum, ensure stateful requests are configured:
php// config/sanctum.php 'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', 'localhost')),
phpfinal class UploadInvoiceRequest extends FormRequest { public function authorize(): bool { return (bool) $this->user()?->can('upload-invoice'); } public function rules(): array { return [ 'invoice' => ['required', 'file', 'mimes:pdf', 'max:5120'], ]; } }
php$path = $request->file('invoice')->store( 'invoices', config('filesystems.private_disk', 'local') // set this to a non-public disk );
throttle middleware on auth and write endpointsphpuse Illuminate\Cache\RateLimiting\Limit; use Illuminate\Http\Request; use Illuminate\Support\Facades\RateLimiter; RateLimiter::for('login', function (Request $request) { return [ Limit::perMinute(5)->by($request->ip()), Limit::perMinute(5)->by(strtolower((string) $request->input('email'))), ]; });
Use encrypted casts for sensitive columns at rest.
phpprotected $casts = [ 'api_token' => 'encrypted', ];
Example middleware to set headers:
phpuse Illuminate\Http\Request; use Symfony\Component\HttpFoundation\Response; final class SecurityHeaders { public function handle(Request $request, \Closure $next): Response { $response = $next($request); $response->headers->add([ 'Content-Security-Policy' => "default-src 'self'", 'Strict-Transport-Security' => 'max-age=31536000', // add includeSubDomains/preload only when all subdomains are HTTPS 'X-Frame-Options' => 'DENY', 'X-Content-Type-Options' => 'nosniff', 'Referrer-Policy' => 'no-referrer', ]); return $response; } }
config/cors.phpphp// config/cors.php return [ 'paths' => ['api/*', 'sanctum/csrf-cookie'], 'allowed_methods' => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'], 'allowed_origins' => ['https://app.example.com'], 'allowed_headers' => [ 'Content-Type', 'Authorization', 'X-Requested-With', 'X-XSRF-TOKEN', 'X-CSRF-TOKEN', ], 'supports_credentials' => true, ];
phpuse Illuminate\Support\Facades\Log; Log::info('User updated profile', [ 'user_id' => $user->id, 'email' => '[REDACTED]', 'token' => '[REDACTED]', ]);
composer audit regularlyUse signed routes for temporary, tamper-proof links.
phpuse Illuminate\Support\Facades\URL; $url = URL::temporarySignedRoute( 'downloads.invoice', now()->addMinutes(15), ['invoice' => $invoice->id] );
phpuse Illuminate\Support\Facades\Route; Route::get('/invoices/{invoice}/download', [InvoiceController::class, 'download']) ->name('downloads.invoice') ->middleware('signed');
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 14,167 | 10,893 | -23% | 1 | 1 | 0% | 2,706 | 4,023 | +49% | 0 | 0 | — |
case-02 | pass→pass | 10,301 | 3,851 | -63% | 1 | 1 | 0% | 1,917 | 2,638 | +38% | 0 | 0 | — |
case-03 | fail→fail | 13,425 | 6,047 | -55% | 1 | 1 | 0% | 2,602 | 3,122 | +20% | 0 | 0 | — |
case-04 | pass→pass | 10,639 | 9,032 | -15% | 1 | 1 | 0% | 1,856 | 3,577 | +93% | 0 | 0 | — |
case-05 | fail→pass | 7,656 | 6,192 | -19% | 1 | 1 | 0% | 1,225 | 2,907 | +137% | 0 | 0 | — |
case-16 | pass→pass | 11,829 | 7,487 | -37% | 1 | 1 | 0% | 2,029 | 3,207 | +58% | 0 | 0 | — |
case-06 | pass→pass | 12,716 | 5,165 | -59% | 1 | 1 | 0% | 2,098 | 2,809 | +34% | 0 | 0 | — |
case-07 | pass→pass | 12,541 | 7,056 | -44% | 1 | 1 | 0% | 2,340 | 3,161 | +35% | 0 | 0 | — |
case-08 | pass→pass | 10,606 | 5,378 | -49% | 1 | 1 | 0% | 2,027 | 2,858 | +41% | 0 | 0 | — |
case-09 | fail→fail | 12,687 | 4,952 | -61% | 1 | 1 | 0% | 2,212 | 2,724 | +23% | 0 | 0 | — |
case-10 | pass→pass | 10,578 | 6,329 | -40% | 1 | 1 | 0% | 1,966 | 2,993 | +52% | 0 | 0 | — |
case-11 | pass→pass | 6,209 | 4,385 | -29% | 1 | 1 | 0% | 1,154 | 2,686 | +133% | 0 | 0 | — |
case-12 | pass→pass | 9,050 | 4,813 | -47% | 1 | 1 | 0% | 1,563 | 2,662 | +70% | 0 | 0 | — |
case-13 | pass→pass | 13,421 | 5,321 | -60% | 1 | 1 | 0% | 2,247 | 2,833 | +26% | 0 | 0 | — |
case-14 | pass→pass | 5,536 | 3,100 | -44% | 1 | 1 | 0% | 829 | 2,116 | +155% | 0 | 0 | — |
case-15 | pass→pass | 3,784 | 2,465 | -35% | 1 | 1 | 0% | 525 | 2,211 | +321% | 0 | 0 | — |
case-17 | pass→pass | 7,115 | 3,598 | -49% | 1 | 1 | 0% | 1,370 | 2,558 | +87% | 0 | 0 | — |
case-18 | pass→pass | 6,690 | 4,369 | -35% | 1 | 1 | 0% | 1,118 | 2,530 | +126% | 0 | 0 | — |
case-19 | pass→pass | 3,790 | 2,797 | -26% | 1 | 1 | 0% | 629 | 2,328 | +270% | 0 | 0 | — |
case-20 | pass→pass | 12,769 | 11,024 | -14% | 1 | 1 | 0% | 2,671 | 4,135 | +55% | 0 | 0 | — |
case-21 | pass→pass | 12,738 | 12,648 | -1% | 1 | 1 | 0% | 2,486 | 4,461 | +79% | 0 | 0 | — |
case-22 | pass→pass | 17,277 | 14,320 | -17% | 1 | 1 | 0% | 3,576 | 4,692 | +31% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +9 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.