Sample report: OWASP Top 10 vs Laravel¶
What this is
The output of /codenavi:compliance owasp-top10 against laravel/framework 13.x (the framework library, not an application), produced 2026-09-14 by compliance-check v0.1.5. It is a demonstration run against public source used as a testbed, not a security assessment of the project. Every verdict cites the lines it was verified against. The raw file with its integrity footer is at /samples/codenavi-compliance-owasp-top10-laravel-2026-09-14.md (download; view inline); verify it with head -n -1 codenavi-compliance-owasp-top10-laravel-2026-09-14.md | sha256sum against the footer.
Generated 2026-09-14 by CodeNavi compliance-check v0.1.5 · 10 rules
Codebase revision: git 26013ca (laravel/framework 13.x branch, Application::VERSION 13.4.0, upstream commit of 2026-04-08)
Summary: 2 pass · 8 partial · 0 fail · 0 not applicable · 0 cannot check from code
Scope notes: the target is laravel/framework, the framework library itself, not an application built on it. It ships no routes, no user model, no config/ files and no deployment artifact; those live in the application skeleton. So each rule is judged on the controls the framework provides and the defaults it applies when an application wires them in, and a recurring verdict pattern is "the control exists and is correct, but it is opt-in for the application". Where a sub-check has no code in this tree (XML parsing, seed data, VCS exclusion, MFA), it drops out rather than counting against the rule. find_callers was not needed: the framework's own middleware groups are declarative and were read directly.
| Rule | Question | Verdict | Evidence | Note |
|---|---|---|---|---|
| TOP10-A01 | Broken Access Control | partial | src/Illuminate/Auth/Access/Gate.php:396-459 (authorize/inspect/raw, deny by default), src/Illuminate/Auth/Middleware/Authorize.php:55-60, src/Illuminate/Foundation/Http/Middleware/PreventRequestForgery.php:95-112,168-196 (CSRF: Sec-Fetch-Site origin check, then hash_equals on the session token), src/Illuminate/Foundation/Configuration/Middleware.php:485-493 (PreventRequestForgery in the default web group), src/Illuminate/Database/Eloquent/Concerns/GuardsAttributes.php:17-24,193-233 ($guarded = ['*'] by default), src/Illuminate/Routing/Route.php:1247-1274 (scopeBindings defaults to false) |
Worst applicable sub-check: id-scoped records. Implicit route-model binding resolves {post} by id with no ownership scoping unless the route (or a nested parent binding) opts in with scopeBindings(); the default is off, so the app must add a Policy check or scoping on every id-based route. Everything else is on by default: the Gate denies when no ability is defined, CSRF verification runs on every non-read request in the web group with a constant-time compare and an origin pre-check, and Eloquent models refuse every mass-assigned attribute until the app declares $fillable. Outbound fetches: the Http client ships no destination allowlist or private-address block, so SSRF defence is entirely the app's (named gap, not a bypass in framework code). |
| TOP10-A02 | Security Misconfiguration | partial | src/Illuminate/Session/Middleware/StartSession.php:221-236 (session cookie: http_only defaults true, secure and same_site read from config with no default), src/Illuminate/Cookie/CookieServiceProvider.php:20, src/Illuminate/Foundation/Exceptions/Handler.php:847-860,883-898,1031-1042 (stack traces only when app.debug; otherwise Server Error) |
Worst sub-check: cookie flags. The framework sets HttpOnly unconditionally but leaves Secure and SameSite to config/session.php, which is not in this tree (the skeleton ships same_site => 'lax' and secure => env('SESSION_SECURE_COOKIE')), so a misconfigured app can emit a session cookie with neither. Debug: every debug-only render path checks config('app.debug'); production output is a generic message. No XML parser in src/, no seed data, no committed credentials (all env()-driven), and no deploy artifact, so the XML, default-admin, secret, VCS and backend-credential sub-checks drop out. |
| TOP10-A03 | Software Supply Chain Failures | partial | composer.json:20-67 (caret ranges, no composer.lock in the tree), composer.json:215-219 (config.audit.ignore, i.e. Composer's built-in audit is configured), .github/workflows/tests.yml:75,135 and five sibling workflows (composer update on every CI run, which triggers Composer's automatic advisory audit) |
A published library omits a lockfile by Composer convention, so the missing lockfile is not a fail on its own. Versions are floating (^7.4.0 || ^8.0.0) by design. Scanning: no Dependabot/Renovate file, but Composer 2.4+ audits advisories on every composer update, which all CI workflows run, and the audit.ignore block shows that path is live. Partial because nothing pins what a consumer actually installs; that is the consumer's lockfile. Whether any resolvable version carries an open advisory needs a live lookup: cannot check from code. |
| TOP10-A04 | Cryptographic Failures | partial | src/Illuminate/Hashing/BcryptHasher.php:17,55-70 (bcrypt, cost 12 default), src/Illuminate/Hashing/Argon2IdHasher.php:50, src/Illuminate/Encryption/Encrypter.php:39-44,104-130,155-196,219-222,293-298 (AES-CBC with encrypt-then-MAC HMAC-SHA256 and hash_equals, or AES-GCM), src/Illuminate/Support/Str.php:1112-1129 (random_bytes), src/Illuminate/Auth/Notifications/VerifyEmail.php:83-90 (sha1(email) inside a signed URL), src/Illuminate/Routing/UrlGenerator.php:486-493 (HMAC-SHA256 signature check, the wrapper), src/Illuminate/Validation/NotPwnedVerifier.php:68-75 (SHA-1 for the k-anonymity range query, by protocol) |
Worst sub-check: a weak hash on a nominal token path, proven non-exploitable by a stronger wrapper. The email-verification link carries sha1(email) as a route parameter, but the whole URL is HMAC-SHA256 signed and expiry-checked before the hash is compared, so the SHA-1 is a binding value, not the authenticator (partial per the rule's own carve-out, wrapper cited). Everything else passes: bcrypt at cost 12 (Argon2id available), authenticated encryption in both cipher families with MAC verified in constant time before decrypt, random_bytes for every token and session id, 256-bit keys. MD5/SHA-1 elsewhere are cache keys and ETags. Note the Encrypter constructor's own default cipher is aes-128-cbc; the skeleton passes AES-256-CBC. |
| TOP10-A05 | Injection | pass | src/Illuminate/Database/Connection.php:735-771 (PDO bindValue with typed params, prepareBindings), src/Illuminate/Database/Query/Builder.php:4562-4571 and Query/Expression.php:11 (raw() typed literal-string, the explicit opt-out), src/Illuminate/View/Compilers/BladeCompiler.php:103,110 and Concerns/CompilesEchos.php:39-123 ({{ }} compiles to e(), {!! !!} is the raw opt-out), src/Illuminate/Support/helpers.php:124-139 (htmlspecialchars with ENT_QUOTES), src/Illuminate/Process/PendingProcess.php:113-118 (array-form command supported), src/Illuminate/Filesystem/Filesystem.php:358,364 (the one internal shell-out escapes both arguments) |
Every applicable sub-check passes at the framework level. SQL: the query builder binds every value through PDO; string interpolation exists only behind raw()/whereRaw(), which are named and typed as literal-string opt-outs. HTML: the default Blade echo escapes with ENT_QUOTES|ENT_SUBSTITUTE; the raw echo is a distinct, visibly different syntax. OS: Process::run accepts an argument array; the framework's own exec call wraps both values in escapeshellarg. Validation: the validator runs server-side by construction (FormRequest, validate()), independent of any client. Whether an application uses the opt-outs on untrusted input is an application check, not visible in this tree. |
| TOP10-A06 | Insecure Design | partial | src/Illuminate/Validation/Concerns/ValidatesAttributes.php:1713-1773 (mimes matches the extension guessed from file content, mimetypes the sniffed MIME type; both refuse PHP extensions unless explicitly allowed), src/Illuminate/Validation/Rules/File.php:132-159 (File::types(), ->extensions(), File::image()), src/Illuminate/Http/FileHelpers.php:42-55 (hashName: random name plus content-guessed extension), src/Illuminate/Http/UploadedFile.php:84-97 (storeAs performs no type check) |
Upload proxy: the type checks are content-based, not header-based, and the PHP blocklist is on by default inside them, which is stronger than the rule asks. Partial because they are validation rules an application must attach; UploadedFile::store() accepts and persists anything when no rule is applied, and stored names keep a content-derived extension. Role-gated UI vs backend: the Gate and can middleware exist (A01); Blade's @can is a view-side convenience with no automatic server twin, so parity is the app's job. Threat modelling, secure SDLC and business-logic abuse cases: see "Not checkable from code". |
| TOP10-A07 | Authentication Failures | partial | src/Illuminate/Routing/Middleware/ThrottleRequests.php:85-106 (throttling middleware, opt-in per route; api group applies it only when a limiter is configured, Foundation/Configuration/Middleware.php:495-499), src/Illuminate/Validation/Rules/Password.php:42,166-173,259 (default rule min(8), uncompromised() breach check available), src/Illuminate/Session/Store.php:714,722-725 (40-char Str::random session id, format-validated on load), src/Illuminate/Auth/SessionGuard.php:584-589 (regenerate(true) on login), src/Illuminate/Session/Middleware/StartSession.php:169-179,259 (lifetime from config, GC lottery), src/Illuminate/Auth/SessionGuard.php:650-671,704-715,723-728 (logout: session key removed, remember token rotated, recaller cookie forgotten) |
Worst applicable sub-check: logout. SessionGuard::logout removes the auth key and rotates the remember-me token but does not invalidate or regenerate the session id; the skeleton's starter kits add session()->invalidate() themselves. Login throttling is provided but not applied to any login route in this tree (the framework has no login route). Session ids: 40 alphanumerics from random_bytes, about 238 bits, regenerated with the old id destroyed on every successful login (fixation covered). Password policy: the framework default is 8 characters and a HaveIBeenPwned k-anonymity check is one call away; both are opt-in rules. Idle timeout: driven by session.lifetime; handlers expire by last-activity, GC runs by lottery. MFA: no second-factor code in this tree (it lives in Fortify), so that sub-check drops out. |
| TOP10-A08 | Software or Data Integrity Failures | pass | src/Illuminate/Encryption/Encrypter.php:155-196 (MAC verified with hash_equals before openssl_decrypt, and only then unserialize), src/Illuminate/Cookie/Middleware/EncryptCookies.php:111-148 (every incoming cookie decrypted through that path, with a per-cookie value prefix check), src/Illuminate/Session/Store.php:126-141 (session data read from the server-side handler), src/Illuminate/Queue/CallQueuedHandler.php:92-103 (job payload from the queue backend, optionally encrypted), src/Illuminate/Cache/FileStore.php:393-399 and siblings (allowed_classes restriction available) |
Every path that deserialises data a client can influence is integrity-checked first: cookies, the cookie session driver and encrypted queue jobs all go through the encrypter, which rejects an invalid MAC before any unserialize. Native unserialize on session, cache and queue payloads reads only server-side stores the application controls; the cache stores expose an allowed_classes restriction for defence in depth. No auto-update or artifact-fetch feature exists in the framework, so that sub-check drops out. |
| TOP10-A09 | Security Logging and Alerting Failures | partial | src/Illuminate/Auth/SessionGuard.php:854-857 (Failed event on a bad login), src/Illuminate/Auth/Events/Lockout.php:7, src/Illuminate/Auth/Access/Gate.php:454-458 (GateEvaluated event on every authorization decision), src/Illuminate/Auth/Access/Response.php:148-157 (denial becomes AuthorizationException, no log call), src/Illuminate/Foundation/Exceptions/Handler.php:150-164 (AuthenticationException and AuthorizationException are in $internalDontReport, so the default reporter never logs them) |
Authentication failures and authorization denials are surfaced as events, not written to a log by the framework; the exception handler explicitly excludes both exception types from reporting, so with no listener registered a failed login or a denied action leaves no log line. That is a design choice (the app decides what is security-relevant), but it means the rule's evidence_of_pass is unmet out of the box. Log injection: the logger is Monolog with structured context, so values are encoded by construction, and the handler never flashes password fields. Retention, SIEM and alerting: cannot check from code. |
| TOP10-A10 | Mishandling of Exceptional Conditions | partial | src/Illuminate/Auth/Access/Gate.php:408-423 (inspect turns a thrown AuthorizationException into a deny response), src/Illuminate/Encryption/Encrypter.php:187-193 (bad MAC or failed decrypt throws), src/Illuminate/Foundation/Http/Middleware/PreventRequestForgery.php:187-193 (undecryptable XSRF header becomes an empty token, which fails the match), src/Illuminate/Foundation/Exceptions/Handler.php:849-860,1033-1041 (raw exception detail only when app.debug); src/Illuminate/Validation/NotPwnedVerifier.php:83-102 (the fail-open) |
Security checks in the auth, CSRF and encryption paths fail closed: an exception inside an authorization callback is converted to a denial, a bad MAC aborts, an undecryptable token counts as no token. The named gap is the breached-password check: if the HaveIBeenPwned request throws or returns non-2xx, search() reports the exception and returns an empty list, so verify() treats the password as not breached and validation passes (fail open on error). Exception detail: with debug off the client sees Server Error and a status code; with debug on, file, line and trace are rendered, gated by config. |
Not checkable from code¶
- TOP10-A03: whether the versions a consumer resolves from these caret ranges carry open advisories needs a live Packagist/OSV lookup against that consumer's lockfile.
- TOP10-A06: threat modelling, secure-SDLC practice and business-logic abuse-case coverage are process records of the framework project, not code patterns.
- TOP10-A09: log retention, SIEM forwarding and alerting rules belong to the deploying application and its operations.
Scope caveat specific to this codebase¶
This is a framework, so most "is the control on the execution path?" questions resolve to "the framework puts it on the path when the application uses the default middleware groups and rules". Verdicts above name which controls are default-on (CSRF, mass-assignment guard, session regeneration, cookie HttpOnly, PDO binding, Blade escaping) and which are opt-in (route throttling, scoped bindings, upload rules, password rules, breach check, auth-failure logging). An application-level run against a real Laravel app would inherit the default-on items and have to prove the opt-in ones.
This is a gap analysis produced to prepare for an audit. It is not a certification, and absence of evidence is not proof of absence.