agentsclimarketplace

Laravel security

Skill pekral/cursor-rules/skills/laravel-security

PHP and Laravel Cursor rules — coding standards, testing, and conventions for the Cursor editor. Install via Composer.

Install
npx -y skills add pekral/cursor-rules --skill laravel-security

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 5 stars5 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Use when building, configuring, or hardening security-sensitive Laravel features — authentication, authorization, Eloquent safety, CSRF/XSS, API security, file uploads, secrets, and production configuration. Provides condensed, copy-ready secure defaults for Laravel 11 / PHP 8.3.

The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.

SKILL.md

13.7 KB, as published. Nobody here has run it

Laravel Security Best Practices

Constraints

  • Apply @rules/security/backend.md and @rules/security/frontend.md
  • Apply @rules/php/core-standards.mdcfinal classes, declare(strict_types=1), typed signatures
  • If the project uses Laravel, also apply @rules/laravel/laravel.mdc, @rules/laravel/architecture.mdc, @rules/laravel/filament.mdc, @rules/laravel/livewire.mdc
  • Stack: Laravel 11 / PHP 8.3, Filament, Livewire, Alpine.js, Blade, Tailwind, Pest, Vite, MySQL, Redis
  • Never hardcode secrets; never reveal them in output
  • Hard limits: this file stays <= 500 lines and <= 5000 tokens

Purpose

Secure-by-default building blocks for security-sensitive Laravel work. Use the matching section, copy the minimal snippet, and verify against the checklist. For an audit of existing code use @skills/security-review/SKILL.md.

Use when

  • Setting up authentication / authorization (Sanctum, gates, policies, middleware)
  • Configuring production settings and environment variables
  • Writing secure Eloquent queries and models
  • Hardening CSRF / XSS / input validation / file uploads / API endpoints
  • Managing secrets, queue payloads, and security event logging

Production Configuration

// config/app.php
'debug' => (bool) env('APP_DEBUG', false), // CRITICAL: never true in production
'key' => env('APP_KEY'),                    // php artisan key:generate

// config/session.php
'secure' => env('SESSION_SECURE_COOKIE', true),
'http_only' => true,
'same_site' => 'lax',

Validate required config at boot and fail fast:

// AppServiceProvider::boot()
foreach (['app.key', 'database.connections.mysql.database'] as $key) {
    if (empty(config($key))) {
        throw new RuntimeException("Missing required config key: {$key}");
    }
}

HTTPS and trusted proxies:

if (app()->environment('production')) {
    URL::forceScheme('https');
}
// Use specific CIDR ranges, never '*' (X-Forwarded-* spoofing)
'trusted_proxies' => ['10.0.0.0/8', '172.16.0.0/12'],

Keep .env out of version control (.gitignore ships with .env); ship .env.example with empty placeholders.

Authentication

Sanctum (API tokens)

// config/sanctum.php
'expiration' => 60 * 24,                          // minutes; null = never
'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''),

$token = $user->createToken('api', ['posts:read', 'posts:write'])->plainTextToken;

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');
});

Passwords

// config/hashing.php — bcrypt rounds >= 12, or Argon2id
'bcrypt' => ['rounds' => env('BCRYPT_ROUNDS', 12)],

// FormRequest rules
'password' => [
    'required', 'confirmed',
    Password::min(12)->letters()->mixedCase()->numbers()->symbols()->uncompromised(),
],

Sessions

// config/session.php — 'driver' => 'database' or 'redis' (avoid 'file' in prod)

public function store(LoginRequest $request): RedirectResponse
{
    $request->authenticate();
    $request->session()->regenerate(); // CRITICAL: prevents session fixation
    return redirect()->intended('/dashboard');
}

public function destroy(Request $request): RedirectResponse
{
    Auth::guard('web')->logout();
    $request->session()->invalidate();
    $request->session()->regenerateToken();
    return redirect('/');
}

Authorization

Gates

// AppServiceProvider::boot()
Gate::define('update-post', fn (User $user, Post $post): bool => $user->id === $post->user_id);

Gate::before(function (User $user, string $ability): ?bool {
    return $user->role === 'super-admin' ? true : null; // null = fall through
});

// Controller
Gate::authorize('update-post', $post);

Policies

final class PostPolicy
{
    public function view(?User $user, Post $post): bool
    {
        return $post->is_published || ($user && $user->id === $post->user_id);
    }

    public function update(User $user, Post $post): bool
    {
        return $user->id === $post->user_id;
    }
}

// Controller: $this->authorize('update', $post);
// Blade: @can('update', $post) ... @endcan

Laravel 11 auto-discovers policies by naming convention; register explicitly only when names differ.

Middleware

