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.
- CORS misconfiguration on credentialed APIs
- XML external entity (XXE) processing
- Insecure randomness for security tokens
- Code injection
- CSV formula injection
- LDAP filter injection
- NoSQL operator injection
- OS command and argument injection
- Server-side template injection
- XPath injection
- Clickjacking
- Regular-expression denial of service (ReDoS)
- Unbounded resource consumption
- Hard-coded secrets
- Improper TLS certificate validation
- JWT validation in APIs (bearer tokens)
- CRLF injection in logs and response headers
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.
| Version | Latest | Status | Released | End of support |
|---|---|---|---|---|
| 13 | 13.35.0 | SupportedBug fixes until the third quarter of 2027; PHP 8.3 to 8.5. The cards use 13.34.0. | 17 March 2026 | 17 March 2028 |
| 12 | 12.69.3 | Security fixes onlyBug fixes ended on 2026-08-13; PHP 8.2 to 8.5. | 24 February 2025 | 24 February 2027 |
| 11 | 11.57.0 | End of lifeSecurity fixes ended on 2026-03-12; 11.57.0 (2026-09-29) backported two small changes. | 12 March 2024 | 12 March 2026 |
| Version | Latest | Status | Released | End of support |
|---|---|---|---|---|
| 8.5 | 8.5.11 | SupportedActive support until 2027-12-31; the cards use it. | 20 November 2025 | 31 December 2029 |
| 8.4 | 8.4.26 | SupportedActive support until 2026-12-31, then security fixes only until 2028-12-31. | 21 November 2024 | 31 December 2028 |
| 8.3 | 8.3.35 | Security fixes onlyThe oldest PHP that Laravel 13 supports. | 23 November 2023 | 31 December 2027 |
| 8.2 | 8.2.34 | Security fixes onlyLaravel 12 only; security fixes end on 2026-12-31. | 8 December 2022 | 31 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
Minimum secure versions
The lowest release in each supported line that fixes every published advisory listed here. Anything older is missing a security fix.
| Package | At least | Why |
|---|---|---|
laravel/framework | 13.30.0 or 12.69.0 | GHSA-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 |
php | 8.5.11, 8.4.26, 8.3.35 or 8.2.34 | The 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
TrustProxiesandTrustHosts(global): Early in the global stack. ConfiguretrustProxies(at: ...)soisSecure(), the client IP and generated URLs come from your load balancer's headers and only from it; enabletrustHosts()unless the web server already rejects unknown Host headers.TrimStringsandConvertEmptyStringsToNull(global): They change input before your code sees it;TrimStringsleavespassword,password_confirmationandcurrent_passwordalone.EncryptCookies,AddQueuedCookiesToResponse,StartSession(web group): Cookies are decrypted before the session reads its ID from one.PreventRequestForgery(web group): AfterStartSession, because the CSRF token lives in the session; it checks every POST, PUT, PATCH and DELETE.auth, thenSubstituteBindings: Laravel's middleware priority runs authentication before route model binding, so an unauthenticated request is refused before a model is loaded.canmiddleware or#[Authorize], then the controller: Authorization runs last; queries in the controller still need the signed-in user's scope.
Secure defaults and settings
| Setting | Value | Why |
|---|---|---|
APP_DEBUG | true in .env.example | Set false: the debug page shows configuration, queries and stack traces. |
APP_KEY | Empty until php artisan key:generate | Keep it secret and per environment; rotate with APP_PREVIOUS_KEYS so existing cookies still decrypt. |
SESSION_SECURE_COOKIE | Unset: Secure only on requests that arrive over HTTPS | Set true; behind a TLS-terminating proxy without trusted proxies the cookie loses Secure. |
SESSION_LIFETIME, SESSION_EXPIRE_ON_CLOSE | 120, false | Set the idle limit your risk calls for (30 minutes in the cards) and add an absolute limit in middleware. |
SESSION_ENCRYPT | false | Set true to encrypt session data at rest in the database, files or cache. |
SESSION_HTTP_ONLY, SESSION_SAME_SITE | true, lax | Keep them. |
HASH_DRIVER, BCRYPT_ROUNDS | bcrypt, 12 | Prefer argon2id; never copy BCRYPT_ROUNDS=4 from the skeleton's phpunit.xml. |
LOG_LEVEL | debug in .env.example | Raise 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.
| API | Risk | Instead |
|---|---|---|
{!! !!}, HtmlString, Str::markdown() without html_input | Turns 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 interpolation | SQL 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 request | Path traversal. | realpath() and a containment check. |
Process::run(), exec() or shell_exec() with a command string | OS command injection. | An array of arguments: Process::run([...]) runs without a shell. |
unserialize() on request or cookie data | PHP 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{!! !!}orHtmlString. - v5.0.0-3.5.1
PreventRequestForgerycovers 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()orregenerate()). - 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_DEBUGisfalsein production. - v5.0.0-11.4.2 Passwords use
Hash::make()with Argon2id, or bcrypt at cost 12 or more.