Fix cards for PHP and Laravel

A01:2025 Broken Access Control

  • Cross-site request forgery (CSRF)CWE-352

    Keep PreventRequestForgery, which Laravel puts in the web middleware group, on every state-changing route: it accepts a request that the browser marks Sec-Fetch-Site: same-origin, and otherwise requires the session's token in _token or X-CSRF-TOKEN.

  • Missing object-level authorization (IDOR and BOLA)CWE-862 · CWE-639 · CWE-863

    Build one query that holds the rule, from $request->user()'s tenant and role, never from request data: Invoice::where('tenant_id', $user->tenant_id), narrowed to owner_id for members.

  • Open redirectCWE-601

    Treat next as untrusted: resolve it against the current URL with PHP 8.5's Uri\WhatWg\Url::parse(), the parser browsers use, and redirect()->away() to the serialized result only when its scheme is https and its host is on your allowlist.

  • Path traversalCWE-22 · CWE-23 · CWE-36 · CWE-59

    Resolve both paths and compare whole components: $base = realpath($root), $target = realpath($base.'/'.$name), and serve response()->file($target) only if $target starts with $base.DIRECTORY_SEPARATOR. realpath() collapses ../ and follows symlinks, and returns false for missing files.

  • Server-side request forgery (SSRF)CWE-918

    Attach the policy to the connection, not the host string. Parse with Uri\WhatWg\Url, which reads any IPv4 notation as the address it names; allow only http and https on ports 80 and 443 without credentials; resolve names and refuse any address that fails FILTER_FLAG_GLOBAL_RANGE, plus multicast and NAT64; connect to the vetted address; vet every redirect; cap time and bytes.

A04:2025 Cryptographic Failures

  • Weak password hashingCWE-916 · CWE-759 · CWE-327

    Set HASH_DRIVER=argon2id and use Laravel's Hash facade: Hash::make() writes a salted Argon2id hash that records its parameters, Hash::check() verifies it in constant time, and Hash::needsRehash() after a correct password tells you to store a new hash.

A05:2025 Injection

  • Cross-site scripting (XSS)CWE-79

    Print user data with Blade's {{ $value }}, which runs htmlspecialchars() with ENT_QUOTES, so < > & " and ' become entities in text and quoted attributes.

  • SQL injectionCWE-89

    Let the query builder bind values: DB::table('orders as o')->where('c.name', $name)->where('o.status', $status) sends both as PDO parameters, and raw SQL takes ? placeholders with a bindings array.