Route::put('/posts/{post}', [PostController::class, 'update'])->middleware('can:update,post');

Route::middleware(['auth', 'role:admin'])->group(function () {
    Route::get('/admin', [AdminController::class, 'index']);
});

For Filament, enforce access via policies and canAccessPanel(); for Livewire, re-check authorization inside actions — a mounted component is not an authorization boundary (@rules/laravel/filament.mdc, @rules/laravel/livewire.mdc).

Eloquent Security

Mass assignment

final class User extends Authenticatable
{
    protected $fillable = ['name', 'email', 'phone', 'avatar'];
    // NEVER list 'role', 'is_admin'; NEVER use $guarded = []
}

User::create($request->validated());        // GOOD: validated fields only
// User::create($request->all());           // VULNERABLE

SQL injection

User::where('email', $userInput)->first();                  // parameterized
User::whereRaw('email = ?', [$userInput])->first();         // parameterized
DB::select('SELECT * FROM users WHERE email = ?', [$input]); // parameterized

// VULNERABLE — never interpolate user input:
// User::whereRaw("email = '{$userInput}'")->first();
// User::orderByRaw($userInput);  DB::statement("... '{$userInput}'");

Casting and hidden attributes

final class User extends Authenticatable
{
    protected $casts = [
        'is_admin' => 'boolean',
        'settings' => 'array',
        'metadata' => 'encrypted:array', // Laravel 11 encrypted cast
        'password' => 'hashed',          // auto-hash on set
    ];

    protected $hidden = ['password', 'remember_token', 'two_factor_secret'];
}

CSRF Protection

CSRF is on by default for the web group. State-changing forms need @csrf:

<form method="POST" action="/posts">@csrf ...</form>

Exclude only specific signature-verified webhooks — never blanket api/* (stateful Sanctum needs CSRF):

// bootstrap/app.php — $middleware->validateCsrfTokens(except: ['stripe/*'])

For JS, send the token header (Axios ships preconfigured in Laravel):

<meta name="csrf-token" content="{{ csrf_token() }}">

XSS Prevention

{{ $userInput }}        {{-- SAFE: auto-escaped --}}
{!! $userInput !!}      {{-- DANGEROUS: raw, never with user input --}}
{!! $trustedHtml !!}    {{-- only for content you fully control --}}

<script>
    const user = @js($user);      {{-- escaped for JS context --}}
    const cfg  = @json($config);
</script>

When user HTML must survive, purify with an allowlist before storing/output:

// composer require ezyang/htmlpurifier
$config = \HTMLPurifier_Config::createDefault();
$config->set('HTML.Allowed', 'p,b,i,a[href],ul,ol,li,br');
$config->set('URI.AllowedSchemes', ['http', 'https', 'mailto']);
$clean = (new \HTMLPurifier($config))->purify($dirty);

In Alpine, prefer x-text over x-html; only use x-html on sanitized content. Add security headers via middleware:

$response->headers->set('X-Content-Type-Options', 'nosniff');
$response->headers->set('X-Frame-Options', 'DENY');
$response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');
$response->headers->set('Content-Security-Policy',
    "default-src 'self'; frame-ancestors 'none'");

Input Validation

Always validate through a FormRequest; never persist $request->all():

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'],
            'content' => ['required', 'string', 'max:10000'],
            'tags'    => ['array'],
            'tags.*'  => ['integer', 'exists:tags,id'],
        ];
    }
}

Keep validation messages generic — never leak which auth factor failed, whether a record exists, or framework internals (@rules/security/backend.md).

API Security

Rate limiting

// AppServiceProvider::boot()
RateLimiter::for('api', fn (Request $r) =>
    Limit::perMinute(60)->by($r->user()?->id ?: $r->ip()));

RateLimiter::for('auth', fn (Request $r) =>
    Limit::perMinute(5)->by($r->ip()));

Route::post('/login', [AuthController::class, 'login'])->middleware('throttle:auth');

CORS

// config/cors.php
'paths' => ['api/*', 'sanctum/csrf-cookie'],
'allowed_origins' => explode(',', env('CORS_ALLOWED_ORIGINS', '')), // explicit allowlist
'supports_credentials' => true, // required for Sanctum SPA auth
// NEVER ['*'] when credentials are supported

File Upload Security

public function rules(): array
{
    return [
        'document' => ['required', 'file', 'mimes:pdf,doc,docx', 'max:10240',
                       'extensions:pdf,doc,docx'],
        'avatar'   => ['nullable', 'image', 'mimes:jpg,jpeg,png,webp', 'max:2048',
                       'dimensions:max_width=2000,max_height=2000'],
    ];
}

Store sensitive files off the public disk and serve through an authorized, time-limited URL:

$path = $request->file('document')->store('documents', 'local'); // not 'public'

public function download(Request $request, string $path): RedirectResponse
{
    $this->authorize('download', $path);
    return redirect(Storage::temporaryUrl($path, now()->addMinutes(15)));
}

Secrets and Dependencies

composer audit          # run in CI; fail the build on advisories
# keep composer.lock committed; run composer update deliberately, never in CI

Read every secret from env()/config(); validate presence at boot. For production use a secret manager rather than a deployed .env.

Queue Security

// Encrypt sensitive payloads on the wire
final class ProcessPaymentJob implements ShouldQueue, ShouldBeEncrypted
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public function __construct(
        private readonly string $paymentIntentId,
        private readonly string $cardFingerprint,
    ) {}

    public function handle(): void { /* ... */ }

    public function retryUntil(): \Carbon\CarbonInterface
    {
        return now()->addMinutes(5);
    }
}

