aidantwoods/secureheaders
SecureHeaders is a PHP library for adding security headers to HTTP responses. It helps configure CSP, HSTS, X-Frame-Options, Referrer-Policy, and related headers with sensible defaults and a fluent API, making it easier to harden web apps against common browser-side attacks.
Install via Composer and verify the resolved version meets your PHP constraint (this package predates PHP 8, so test on your runtime before committing):
composer require aidantwoods/secureheaders
Look first at the README quickstart and the examples/ directory — the library exposes two entry points: a "just send headers" procedural flow and a PSR-7 flow. For a legacy or front-controller app, the minimal first use case is three lines at the very top of public/index.php, before any output:
use Aidantwoods\SecureHeaders\SecureHeaders;
$headers = new SecureHeaders();
$headers->errorReporting(true); // dev only — surfaces invalid/conflicting config
$headers->strictMode(); // opinionated defaults: CSP, HSTS, nosniff, frame options, referrer policy
$headers->apply(); // compute final header set
$headers->output(); // emit via header()
If you're on PSR-7 (Slim, Laminas, Laravel via a bridge), skip output() and hand the library a response instead:
$response = $headers->applyTo($response); // returns the response with headers applied
The core objects you'll touch day-to-day are SecureHeaders (policy builder), Header (immutable name/value pair), and — for framework work — whatever PSR-7 ResponseInterface you already produce. Inspect what the library decided before sending with $headers->getHeaders(), which returns an array of Header objects; this is the single best debugging affordance in the package.
Framework middleware. In Laravel, wrap the library in a global or web-group middleware so every response is covered:
public function handle($request, Closure $next)
{
$response = $next($request);
$headers = new SecureHeaders();
$headers->errorReporting(app()->environment(['local', 'testing']));
$headers->strictMode();
$headers->addHeader('Permissions-Policy', 'geolocation=(), camera=(), microphone=()');
return $headers->applyTo($response);
}
Because PSR-7 responses are immutable, always reassign the return value — do not rely on in-place mutation.
Layered policies. Start from strictMode() for the global baseline, then add or remove specific directives per route group. Admin panels, API routes, and public marketing pages rarely want the same CSP. Keep a small helper that clones the base config and applies overrides so you don't re‑implement the defaults per controller.
Custom CSP with nonces. strictMode() will block inline scripts and styles, which is the correct posture but will break most existing templates. Migrate by generating a per-request nonce, adding it to the script-src/style-src directives via addHeader(), and threading the same value into the rendered <script nonce="..."> tags. This requires the nonce to be generated before the header is finalized and before the body is rendered — usually meaning the middleware seeds a request attribute that the view layer reads. Pages using nonces must not be full-page cached without varying on them.
Report-only rollout. For staging, add Content-Security-Policy-Report-Only with a report-uri/report-to directive alongside (or instead of) the enforcing header. This lets you discover the inline scripts and third-party origins you missed before users do, and it's the cheapest way to tune a policy on a mature app.
Testing. In PHPUnit, assert against getHeaders() rather than header() (which is a no-op under CLI). A one-line helper that maps Header objects to name: value strings makes snapshot assertions trivial and catches accidental directive regressions when someone edits the global config.
Cookie handling. Where the package rewrites Set-Cookie to add SameSite, it can only see headers that exist at the time it runs. Set all cookies before the middleware applies, or set the attribute manually with setcookie() options — mixing the two is a common source of inconsistency.
frame-ancestors/report-to modernizations. It still works, but treat it as a stable-but-frozen dependency: pin the version, don't expect fixes for new browsers, and plan to add newer headers yourself via addHeader(). Audit against PHP 8.x and PSR-7 v1 vs v2 before upgrading your stack.apply() then output() is not optional. Calling output() without apply() (or the reverse in the wrong order) ships an empty or stale header set. If headers suddenly vanish, this ordering is the first thing to check.output() calls PHP's header(), which silently fails once the body has started. In legacy apps, ensure ob_start() is active or that the middleware runs early enough. The same applies to whitespace or a UTF‑8 BOM in an included file — the classic "headers already sent" trap.errorReporting(true) throws/warns in production. Wire it to environment detection only; leave it off in prod so a bad directive logs rather than a 500.Strict-Transport-Security for a host you serve over plain HTTP in development — browsers will pin https://localhost and refuse to let you back in. Gate it on environment, not just on strictMode().Content-Security-Policy values that combine into something stricter than intended. Use forceHeader()/removeHeader() deliberately when overriding a value set elsewhere.X-Frame-Options can disagree. Modern browsers prefer frame-ancestors; keep the two consistent or you'll get confusing embedding behavior that varies by browser.addHeader, removeHeader, and forceHeader are the intended escape hatches for anything the library doesn't model natively — that's where COOP, COEP, Cross-Origin-Resource-Policy, and Permissions-Policy belong. Wrap them in your own config so they travel with the library's output and get tested together.curl -I https://staging.example.com is the ground truth; framework middleware ordering, proxy header rewriting, and CDN overrides can all undo what the library computed, and none of that shows up in getHeaders().How can I help you explore Laravel packages today?