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=true

None 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 --dev

Install 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
RequirementSupported
PHP7.4 and 8.x
Laravel8, 9, 10, 11, 12, 13
LicenseMIT

Running a Scan

php artisan guarddog:scan

That 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=critical

Run 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 riskDB::statement(), DB::raw() and whereRaw() 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 $guarded or no $fillable, where a crafted request can set columns you never intended.
  • Dangerous functionseval(), exec(), shell_exec() and system().

Configuration

  • APP_DEBUG=true in a production environment.
  • A weak or missing APP_KEY.
  • Default database credentials left in place.
  • .env committed 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-stability set to dev, 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:

SeverityCostMeaning
Critical−15Exploitable now. Fix before the next deploy.
Warning−5Risky pattern, or exploitable given one more mistake.
Notice−1Hardening 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-html

Start 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 auth middleware. 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.

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.