Logging Security Events

// config/logging.php — dedicated 'security' channel
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(),
            'url'     => request()->fullUrl(),
        ], $context));
    }
}

SecurityLogger::log('failed_login_attempt', ['email' => $email]);
SecurityLogger::log('role_change', ['target_user' => $targetId, 'new_role' => 'admin']);

Quick Security Checklist

CheckDescription
APP_DEBUG=falseNever run with debug enabled in production
APP_KEY setAlways run php artisan key:generate
HTTPS enforcedForce HTTPS in production via middleware or proxy
$fillable whitelistedNever use $guarded = []
CSRF active@csrf on all state-changing forms
Sanctum scopesToken abilities enforced per route
Rate limitingThrottle API and auth endpoints
Input validationFormRequest with specific rules, never $request->all()
File upload restrictionsValidate MIME, extension, size, dimensions
composer audit in CICheck dependencies for known vulnerabilities
Password hashingLaravel bcrypt/Argon2, 'password' => 'hashed' cast
Session regenerationCall $request->session()->regenerate() on login
Security headersCSP, X-Frame-Options, X-Content-Type-Options
Security event loggingAudit auth failures, role changes, suspicious activity
.env not committedVerify .gitignore includes .env

Laravel Security Audit

When auditing an existing Laravel application (instead of building new features), use @skills/laravel-security/references/audit-workflow.md. It covers the 7 audit areas — Authorization/IDOR/BOLA, Authentication, Validation, XSS, File upload, Secrets/configuration, Dependencies — with severity mapping (Critical/High/Medium/Low/Info → CR scale Critical/Moderate/Minor), Grep patterns, and a required regression-test sketch per confirmed finding. The building blocks in this file (Production Configuration, Authentication, Authorization, Eloquent Security, CSRF, XSS Prevention, Input Validation, File Upload Security, Secrets and Dependencies) are the reference fixes the audit workflow links back to.

Done when

  • The relevant secure default is applied and matches the project's existing style
  • No secret is hardcoded; required config/secrets are validated at boot
  • Every applicable row of the checklist is satisfied or consciously waived
  • Tests (Pest) cover the new auth/authorization/validation behavior

Related Skills

  • @skills/security-review/SKILL.md — review workflow for existing code
  • @skills/security-threat-analysis/SKILL.md — remediate a referenced advisory/CVE
  • @skills/test-driven-development/SKILL.md — drive the implementation test-first

Gives 2 of the 12 instructions most security skills give

Counted across 648 of the 828 authors here whose files we hold, read 2026-08-06

  • parameterize all database querieshere, and in 67 of 648, across 49 files
  • hash passwords using bcrypt scrypt or argon2in 48 of 648, across 35 files
  • apply rate limiting to authentication endpointsin 48 of 648, across 24 files
  • Configure security headersin 35 of 648, across 18 files
  • validate all inputshere, and in 32 of 648, across 24 files
  • validate all external input at the system boundaryin 29 of 648, across 18 files
  • run containers as a non-root userin 28 of 648, across 15 files
  • use httponly secure samesite cookies for sessionsin 26 of 648, across 15 files
  • run dependency audits before every releasein 21 of 648, across 10 files
  • encode output to prevent cross-site scriptingin 21 of 648, across 10 files
  • copy dependencies before source codein 20 of 648, across 9 files
  • store secrets in environment variablesin 20 of 648, across 17 files

Said here and by no other author read

  • use strict types and final classes
  • validate required configuration at boot
  • use specific CIDR ranges for trusted proxies
  • enforce access via policies and middleware
  • define fillable attributes explicitly
  • purify user HTML with an allowlist

Grouped from the skills themselves: near-identical wordings counted once, and counted by distinct author, so one author publishing three of these counts once.

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.