Security-focused static analysis for Laravel applications. One artisan command, 88 checks across config, cookies, headers, auth, routing, models, SQL, XSS, files, injection, crypto, dependencies, ecosystem packages and more β plus an optional runtime probe that verifies headers on the live app.
Why LaraScan? Most Laravel security issues come from misconfiguration or forgotten dev settings in production β debug on, secure cookies off, hardcoded API keys in code. LaraScan scans for them in one shot, AST-based where it matters, with sane defaults and a clean CI workflow.
larascan security scan
ββββββββββββββββββββββββββββββββββββββββββββ
Application configuration
βββββββββββββββββββββββββ
β config.app-env
ββ INFO APP_ENV is 'local' β leaks development-mode behavior in production.
β config.env-example-sync
ββ LOW Keys present in .env but missing from .env.example: MISTRAL_API_KEY
ββ LOW Keys present in .env.example but missing from .env: RESPONSE_CACHE_*
Cookies & sessions
ββββββββββββββββββ
β cookies.session-encrypt
ββ HIGH session.encrypt is false β session payloads are stored in plaintext.
ββββββββββββββββββββββββββββββββββββββββββββ
Report Card
ββββββββββββββββββββββββββββββββββββββββββββ
Application configuration ββββββββββββββββββββ 57% (4/7)
Cookies & sessions ββββββββββββββββββββ 71% (5/7)
HTTP headers ββββββββββββββββββββ 20% (1/5)
...
Total: 45 passed 19 failed 6 skipped 0 errored
Highest severity: CRITICAL
composer require baspa/larascan --dev
php artisan larascan:installThe install command publishes config/larascan.php and optionally .github/workflows/larascan.yml (CI workflow stub).
php artisan larascan # run all enabled checks
php artisan larascan --only-failed # hide passed + skipped
php artisan larascan --category=config
php artisan larascan --fail-on=high # CI threshold (exit 1 on findings β₯ high)
php artisan larascan:list # list all registered checksphp artisan larascan:advise # surface heuristic security advisories
php artisan larascan:advise --advice=advise.auth.*
php artisan larascan:advise --category=authAdvise is intentionally non-gating: exit code is always 0. For architectural items that no scanner can detect, see docs/manual-security-checklist.md.
| Flag | Default for | Description |
|---|---|---|
| (none) | TTY / humans | Categorized output with a Report Card at the end |
--format=json |
AI agents | Structured JSON. Auto-selected when laravel/agent-detector flags the run as an agent (Claude Code, Cursor, Codex, Copilot, etc.). |
--format=sarif |
GitHub Code Scanning | SARIF 2.1.0 report with one result per finding. |
Force JSON manually with LARASCAN_AGENT_MODE=1 or --format=json.
Any format can be written to a file with --output=PATH. For --format=json and --format=sarif, stdout still gets the human report (so CI logs stay readable); with --format=human, stdout only gets a Report written to ... confirmation. The exit code is unchanged either way.
Published config/larascan.php controls:
fail_onβ severity threshold for non-zero exit code (critical|high|medium|low|info, defaulthigh)checksβ per-check enable map ('cookies.session-secure' => ['enabled' => false])ignoreβ glob patterns to skip during AST scanstoolsβ override binary paths via env vars:LARASCAN_COMPOSER_BIN,LARASCAN_NPM_BIN,LARASCAN_SEMGREP_BIN
See docs/configuration.md for full details.
The published workflow runs on PR + push to main + nightly. It uses --only-failed to keep CI logs lean, with the Report Card at the end for the overview.
php artisan larascan:install --workflowExit codes: 0 clean, 1 findings β₯ --fail-on, 2 a check errored. See docs/ci-integration.md.
Findings can show up as Code Scanning alerts on the Security tab and as PR annotations:
php artisan larascan --format=sarif --output=larascan.sarifThe published workflow already does this and uploads the report via github/codeql-action/upload-sarif (the workflow grants security-events: write; private repos need GitHub Advanced Security β remove the upload step if unavailable).
Severity mapping:
| Larascan severity | SARIF level | security-severity |
|---|---|---|
| critical | error | 9.8 |
| high | error | 8.0 |
| medium | warning | 5.5 |
| low | note | 3.0 |
| info | note | 0.0 |
Findings without a file (config-level checks like config.app-debug) are anchored to composer.json:1 so GitHub doesn't drop them; those results carry a larascan.synthesizedLocation property.
A mature codebase will light up on the first scan. Rather than fixing everything before CI can go green, record the current findings as a baseline so CI only fails on new findings:
php artisan larascan:baseline # writes larascan-baseline.json
git add larascan-baseline.json && git commit -m "Add larascan baseline"From then on, plain php artisan larascan runs suppress baselined findings β they're counted (N baselined) rather than hidden, so you can still see them β and only findings that aren't in the baseline count toward the --fail-on threshold. Chip away at the baselined findings over time; re-run larascan:baseline to shrink the file.
Baseline identity is line-insensitive: a finding is matched on a hash of its check id, file and normalized message, so unrelated edits that shift line numbers don't break the baseline. When the source has changed enough that baselined findings no longer occur, the scan reports N stale baseline entries with a hint to re-run the command and prune them.
php artisan larascan --baseline=path/to/baseline.json # override the path
php artisan larascan --no-baseline # ignore the baseline entirelyPath resolution order: --baseline flag, then config('larascan.baseline'), then an implicit larascan-baseline.json in the project root if present. An explicitly named baseline (flag or config) that's missing or invalid is an error; the implicit default may simply be absent.
Static checks confirm the config is right; they can't tell you whether a middleware actually runs or whether a proxy strips a header on the way out. larascan:probe sends one real HTTP GET to the running app and verifies the security headers and cookie flags are actually present in the response:
php artisan larascan:probe --url=https://staging.example.testThe target URL resolves from --url, then config('larascan.probe.url') (env LARASCAN_PROBE_URL), then app.url. The probe checks HSTS, X-Content-Type-Options, X-Frame-Options, Referrer-Policy, CSP, cookie Secure/HttpOnly/SameSite flags, Server/X-Powered-By disclosure, and the httpβhttps redirect. These are reported under probe.* check ids, distinct from the static headers.* checks.
Findings against local targets (localhost, 127.0.0.1, *.test, *.local) are downgraded to Info β probing a dev box shouldn't fail CI.
| Flag | Description |
|---|---|
--url=URL |
Target URL (overrides config/app.url) |
--fail-on=SEVERITY |
Severity threshold for non-zero exit (default from larascan.fail_on, else high) |
--probe=PATTERN |
Filter probes by id pattern, repeatable (e.g. --probe=probe.cookie*) |
--timeout=SECONDS |
Request timeout (default 5) |
--insecure |
Skip TLS certificate verification |
--ignore-errors |
Exit 0 even when the request fails |
--only-failed |
Hide passed and skipped probes |
--format=human|json |
Output format (json auto-selected for agents) |
88 checks across 17 categories. Some require optional packages β those checks self-skip when the package isn't installed.
Show all 88 checks
Config (config.*) β 10
config.app-debugβ APP_DEBUG must be false in productionconfig.app-keyβ APP_KEY must be setconfig.app-envβ APP_ENV must not be a development value in productionconfig.env-not-committedβ .env must be gitignored and never committedconfig.env-example-syncβ .env and .env.example must share key setsconfig.env-calls-outside-configβ env() calls outside config/ defeat config cachingconfig.log-levelβ Default log channel must not be at debug in productionconfig.debug-blacklistβ debug_blacklist must redact sensitive env keys when debug is onconfig.trusted-proxiesβ Trusted proxies must not be wildcardconfig.mail-smtp-encryptionβ Remote SMTP mailers must force TLS encryption
Cookies & sessions (cookies.*) β 7
cookies.session-secureβ SESSION_SECURE_COOKIE must be true in productioncookies.session-http-onlyβ SESSION_HTTP_ONLY must be truecookies.session-same-siteβ SESSION_SAME_SITE must be lax or strictcookies.session-encryptβ session.encrypt should be truecookies.session-lifetimeβ session.lifetime must be within a reasonable rangecookies.encrypt-middlewareβ EncryptCookies middleware must be registeredcookies.encrypt-excludesβ Sensitive cookies must not be in EncryptCookies::$except
Headers (headers.*) β 8
headers.cors-wildcardβ CORS allowed_origins must not be wildcard with credentials enabledheaders.hstsβ HSTS header middleware must be active in productionheaders.x-content-type-optionsβ X-Content-Type-Options: nosniff middleware must be activeheaders.x-frame-optionsβ X-Frame-Options or frame-ancestors must be setheaders.referrer-policyβ Referrer-Policy header middleware should be activeheaders.csp-definedβ CSP middleware must be active (requires spatie/laravel-csp)headers.csp-unsafe-inlineβ CSP must not use unsafe-inline or unsafe-eval (requires spatie/laravel-csp)headers.csp-base-uriβ Spatie CSP policy must include abase-uridirective
Auth (auth.*) β 10
auth.bcrypt-roundsβ BCRYPT_ROUNDS must be 12 or higherauth.sanctum-expirationβ Sanctum tokens must have an expiration (requires laravel/sanctum)auth.login-throttleβ Login routes must have throttle middlewareauth.password-column-plainβ User model must hide or hash the password columnauth.signed-routes-verifyβ Email verification routes must use signed middlewareauth.api-ability-scopingβ Sanctum tokens must be created with explicit abilities (requires laravel/sanctum)auth.signed-url-no-paramsβ Signed URLs must include user-bound route parametersauth.otp-rate-limitingβ OTP/2FA verification routes must havethrottle:middlewareauth.registration-rate-limitβ Registration routes must havethrottle:middlewareauth.jwt-missing-expirationβ Tymon JWTjwt.ttlmust not be null or 0
CSRF (csrf.*) β 2
csrf.middleware-disabledβ VerifyCsrfToken middleware must be registeredcsrf.except-suspiciousβ CSRF except list must not contain wildcard patterns
Routing (routing.*) β 2
routing.state-mutating-getβ GET routes must not invokedestroy/delete/remove/deactivate/disablecontroller methodsrouting.api-http-onlyβ API routes underapi/*must enforce HTTPS whenAPP_URLishttp://
Models (models.*) β 4
models.unguardedβ Eloquent models must not use$guarded = []models.unguard-callβ No staticModel::unguard()calls in application codemodels.foreign-key-fillableβ Foreign key columns should not be in$fillablemodels.force-fill-user-inputβforceFill()calls bypass mass-assignment protection
SQL (sql.*) β 5
sql.raw-user-inputβ DB::raw / whereRaw / selectRaw with user inputsql.raw-order-byβ orderByRaw with user inputsql.variable-table-columnβ Variable arguments to DB::table / from / selectsql.validation-rule-injectionβ Validation rules from variable sourcesql.orwhere-scope-bypassβ->orWhere(...)must not be chained directly off->where(...)outside a closure group
XSS (xss.*) β 4
xss.blade-unescapedβ Blade{!! $var !!}with PHP variables risks XSSxss.html-stringβIlluminate\Support\HtmlStringproduces unescaped HTMLxss.url-javascript-protocolβjavascript:URLs in href/src are XSS sinksxss.htmlstring-castβ Eloquent$casts/casts()must not cast attributes toHtmlString::class
Files (files.*) β 5
files.path-traversalβ Storage/File operations with user-controlled pathsfiles.unlink-user-inputβunlink()/rmdir()in application codefiles.upload-mimes-validationβ Validation by extension rather than MIMEfiles.public-executable-uploadsβ Upload rules allowing .php/.phtml/.pharfiles.disk-visibilityβ Public-visibility disk with a sensitive name/root, or an s3 disk with no explicit visibility
Injection (injection.*) β 5
injection.commandβexec/shell_exec/system/passthrucallsinjection.process-shellβProcess::fromShellCommandline()usageinjection.unserializeβunserialize()of any inputinjection.open-redirectβredirect()with user-controlled URLinjection.host-headerβapp.urlmissing or pointing to localhost
Crypto & secrets (crypto.*) β 5
crypto.weak-hashβ md5/sha1 for security purposescrypto.weak-randomβ rand/mt_rand/uniqid for security tokenscrypto.cipher-not-pinnedβconfig/app.phpdoes not pin the ciphercrypto.hardcoded-secretβ High-entropy secrets or known token patterns in codecrypto.password-self-generatedβ Weak generators (Str::random,md5,uniqid,random_bytes,bin2hex) must not be used in password contexts β useStr::password()
Dependencies (dependencies.*) β 4
dependencies.composer-auditβ wrapscomposer auditfor PHP CVE detectiondependencies.npm-auditβ wrapsnpm auditwhen apackage.jsonis presentdependencies.minimum-stability-devβ composer.json minimum-stability is 'dev' without prefer-stabledependencies.outdated-phpβ PHP version at or near end-of-life
PHP (php.*) β 5
php.expose-phpβ expose_php must be offphp.display-errorsβ display_errors must be off in productionphp.allow-url-fopenβ allow_url_fopen should be offphp.public-sensitive-filesβ No .env / .git / .sql backups in public/php.phpinfoβ Nophpinfo()calls in application code
Logging (logging.*) β 3
logging.dd-dump-debugβ Nodd()/dump()/var_dump()in application codelogging.custom-error-pagesβresources/views/errors/500.blade.phpand503.blade.phpmust existlogging.sensitive-in-log-contextβ Log context arrays must not contain password/token/secret keys
Repo & CI (repo.*) β 4
repo.dependabotβ.github/dependabot.ymlshould exist for automated dep updatesrepo.gitleaks-historyβ No high-entropy secrets in git history (last 100 commits)repo.debug-toolbarsβ Debug packages (debugbar, telescope) must be inrequire-devonlyrepo.security-txtβpublic/.well-known/security.txtshould exist so researchers know how to report issues
Ecosystem packages (ecosystem.*) β 5
ecosystem.telescope-productionβ Telescope must not be enabled in production without an explicitviewTelescopegate (requires laravel/telescope)ecosystem.horizon-gateβ HorizonviewHorizongate must not be trivially true, and must be defined in production (requires laravel/horizon)ecosystem.pulse-gateβ PulseviewPulsegate must not be trivially true, and must be defined in production (requires laravel/pulse)ecosystem.debugbar-enabledβ Debugbar must not be enabled at runtime in production (requires barryvdh/laravel-debugbar)ecosystem.livewire-upload-rulesβ Customized Livewire temporary uploads must keep amax:size rule and not strip throttle middleware (requires livewire/livewire)
- PHP 8.3+
- Laravel 11 / 12 / 13
See CONTRIBUTING.md. Tests must pass, PHPStan must be clean at level 8, Pint must be clean.
If you discover a security issue, please email hello@baspa.dev instead of opening a public issue.
- Enlightn β the original Laravel performance + security scanner. Its analyzer-per-check pattern and report card concept shaped how LaraScan is structured.
- Securing Laravel β Stephen Rees-Carter's writing and newsletter, the practical reference for what to check and why it matters.
The MIT License (MIT). See LICENSE.md.
