DEV Community

Cover image for What a Filesystem Scan Can and Cannot Tell You After a Laravel Intrusion
Harry Agustiana
Harry Agustiana

Posted on

What a Filesystem Scan Can and Cannot Tell You After a Laravel Intrusion

Most Laravel security tools look at your source code, config, and dependencies before you deploy. That is useful, but it answers one question: is my code safe? After a compromise, you need a different question answered: has anything on this server been added or changed that should not be there?

That is the gap Laravel Scalpel was built for. It is an open-source intrusion evidence scanner that runs inside a Laravel app and inspects the deployed filesystem.

A quick update first. Scalpel is now at v1.11.0 with 66 GitHub stars, and it was recently covered by Laravel News. More important to me, issues and pull requests are now coming from other developers, and the repo has contributors besides me. This post covers how the scanner works and one lesson from the Laravel News test that is worth knowing if you plan to run it in CI.

What it checks

php artisan scalpel:scan runs six scanners by default. Five look at the current files, and one compares them with a saved snapshot.

Structural. Executable PHP files in places PHP should never live, by default public/ and storage/. This covers .php and less common extensions like .phtml and .phar, plus double extensions such as shell.php.jpg.

Obfuscated code. Common backdoor patterns such as eval(base64_decode(...)), compressed payload execution, dynamic function calls, direct evaluation of request input, and long encoded strings.

.htaccess and .user.ini. Handler mappings that let the web server run scripts, external redirects, and PHP directives like auto_prepend_file. That last one is a classic persistence trick, because it runs a hidden file on every request.

Environment. A missing or unreadable .env, a .env under public/, an empty APP_KEY, keys that drift from .env.example, and APP_DEBUG=true in production.

Baseline diff. Record a known-good state, then report added, modified, and deleted files later.

php artisan scalpel:baseline   # record SHA-256, size, and mtime
php artisan scalpel:diff       # compare the current files against it
Enter fullscreen mode Exit fullscreen mode

Create the baseline only when you trust the application state, and turn on HMAC signing before you create the first one. A signed baseline lets scalpel:diff detect a baseline that was regenerated without the signing key.

The limit you should understand first

Scalpel runs in the same process and with the same permissions as your application. If an attacker can change your code, they can also change the scanner or its config. A filesystem scan detects evidence of an intrusion. It is not a firewall and it does not contain anything.

For that reason, run it from an external trigger where possible, keep code directories read-only, and ship results to storage outside the server being scanned.

What an independent test found

The Laravel News review ran Scalpel 1.9.0 on a fresh Laravel app. With a clean state and a baseline, the default scan reported nothing. After adding four harmless fixtures (a fake eval(base64_decode(...)) inside if (false), an avatar.php.jpg, an .htaccess handler mapping, and a .user.ini with auto_prepend_file), the content scanners caught them.

The more interesting result was a false positive pattern. After php artisan optimize compiled the framework views, the next scan reported about 100 MEDIUM and two HIGH findings inside storage/framework/views. The structural scanner allows that directory, but the obfuscated-code scanner still reads the compiled files.

If your app caches views in production, test this before you use Scalpel as a deployment gate. You can add storage/framework/views to content_scan_excluded_paths, but that means skipping content checks for every compiled view. Alternatively, run php artisan optimize:clear and recreate the baseline before scanning.

Using it in CI

Both scalpel:scan and scalpel:diff support table, JSON, GitHub Actions annotation, and SARIF output. --fail-on sets the lowest severity that fails a job.

php artisan scalpel:scan --format=sarif --fail-on=MEDIUM
Enter fullscreen mode Exit fullscreen mode

Exit code 0 means a complete scan with no findings, 1 means findings at or above your threshold, and 2 means findings below it or an incomplete scan, so an unreadable directory never looks like a clean result. A ScanFinished event is also dispatched, so you can send your own Slack, mail, or webhook alert without parsing command output.

Try it

composer require hryagstn/laravel-scalpel
php artisan vendor:publish --tag=scalpel-config
Enter fullscreen mode Exit fullscreen mode

It needs PHP 8.2+ and supports Laravel 10 to 13. If you hit a false positive or a case it misses, please open an issue. Real-world reports like the one above are what make the scanner better.

Top comments (0)