owasp/csrf-protector-php
OWASP CSRF Protector for PHP helps defend web apps against cross-site request forgery using token generation, validation, and configurable per-session or per-form protection. It integrates into existing PHP projects to secure state-changing requests.
First, decide whether you need it. A normal Laravel app already has CSRF protection through VerifyCsrfToken and @csrf. This package is a standalone, session-based PHP library; it is not a Laravel package, has no service provider, no Blade integration, and does not know about Laravel’s session store. Use it only when you have a concrete reason: a legacy PHP entry point mounted inside/alongside Laravel, a non-Laravel app sharing the codebase, or a compliance requirement that explicitly names this library.
Minimal integration steps
Install and inspect before wiring anything:
composer require owasp/csrf-protector-php
Then read, in this order: README.md, examples/, and the sample config file (csrfp.config.php or similarly named in the release). Confirm the actual entry points and whether the library uses native $_SESSION, $_POST, and session_start() directly. That determines the entire integration strategy.
Check the license. Packagist reports NOASSERTION; confirm the repository license file before shipping it in a commercial product.
Create a Laravel config wrapper rather than editing vendor files:
// config/csrfprotector.php
return [
'token_name' => env('CSRF_PROTECTOR_TOKEN_NAME', 'csrf_token'),
'log_path' => storage_path('logs/csrf-protector.log'),
'enabled' => env('CSRF_PROTECTOR_ENABLED', true),
];
As of v1.0.2 the library’s default logger is PHP’s error_log instead of a file writer. log_path is only meaningful if you explicitly re-enable/configure the file-based logger; otherwise output goes wherever PHP’s error_log is pointed (see Gotchas).
Register a small service provider/singleton that loads the library and applies your config. Keep the library’s global state isolated. If you want the old file-based behavior, wire it here — do not rely on the release default.
Pick your first use case: protect one legacy route group or one legacy form, not the whole application. For example, a legacy admin script served outside Laravel’s web middleware, or a Blade form that posts to a legacy controller. Add a hidden field:
<input type="hidden"
name="{{ config('csrfprotector.token_name') }}"
value="{{ $csrfToken }}">
and validate it in a dedicated middleware before the request reaches the legacy code.
Register the middleware narrowly:
// Laravel 11: bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
$middleware->alias([
'legacy.csrf' => \App\Http\Middleware\LegacyCsrf::class,
]);
})
Route::middleware(['legacy.csrf'])->group(function () {
Route::post('/legacy/admin/action', [LegacyController::class, 'action']);
});
The first thing to verify is not the token generation, but the session boundary. If the library calls session_start() and reads $_SESSION, it will not see Laravel’s session data by default.
Pattern 1: Isolated legacy bridge (recommended when using the package as-is).
Run the legacy code and the CSRF library against their own native PHP session, with a distinct session name and cookie path so it cannot collide with Laravel’s session cookie. Put those routes outside Laravel’s web group, or explicitly remove Laravel’s session and CSRF middleware for that group. This is the least surprising way to use a standalone library.
Route::withoutMiddleware([
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
])->middleware(['legacy.csrf'])->group(function () {
Route::post('/legacy/endpoint', [LegacyController::class, 'handle']);
});
Pattern 2: Laravel-session adapter.
If you must keep Laravel’s session, do not start a second native session. Instead, write an adapter that hydrates $_SESSION from Laravel’s session before calling the library and copies any regenerated token back afterward. This is brittle but workable if the library exposes explicit getToken()/verifyToken() methods. Avoid relying on the library’s automatic request interception in this mode, because it reads $_POST and may terminate the request directly.
public function handle(Request $request, Closure $next)
{
if ($request->isMethodSafe()) {
return $next($request);
}
$_SESSION = $request->session()->all();
$token = $request->input(config('csrfprotector.token_name'));
if (! \CSRFProtector::verifyToken($token)) {
abort(403, 'Invalid CSRF token');
}
$request->session()->put($_SESSION);
return $next($request);
}
(Confirm the exact method names against the release’s examples/; the API is small but older releases vary.)
Pattern 3: Blade directive or component for token injection.
Wrap token rendering once so forms do not drift:
// AppServiceProvider::boot()
Blade::directive('legacyCsrf', function () {
return "<?php echo '<input type=\"hidden\" name=\"'
. config('csrfprotector.token_name')
. '\" value=\"' . e(app(\App\Services\LegacyCsrf::class)->token()) . '\">'; ?>";
});
<form method="POST" action="/legacy/admin/action">
@legacyCsrf
...
</form>
Pattern 4: AJAX and meta-tag workflow.
For legacy pages that submit via JavaScript, emit the token in a meta tag and send it in a header. Keep the header name identical across frontend and middleware:
<meta name="csrf-token" content="{{ $csrfToken }}">
fetch('/legacy/endpoint', {
method: 'POST',
headers: {
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content,
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
});
If the library only checks $_POST, this pattern requires an adapter; otherwise the JSON body will bypass validation.
Pattern 5: Selective middleware on unsafe methods only.
Do not run validation on GET, HEAD, or OPTIONS. Decide middleware order deliberately: session first, CSRF second, auth third for legacy admin routes, so unauthenticated users do not get misleading CSRF errors.
Pattern 6: Testing workflow.
For feature tests, either disable the middleware with $this->withoutMiddleware(LegacyCsrf::class) for unrelated tests, or seed the exact session key the library expects and post the matching token. If the library uses native sessions, test through the real HTTP/session stack rather than Laravel’s session helpers. Since v1.0.2 logs via error_log, failures in tests can spill into your PHPUnit output — set error_log in phpunit.xml (or stub the logger) so assertions on output and CI logs stay clean.
VerifyCsrfToken. You will get double validation, confusing 419/403 responses, and two token names in forms. Scope it to legacy routes or remove Laravel’s middleware for those routes.Session store is not $_SESSION. A library that calls session_start() will create a second session cookie, and tokens generated in one store will not validate in the other. Choose either an isolated native session or an explicit adapter, not both accidentally.$_POST is not Laravel’s request input. Middleware inspecting $request->input() and library code inspecting $_POST can disagree, especially for JSON, PUT/PATCH, and multipart requests. Normalize the token source once, or restrict legacy CSRF routes to classic form posts.@csrf emits _token; this library commonly expects csrf_token or a configured name. A form can look protected while posting the wrong field. Centralize the field name in config and a Blade component.exit or emit a raw 403. That breaks JSON clients, testing, and Laravel’s error reporting. Wrap validation in your own middleware and throw abort(403) / ValidationException instead where possible.error_log (v1.0.2). The file-based default logger is gone. Output lands wherever PHP’s error_log ini setting points: for PHP-FPM/nginx that is usually the FPM error log or /var/log/php*.log, not storage/logs/laravel.log, and it bypasses Laravel’s Monolog channels. Consequences:
log_path entry in your config wrapper is now inert unless you explicitly re-enable file logging; don’t assume tokens/debug lines appear there.display_errors/error_log, verbose CSRF diagnostics (token mismatch, missing session) can leak to the browser or a world-readable log. Keep logging off in production and verify error_log is not web-served.Log::channel(...) instead of relying on the library default.csrf-protector.log still has a source.LegacyCsrf::token(), LegacyCsrf::validate()), a Blade directive, and one middleware. That gives you a single place to swap the implementation later and avoids scattering vendor-specific calls through controllers.error_log target (not storage/logs) for the v1.0.2 default logger, then check proxy/HTTPS settings.How can I help you explore Laravel packages today?