psalm/plugin-laravel
Laravel Psalm plugin for deep static analysis plus taint-based security scanning. Detect SQL injection, XSS, SSRF, shell injection, path traversal, and open redirects by tracking user input through Laravel code—without executing it. Complements Larastan/PHPStan.
composer.json allows dev/beta packages):
composer config minimum-stability dev && composer require --dev psalm/plugin-laravel:^4.8
psalm.xml (default strictness level 4):
./vendor/bin/psalm-laravel init
./vendor/bin/psalm-laravel analyze
For CI/CD: Use the one-liner to auto-generate GitHub Actions:
./vendor/bin/psalm-laravel add github
Analyze a vulnerable route (e.g., SQL injection via user input):
// app/Http/Controllers/SearchController.php
Route::get('/search', function (Request $request) {
$sortBy = $request->input('sort'); // Tainted source
User::where('name', 'John')->orderBy($sortBy)->get(); // Psalm flags tainted SQL
});
Expected Output:
ERROR TaintedSql: Detected tainted SQL in SearchController.php:12
Type-Centric Development
Auth::user() → User|null). Use Auth::guard('admin')->user() for narrowed types.where{Column} (e.g., User::whereName()) with type-safe arguments:
User::whereName('John')->get(); // Psalm infers `Collection<User>`
Security-Integrated CI
./vendor/bin/psalm --set-baseline=psalm-baseline.xml
errorLevel (e.g., 1 for strictest) in psalm.xml:
<errorLevel>1</errorLevel>
Custom Checks
ModelPropertyHandler for SET columns):
<fileList>
<directory name="app/Models" />
</fileList>
<plugins>
<pluginClass name="Psalm\Plugin\Laravel\Plugin" />
</plugins>
./vendor/bin/psalm-laravel analyze --testbench
Collection::macro('sum', fn() => ...)).Carbon::parse($date)->startOfDay(); // Psalm narrows to `Carbon`
False Positives in Taint Analysis
Js::from()/Js::encode() may flag legitimate uses.Js::from($request->input('safe_data'))->encode(); // No false alarm
Facade Resolution Gaps
--testbench flag or manually add stubs to psalm.xml:
<stubFiles>
<directory name="vendor/package/src/Stubs" />
</stubFiles>
Dynamic where{Column} Overhead
User::whereHas('posts.wherePublished')) may fail.User::whereHas('posts', fn($q) => $q->wherePublished(true));
./vendor/bin/psalm-laravel diagnose
resolveConfigReturnTypes if config() narrowing is too strict:
<param name="resolveConfigReturnTypes" type="bool">false</param>
Custom Taint Sources/Sinks
Add stubs for unsupported methods (e.g., File::put()):
// stubs/laravel/Storage.php
namespace Illuminate\Support\Facades;
class Storage {
#[TaintSource]
public function put(string $path, string $contents) { ... }
}
Eloquent Attribute Handling
Override ModelPropertyHandler for custom attributes:
// app/Providers/AppServiceProvider.php
Psalm\Plugin\Laravel\Plugin::addModelPropertyHandler(
\App\Models\User::class,
fn($model, $property) => match($property) {
'full_name' => 'string',
default => null,
}
);
CI Optimization
Cache Psalm storage between runs (.psalm-cache):
# .github/workflows/psalm.yml
jobs:
psalm:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: composer install
- run: ./vendor/bin/psalm-laravel analyze --no-cache
How can I help you explore Laravel packages today?