The Problem It Solves
Every Laravel application accumulates the same handful of security mistakes. Not exotic ones — the boring, well-documented ones that ship anyway because nobody went looking:
// A whereRaw that interpolates request input
$users = DB::table('users')->whereRaw("email = '{$request->email}'")->get();
// Unescaped Blade output on user-supplied content
{!! $comment->body !!}
// A model with no mass-assignment protection at all
class User extends Model
{
protected $guarded = [];
}
// APP_DEBUG left on in production — full stack traces to the public
APP_DEBUG=trueNone of these are hard to find if you know to look. The difficulty is that they are scattered across hundreds of files, and a manual review takes a day you do not have. GuardDog automates the first pass so the review starts from a list instead of from nothing.
Laravel GuardDog is a free, MIT-licensed dev dependency. It runs entirely locally, reads your code without modifying anything, and needs no configuration to be useful. It has crossed 2,300 downloads on Packagist.
Installing It
composer require jaydeep/laravel-guarddog --devInstall it with --dev. It is an analysis tool, not a runtime component — there is no reason for it to be in your production dependency tree.
The service provider is auto-discovered, so that is the whole installation. If you want to customise which checks run, publish the config:
php artisan vendor:publish --tag=guarddog-config| Requirement | Supported |
|---|---|
| PHP | 7.4 and 8.x |
| Laravel | 8, 9, 10, 11, 12, 13 |
| License | MIT |
Running a Scan
php artisan guarddog:scanThat scans the project, prints a summary to the console, and writes a self-contained HTML report you can open in a browser or attach to a ticket. Each finding names the file, the line, the severity and a specific fix — not a generic OWASP link.
The other three invocations cover the cases you will actually hit:
# Console only — no report file written
php artisan guarddog:scan --no-html
# Put the report somewhere specific
php artisan guarddog:scan --output=storage/reports/security.html
# Exit non-zero when a critical issue is found — for CI
php artisan guarddog:scan --fail-on=criticalRun it once on an existing project before you change anything. The first report is usually longer than expected, and the point is to get a baseline you can drive down — not to fix everything the same afternoon.
What It Checks
The checks fall into four groups, because the four groups fail in different ways.
Code-level
- Raw SQL injection risk —
DB::statement(),DB::raw()andwhereRaw()that interpolate rather than bind. - Unescaped Blade output —
{!! !!}on values that could carry user input, which is the standard route to stored XSS. - Mass assignment — models with an empty
$guardedor no$fillable, where a crafted request can set columns you never intended. - Dangerous functions —
eval(),exec(),shell_exec()andsystem().
Configuration
APP_DEBUG=truein a production environment.- A weak or missing
APP_KEY. - Default database credentials left in place.
.envcommitted to git — still one of the most common real-world breaches.- Session and cookie security flags that are not set.
Routes and middleware
- Routes with no authentication middleware that look like they need it.
- CSRF exclusions in
VerifyCsrfToken— each one is a deliberate hole worth re-justifying. - Overly permissive CORS, in particular a wildcard origin combined with credentials.
Dependencies
- Packages with known CVEs.
minimum-stabilityset todev, which quietly opens you to unreviewed upstream code.
How the Score Works
Findings roll up into a single number out of 100, so you can track it over time and put it in a pull request comment:
| Severity | Cost | Meaning |
|---|---|---|
| Critical | −15 | Exploitable now. Fix before the next deploy. |
| Warning | −5 | Risky pattern, or exploitable given one more mistake. |
| Notice | −1 | Hardening opportunity. |
A score above 80 is a healthy project; above 90 is genuinely good. The number is a trend line, not a certificate — a 95 with one unfixed critical is worse than an 82 with thirty notices.
Configuring It
// config/guarddog.php
return [
'enabled_checks' => [
'sql_injection',
'unescaped_blade',
'mass_assignment',
'debug_mode',
],
'exclude_paths' => [
'database/seeders',
'database/factories',
],
'fail_on' => 'critical', // critical | warning | notice
];exclude_paths is the setting you will reach for first. Seeders and factories legitimately contain hardcoded values and raw SQL, and flagging them every run trains people to ignore the report — which is the failure mode that matters most for a tool like this.
Resist the urge to disable a check because it is noisy. If unescaped_blade produces forty findings, that is a real answer about the codebase. Exclude the specific paths that are genuinely safe and leave the check on.
Wiring It Into CI
A scanner you have to remember to run is a scanner you stop running. Put it in the pipeline:
# .github/workflows/security.yml
name: Security
on: [push, pull_request]
jobs:
guarddog:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
- run: composer install --no-interaction --prefer-dist
- run: php artisan guarddog:scan --fail-on=critical --no-htmlStart with --fail-on=critical so the build only breaks on things that are genuinely exploitable. Once the critical count is at zero and staying there, tighten it to warning.
Locally, a composer script makes it a habit:
{
"scripts": {
"security": "@php artisan guarddog:scan",
"check": [
"@php artisan test",
"@php artisan guarddog:scan --fail-on=critical --no-html"
]
}
}What It Cannot Do
Being straight about this matters more for a security tool than for anything else.
- It is static analysis. It reads your code; it does not execute it. Anything assembled at runtime — a query built from a variable that came from three files away, a dynamic method call — can slip past.
- It does not understand your authorisation model. It can tell you a route has no
authmiddleware. It cannot tell you whether a user should be allowed to edit that particular record. Policies still need human review. - It is not a penetration test. It finds known bad patterns in source. It does not probe a running application, test business logic, or attempt anything.
- A clean report is not proof. It means the checks it runs found nothing, which is a genuinely useful signal and nothing more.
Treat it as the cheap first pass that clears the obvious problems, so your expensive human review time goes to the things a tool cannot judge.
Links
- Packagist: jaydeep/laravel-guarddog
- GitHub: JaydeepGadhiya/laravel-guarddog
- Node.js equivalent: GuardDog for Express — the same idea for Node, with a real AST-based analyser
- More of my work: jaydeepgadhiya.netlify.app
Issues and pull requests are welcome. A false positive is a bug worth reporting — include the snippet and the check name and it will get fixed.