FreeGitHubCoding
Laravel Security
Laravel security best practices — authentication, authorization, Eloquent safety, CSRF, XSS prevention, API security, and secure deployment configurations. Use when reviewing Laravel auth, Eloquent safety, CSRF, XSS, API security, or deployment configuration.
affaan-m on GitHub
Curated by PromptsEdge from a public repo · MIT license. All credit goes to the author.
What is this skill?
Comprehensive security guidelines for Laravel applications to protect against common vulnerabilities.
When to use
- 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
php
// 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
bash
# 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
php
// AppServiceProvider::boot() or middleware
if (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.php
public function handle($request, Closure $next)
{
if (!$request->secure() && app()->environment('production')) {
return redirect()->secure($request->getRequestUri());
}
return $next($request);
}
Authentication
Sanctum (API Token Authentication)
php
// 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 routes
Route::middleware('auth:sanctum')->group(function () {
Route::get('/orders', function () {
// User must have 'read' ability
abort_unless(Auth::user()->tokenCan('read'), 403);
// ...
})->middleware('abilities:read');
Route::post('/orders', function () {
// User must have 'write' ability
abort_unless(Auth::user()->tokenCan('write'), 403);
// ...
})->middleware('abilities:write');
});
Password Security
php
// config/hashing.php
// Default is bcrypt. Argon2id is stronger.
'bcrypt' => [
'rounds' => env('BCRYPT_ROUNDS', 12), // Increase for stronger hashing
],
'argon' => [
'memory' => 65536,
'threads' => 4,
'time' => 4,
],
// Password validation in RegisterRequest
public function rules(): array
{
return [
'password' => [
'required',
'confirmed',
Password::min(12)
->letters()
->mixedCase()
->numbers()
->symbols()
->uncompromised(), // Checks haveibeenpwned
],
];
}
// Rate limit login attempts
// App\Http\Controllers\Auth\AuthenticatedSessionController
protected function authenticated(Request $request, $user)
{
if ($user->wasRecentlyLockedOut()) {
// Notify user of suspicious login
$user->notify(new SuspiciousLoginNotification($request->ip()));
}
}
Session Management
php
// config/session.php
'driver' => env('SESSION_DRIVER', 'database'), // database/redis > file
'lifetime' => env('SESSION_LIFETIME', 120),
'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false),
'encrypt' => env('SESSION_ENCRYPT', false),
// Regenerate session on login
// App\Http\Controllers\Auth\AuthenticatedSessionController
public function store(LoginRequest $request): RedirectResponse
{
$request->authenticate();
$request->session()->regenerate(); // CRITICAL: prevents session fixation
return redirect()->intended(RouteServiceProvider::HOME);
}
// Invalidate session on logout
public function destroy(Request $request): RedirectResponse
{
Auth::guard('web')->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect('/');
}
Authorization
Gates
php
// App\Providers\AuthServiceProvider
use App\Models\Post;
use App\Models\User;
use Illuminate\Support\Facades\Gate;
public function boot(): void
{
Gate::define('update-post', function (User $user, Post $post): bool {
return $user->id === $post->user_id;
});
Gate::define('publish-post', function (User $user): bool {
return $user->role === 'editor' || $user->role === 'admin';
});
// Using before() for super-admin override
Gate::before(function (User $user, string $ability): ?bool {
if ($user->role === 'super-admin') {
return true; // Grants all abilities
}
return null; // Fall through to normal checks
});
}
// Usage in controllers
public function update(Request $request, Post $post): RedirectResponse
{
Gate::authorize('update-post', $post);
// Or: $this->authorize('update-post', $post);
// Or: abort_unless(Auth::user()->can('update-post', $post), 403);
// ...
}
Policies
php
// App\Policies\PostPolicy
class PostPolicy
{
use HandlesAuthorization;
public function viewAny(?User $user): bool
{
return true; // Public listing
}
public function view(?User $user, Post $post): bool
{
return $post->is_published || ($user && $user->id === $post->user_id);
}
public function create(User $user): bool
{
return $user->hasVerifiedEmail(); // Must verify email first
}
public function update(User $user, Post $post): bool
{
return $user->id === $post->user_id;
}
public function delete(User $user, Post $post): bool
{
return $user->id === $post->user_id && $post->created_at->diffInDays(now()) <= 30;
}
public function restore(User $user, Post $post): bool
{
return $user->role === 'admin';
}
public function forceDelete(User $user, Post $post): bool
{
return $user->role === 'super-admin';
}
}
// Register in AuthServiceProvider
protected $policies = [
Post::class => PostPolicy::class,
];
// Controller usage
public function show(Post $post): View
{
$this->authorize('view', $post);
return view('posts.show', compact('post'));
}
// Blade usage
@can('update', $post)
<a href="{{ route('posts.edit', $post) }}">Edit</a>
@endcan
@cannot('update', $post)
<span>You cannot edit this post</span>
@endcannot
Middleware Authorization
php
// Using middleware in routes
Route::put('/posts/{post}', [PostController::class, 'update'])
->middleware('can:update,post');
Route::get('/posts/create', [PostController::class, 'create'])
->middleware('can:create,App\Models\Post');
// Custom authorization middleware
// app/Http/Middleware/CheckRole.php
class CheckRole
{
public function handle(Request $request, Closure $next, string $role): mixed
{
if (!$request->user() || $request->user()->role !== $role) {
abort(403, 'Unauthorized. This area requires role: ' . $role);
}
return $next($request);
}
}
// Register in Kernel
protected $routeMiddleware = [
'role' => \App\Http\Middleware\CheckRole::class,
];
// Route usage
Route::middleware(['auth', 'role:admin'])->group(function () {
Route::get('/admin', [AdminController::class, 'index']);
});
Eloquent Security
Mass Assignment Protection
php
// BAD: $guarded = [] allows ALL columns to be mass-assigned
// NEVER use $guarded = [] in production
// GOOD: Whitelist fillable attributes
final class User extends Authenticatable
{
protected $fillable = [
'name',
'email',
'phone',
'avatar',
];
// NEVER add 'role', 'is_admin', 'is_verified' here
}
// GOOD: Explicitly control which fields can be filled in requests
public function store(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 directly
User::create($request->all()); // VULNERABLE to mass assignment!
// BETTER: Use DTOs for creation
$user = User::create($request->validated()); // Only validated fields
SQL Injection Prevention
php
// GOOD: Eloquent automatically parameterizes queries
User::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 input
User::orderByRaw($userInput); // VULNERABLE!
User::groupByRaw($userInput); // VULNERABLE!
// BAD: DB::statement with concatenation
DB::statement("INSERT INTO users (email) VALUES ('{$userInput}')"); // VULNERABLE!
Attribute Casting
php
final class User extends Authenticatable
{
protected $casts = [
'email_verified_at' => 'datetime',
'is_admin' => 'boolean', // Cast to boolean prevents string injection
'settings' => 'array', // Automatically json_encode/json_decode
'metadata' => 'encrypted:array', // Laravel 11+ encrypted casting
'password' => 'hashed', // Laravel 10+ auto-hashes on set
];
}
Model Security
php
final class User extends Authenticatable
{
// Hide sensitive attributes from JSON/API responses
protected $hidden = [
'password',
'remember_token',
'two_factor_secret',
'two_factor_recovery_codes',
];
// Append only safe computed attributes
protected $appends = ['full_name']; // safe
// NEVER append sensitive computed data
}
final class Post extends Model
{
// Global scope to filter soft deleted records
use SoftDeletes;
// Prevent N+1 by restricting lazy loading (optional strict mode)
// AppServiceProvider::boot()
// Model::preventLazyLoading(!app()->isProduction());
}
CSRF Protection
Default Protection
php
// 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)
php
// app/Http/Middleware/VerifyCsrfToken.php
class VerifyCsrfToken extends Middleware
{
// 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.
];
}
CSRF with JavaScript
html
<meta name="csrf-token" content="{{ csrf_token() }}">
<script>
// Axios example (Laravel ships with Axios)
axios.defaults.headers.common['X-CSRF-TOKEN'] = document.querySelector(
'meta[name="csrf-token"]'
).getAttribute('content');
// Fetch example
fetch('/posts', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content'),
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
</script>
XSS Prevention
Blade Templating Security
blade
{{-- 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
php
// When you must allow some HTML, use a whitelist approach
use HTMLPurifier; // Requires: composer require ezyang/htmlpurifier
public function sanitizeHtml(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
blade
{{-- 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>
HTTP Headers for XSS Protection
php
// App\Http\Middleware\SecurityHeaders.php
class SecurityHeaders
{
public function handle(Request $request, Closure $next): mixed
{
$response = $next($request);
$response->headers->set('X-Content-Type-Options', 'nosniff');
$response->headers->set('X-Frame-Options', 'DENY');
$response->headers->set('X-XSS-Protection', '1; mode=block');
$response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');
$response->headers->set(
'Content-Security-Policy',
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'"
);
return $response;
}
}
// Register in kernel
protected $middleware = [
\App\Http\Middleware\SecurityHeaders::class,
];
Input Validation
Form Request Validation
php
final class StorePostRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()?->can('create', Post::class) ?? false;
}
public function rules(): array
{
return [
'title' => ['required', 'string', 'max:255', 'sanitize_html'],
'content' => ['required', 'string', 'max:10000'],
'image' => [
'required',
'image',
'mimes:jpg,jpeg,png,gif,webp', // Whitelist specific types
'max:2048', // 2MB max
],
'tags' => ['array'],
'tags.*' => ['integer', 'exists:tags,id'],
];
}
public function messages(): array
{
return [
'title.max' => 'Post title must not exceed 255 characters.',
'image.max' => 'Image must be under 2MB.',
];
}
// Sanitize input after validation
public function validated($key = null, $default = null): mixed
{
$validated = parent::validated();
$validated['title'] = strip_tags($validated['title']);
return $key ? ($validated[$key] ?? $default) : $validated;
}
}
Custom Validation Rules
php
// app/Rules/StrongPassword.php
class StrongPassword implements Rule
{
public function passes($attribute, $value): bool
{
return preg_match('/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&#^()_\-+=])[A-Za-z\d@$!%*?&#^()_\-+=]{12,}$/', $value);
}
public function message(): string
{
return 'The :attribute must be at least 12 characters with uppercase, lowercase, number, and symbol.';
}
}
// app/Rules/NotBlacklistedDomain.php
class NotBlacklistedDomain implements Rule
{
private array $blacklisted = ['mailinator.com', 'guerrillamail.com'];
public function passes($attribute, $value): bool
{
$domain = substr(strrchr($value, '@'), 1);
return !in_array(strtolower($domain), $this->blacklisted);
}
public function message(): string
{
return 'Email from disposable domains is not allowed.';
}
}
API Security
Rate Limiting
php
// App/Providers/RouteServiceProvider
protected function configureRateLimiting(): void
{
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});
RateLimiter::for('auth', function (Request $request) {
return Limit::perMinute(5)->by($request->ip())
->response(function () {
return response()->json([
'message' => 'Too many login attempts. Try again in 1 minute.',
], 429);
});
});
RateLimiter::for('uploads', function (Request $request) {
return Limit::perHour(10)->by($request->user()?->id ?? $request->ip())
->response(function () {
return response()->json([
'message' => 'Upload limit reached. Try again later.',
], 429);
});
});
}
// Route usage
Route::middleware(['auth:sanctum', 'throttle:api'])->group(function () {
Route::apiResource('posts', PostController::class);
});
Route::post('/login', [AuthController::class, 'login'])
->middleware('throttle:auth');
API Authentication — Sanctum vs Passport
php
// Sanctum (recommended for most apps — simple, first-party, SPA)
// config/sanctum.php
'expiration' => 60 * 24, // Tokens expire after 24 hours
'model' => User::class,
// Issuing scoped tokens
$token = $user->createToken('client-name', [
'posts:read',
'posts:write',
])->plainTextToken;
// Middleware scoping
Route::middleware('auth:sanctum')->group(function () {
Route::get('/posts', [PostController::class, 'index'])
->middleware('abilities:posts:read');
Route::post('/posts', [PostController::class, 'store'])
->middleware('abilities:posts:write');
});
// Passport (OAuth2 — for third-party clients or complex auth flows)
// Install: composer require laravel/passport
Passport::tokensExpireIn(now()->addDays(15));
Passport::refreshTokensExpireIn(now()->addDays(30));
Passport::personalAccessTokensExpireIn(now()->addMonths(6));
CORS Configuration
php
// config/cors.php
return [
'paths' => ['api/*', 'sanctum/csrf-cookie'],
'allowed_methods' => ['*'],
'allowed_origins' => explode(',', env('CORS_ALLOWED_ORIGINS', '')), // Whitelist specific origins
'allowed_origins_patterns' => [],
'allowed_headers' => ['*'],
'exposed_headers' => ['X-Total-Count', 'X-Pagination-Page'],
'max_age' => 0,
'supports_credentials' => true, // Required for Sanctum SPA auth
];
// NEVER: Allow all origins in production unless absolutely necessary
// 'allowed_origins' => ['*'], // Only for truly public APIs
File Upload Security
Validation
php
public function rules(): array
{
return [
'document' => [
'required',
'file',
'mimes:pdf,doc,docx,xls,xlsx', // Whitelist specific MIME types
'max:10240', // 10MB
'extensions:pdf,doc,docx,xls,xlsx', // Verify extension matches MIME
],
'avatar' => [
'nullable',
'image', // Ensures it's a valid image
'mimes:jpg,jpeg,png,webp',
'max:2048',
'dimensions:min_width=100,min_height=100,max_width=2000,max_height=2000',
],
];
}
Secure Storage
php
// 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 access
use Illuminate\Support\Facades\Storage;
public function download(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);
return redirect($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
bash
# 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
bash
# .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
php
// Define a named rate limiter (typically in AppServiceProvider::boot())
RateLimiter::for('payments', fn () => Limit::perMinute(5));
php
// Encrypt sensitive job data by implementing the interface
final class ProcessPaymentJob implements ShouldQueue, ShouldBeEncrypted
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(
private readonly string $paymentIntentId, // Public IDs are fine
private readonly string $cardFingerprint, // Encrypted via ShouldBeEncrypted
) {}
public function handle(): void
{
// Process payment
}
// Limit retries and delay between attempts
public function retryUntil(): Carbon
{
return now()->addMinutes(5);
}
// Rate limit how many jobs of this type can run
public function middleware(): array
{
return [
new RateLimited('payments'),
];
}
}
Logging Security Events
php
// config/logging.php
'channels' => [
'security' => [
'driver' => 'single',
'path' => storage_path('logs/security.log'),
'level' => 'warning',
],
],
// Audit log helper
final class SecurityLogger
{
public static function log(string $event, array $context = []): void
{
Log::channel('security')->warning($event, array_merge([
'user_id' => Auth::id(),
'ip' => request()->ip(),
'user_agent' => request()->userAgent(),
'url' => request()->fullUrl(),
'timestamp' => now()->toIso8601String(),
], $context));
}
}
// Usage
SecurityLogger::log('failed_login_attempt', ['email' => $email]);
SecurityLogger::log('password_change');
SecurityLogger::log('role_change', ['target_user' => $targetId, 'new_role' => 'admin']);
SecurityLogger::log('suspicious_activity', ['reason' => 'multiple_attempts_from_different_ips']);
Quick Security Checklist
| Check | Description |
|---|---|
APP_DEBUG=false | Never run with debug enabled in production |
APP_KEY set | Always run php artisan key:generate |
| HTTPS enforced | Force HTTPS in production via middleware or proxy |
$fillable whitelisted | Never use $guarded = [] |
| CSRF active | @csrf on all state-changing forms |
| Sanctum/Passport configured | API authentication with token abilities/scopes |
| Rate limiting applied | Throttle API and auth endpoints |
| Input validation | FormRequest with specific rules, never $request->all() |
| File upload restrictions | Validate MIME types, size, dimensions |
composer audit in CI | Check dependencies for known vulnerabilities |
password_hash / password_verify | Use Laravel's built-in hashing (bcrypt/Argon2) |
| Session regeneration on login | Call $request->session()->regenerate() |
| Security headers middleware | CSP, X-Frame-Options, X-Content-Type-Options |
| Logged security events | Audit log for auth failures, role changes, suspicious activity |
.env not committed | Verify .gitignore includes .env |
Related Skills
laravel-patterns— Laravel architecture, routing, Eloquent, and API patternsbackend-patterns— General backend API and database patternslaravel-tdd— Laravel testing with PHPUnit and Pest
Install this skill
- 1
Get the skill — it’s free
Use the Get this skill panel. Unlocked skills stay in My skills. - 2
Download or clone the files
Download the zip, or clone the repo and copy theskills/laravel-securityfolder. - 3
Put it where your agent looks for skills
For Claude Code, use your personal skills folder (every project) or a project’s own folder:~/.claude/skills/laravel-security/SKILL.md # all projects .claude/skills/laravel-security/SKILL.md # this project only
- 4
Just ask
No command needed. The agent reads the skill’s description and loads it on its own when your request matches.
SKILL.md frontmatter
What your agent reads to decide when to load this skill.
--- name: laravel-security description: Laravel security best practices — authentication, authorization, Eloquent safety, CSRF, XSS prevention, API security, and secure deployment configurations. Use when reviewing Laravel auth, Eloquent safety, CSRF, XSS, API security, or deployment configuration. ---
Files
This skill lives in a public GitHub repo. Browse the files on GitHub ↗
Related skills
View all →- Windows Desktop E2eFreeE2E testing for Windows native desktop apps (WPF, WinForms, Win32/MFC, Qt) using pywinauto and Windows UI Automation. Use when writing E2E tests for a Windows native desktop app with pywinauto or UI Automation.
- X ApiFreeX/Twitter API integration for posting tweets, threads, reading timelines, search, and analytics. Covers OAuth auth patterns, rate limits, and platform-native content posting. Use when the user wants to interact with X programmatically.
- Skill CreatorFreeCreate new skills, modify and improve existing skills, and measure skill performance. Use when users want to create a skill from scratch, edit, or optimize an existing skill, run evals to test a skill, benchmark skill performance with variance analysis, or optimize a skill's description for better triggering accuracy.
- Workspace Surface AuditFreeAudit the active repo, MCP servers, plugins, connectors, env surfaces, and harness setup, then recommend the highest-value ECC-native skills, hooks, agents, and operator workflows. Use when the user wants help setting up Claude Code or understanding what capabilities are actually available in their environment.