A06:2025 Insecure Design

  • Unrestricted file uploadCWE-434 · CWE-646 · CWE-73 · CWE-770

    Validate with max:512, mimes:png,jpg,jpeg (the type guessed from the contents) and extensions:png,jpg,jpeg (the client's name), require getClientMimeType() to equal getMimeType(), store() on a private disk so hashName() picks the name, and serve with Storage::download() plus X-Content-Type-Options: nosniff.

A07:2025 Authentication Failures

  • Missing authentication for a critical functionCWE-306 · CWE-287 · CWE-288 · CWE-290

    Put every critical route behind one middleware that verifies a server-issued credential: here a Bearer token whose HMAC-SHA256 is checked with hash_equals.

  • Session fixation and session cookiesCWE-384 · CWE-613 · CWE-614 · CWE-1004 · CWE-1275

    Sign users in with Auth::attempt() or Auth::login(), which give the session a new ID, or call $request->session()->regenerate(true) in your own sign-in.

A08:2025 Software or Data Integrity Failures

  • Insecure deserializationCWE-502

    Do not rebuild objects from the token: base64-decode it and json_decode the bytes, then copy only the fields you expect; unserialize instantiates whatever class the string names and runs its __wakeup.

  • Mass assignment (over-posting)CWE-915

    Give each model an allowlist, #[Fillable(['display_name', 'bio'])] (or $fillable), and pass only validated input: $profile->update($request->validate([...rules for the editable fields...])). validate() returns just the keys it has rules for.

Not covered in Laravel yet

These weaknesses have fix cards in other stacks but not in this one.

Supported versions

Every Laravel major release gets bug fixes for 18 months and security fixes for 2 years, and a new major ships about once a year, in the first quarter. Laravel support policy.

Laravel
VersionLatestStatusReleasedEnd of support
1313.35.0SupportedBug fixes until the third quarter of 2027; PHP 8.3 to 8.5. The cards use 13.34.0.17 March 202617 March 2028
1212.69.3Security fixes onlyBug fixes ended on 2026-08-13; PHP 8.2 to 8.5.24 February 202524 February 2027
1111.57.0End of lifeSecurity fixes ended on 2026-03-12; 11.57.0 (2026-09-29) backported two small changes.12 March 202412 March 2026
PHP
VersionLatestStatusReleasedEnd of support
8.58.5.11SupportedActive support until 2027-12-31; the cards use it.20 November 202531 December 2029
8.48.4.26SupportedActive support until 2026-12-31, then security fixes only until 2028-12-31.21 November 202431 December 2028
8.38.3.35Security fixes onlyThe oldest PHP that Laravel 13 supports.23 November 202331 December 2027
8.28.2.34Security fixes onlyLaravel 12 only; security fixes end on 2026-12-31.8 December 202231 December 2026

Each PHP branch gets two years of active support and then two years of security fixes only. Laravel 13 needs PHP 8.3 or later, and the cards' PHPUnit 13 needs 8.4.1 or later. PHP release schedule

How the fix cards choose versions

Minimum secure versions

The lowest release in each supported line that fixes every published advisory listed here. Anything older is missing a security fix.

PackageAt leastWhy
laravel/framework13.30.0 or 12.69.0GHSA-jh5r-qr3c-85q8 (CVE-2026-102279, 2026-09-10): XSS on the debug error page when APP_DEBUG is true. These releases also contain the earlier 2026 fixes for CVE-2026-48019 and CVE-2026-48041. Advisory
php8.5.11, 8.4.26, 8.3.35 or 8.2.34The 24 September 2026 releases fix eleven CVEs in every supported branch, among them CVE-2026-91769 (TLS hostname verification falling back to the CN) and CVE-2026-91766 (credentials leaked on stream wrapper redirects). Advisory

Middleware and security order

  1. TrustProxies and TrustHosts (global): Early in the global stack. Configure trustProxies(at: ...) so isSecure(), the client IP and generated URLs come from your load balancer's headers and only from it; enable trustHosts() unless the web server already rejects unknown Host headers.
  2. TrimStrings and ConvertEmptyStringsToNull (global): They change input before your code sees it; TrimStrings leaves password, password_confirmation and current_password alone.
  3. EncryptCookies, AddQueuedCookiesToResponse, StartSession (web group): Cookies are decrypted before the session reads its ID from one.
  4. PreventRequestForgery (web group): After StartSession, because the CSRF token lives in the session; it checks every POST, PUT, PATCH and DELETE.
  5. auth, then SubstituteBindings: Laravel's middleware priority runs authentication before route model binding, so an unauthenticated request is refused before a model is loaded.
  6. can middleware or #[Authorize], then the controller: Authorization runs last; queries in the controller still need the signed-in user's scope.

Secure defaults and settings

SettingValueWhy
APP_DEBUGtrue in .env.exampleSet false: the debug page shows configuration, queries and stack traces.
APP_KEYEmpty until php artisan key:generateKeep it secret and per environment; rotate with APP_PREVIOUS_KEYS so existing cookies still decrypt.
SESSION_SECURE_COOKIEUnset: Secure only on requests that arrive over HTTPSSet true; behind a TLS-terminating proxy without trusted proxies the cookie loses Secure.
SESSION_LIFETIME, SESSION_EXPIRE_ON_CLOSE120, falseSet the idle limit your risk calls for (30 minutes in the cards) and add an absolute limit in middleware.
SESSION_ENCRYPTfalseSet true to encrypt session data at rest in the database, files or cache.
SESSION_HTTP_ONLY, SESSION_SAME_SITEtrue, laxKeep them.
HASH_DRIVER, BCRYPT_ROUNDSbcrypt, 12Prefer argon2id; never copy BCRYPT_ROUNDS=4 from the skeleton's phpunit.xml.
LOG_LEVELdebug in .env.exampleRaise it in production so logs do not collect request details.

Escape hatches to search for

Framework APIs that switch a protection off. Each one is sometimes needed; search for them in review and make each use justify itself.

APIRiskInstead
{!! !!}, HtmlString, Str::markdown() without html_inputTurns off escaping.{{ }}, and Markdown with html_input => strip.
withoutMiddleware(PreventRequestForgery::class), preventRequestForgery(except: [...])Removes CSRF protection from a route.Send the token in X-CSRF-TOKEN; exclude only signed webhooks.
DB::raw(), whereRaw(), orderByRaw(), DB::select() with interpolationSQL injection.The query builder, or ? bindings passed separately.
#[Unguarded], $guarded = [], Model::unguard(), forceFill($request->all())Mass assignment.#[Fillable] and $request->validated().
redirect()->away(), redirect($request->input(...))Open redirect.An allowlist checked with Uri\WhatWg\Url, or redirect()->intended().
response()->file() or download() with a path from the requestPath traversal.realpath() and a containment check.
Process::run(), exec() or shell_exec() with a command stringOS command injection.An array of arguments: Process::run([...]) runs without a shell.
unserialize() on request or cookie dataPHP object injection.json_decode() into a validated shape.

Checklist, mapped to ASVS 5.0.0

  • v5.0.0-1.2.1 Blade prints user data with {{ }}; no request data reaches {!! !!} or HtmlString.
  • v5.0.0-3.5.1 PreventRequestForgery covers every cookie-authenticated route that changes state.
  • v5.0.0-8.2.2 Controllers load records through queries scoped to the signed-in user and tenant.
  • v5.0.0-1.2.4 No SQL is built by interpolation; raw methods take bindings.
  • v5.0.0-15.3.3 Models declare #[Fillable] and controllers pass validated input.
  • v5.0.0-7.2.4 Sign-in gives the session a new ID (Auth::login() or regenerate()).
  • v5.0.0-3.3.1 Session and XSRF cookies are Secure (SESSION_SECURE_COOKIE=true).
  • v5.0.0-3.4.1 The web server or a middleware sends Strict-Transport-Security; Laravel does not.
  • v5.0.0-13.4.2 APP_DEBUG is false in production.
  • v5.0.0-11.4.2 Passwords use Hash::make() with Argon2id, or bcrypt at cost 12 or more.

References

  1. Laravel: release notes and support policy
  2. Laravel: deployment, debug mode
  3. Laravel: default middleware groups
  4. Laravel: configuring trusted proxies and hosts
  5. Laravel: rotating encryption keys
  6. Laravel framework security advisories
  7. PHP: supported versions
  8. PHP 8 ChangeLog

All fix cards