Upgrading#
This page walks an app from one HELMET release to the next, newest first, using only what CHANGELOG.md says. One section per released version; versions with a break get Breaking, Deprecated, Steps and How to verify. The current release is 0.3.0.
0.3.0 (2026-11-03)#
Breaking#
This is a pre-1.0 minor: apps that pin ^0.2 stay on 0.2.x until they move their constraint to ^0.3. The proxy and
forwarded-header changes make some requests resolve differently, so read the Security lines first:
- An explicitly empty
HELMET_PROXIES=now trusts nobody even whenHELMET_PRESETis set, because only an unset variable falls back to the preset, so an app that clears the list on purpose is not silently re-opened by the platform default. ClientIpfails closed on an unparseableX-Forwarded-Forentry (for example203.0.113.5:443): it returns the peer instead of walking on to an earlier, client-chosen entry, so a malformed header can no longer steer the resolved client IP.- IPv4-mapped IPv6 addresses (
::ffff:a.b.c.d) are unmapped before matching trusted proxies, for both the peer and the forwarded entries, so a dual-stack proxy still matches its IPv4 entry instead of slipping past the trust check. X-Forwarded-Protonow follows one rule everywhere, the last non-empty value inHttpsDetectorand the Laravel normalizer, and the normalizer decides trust with the list Laravel itself uses (trustedproxy.proxieswhen set, elsehelmet.proxies, with*and**trusting every peer as in Symfony), so HELMET and the framework can no longer disagree about which value to read or from whom to accept it.- The CSP nonce meta tag is injected only into the real
<head>, so a literal<head>inside a comment, an attribute value, or a script, style, textarea or title element no longer receives it. - An HTML response that carries a CSP nonce has
publicchanged toprivateands-maxagedropped from itsCache-Control, so a shared cache cannot store a page whose nonce differs per request.
Deprecated#
None listed in the changelog for this release.
Steps#
-
Move the constraint in
composer.jsonfrom^0.2to^0.3, then update:composer update waynestate/helmet -
Check your environment for an explicitly empty
HELMET_PROXIES=. If you want the preset's proxies, unset the variable entirely; if you want to trust nobody, keep it empty. -
On Citadel, confirm the
citadelpreset's NAT assumption:172.18.0.1is the Docker bridge gateway, the preset is safe only when the front proxy reaches the app over that bridge, and a published app port that is source-NATed to the gateway address would make a direct client look like the proxy, so the platform must not publish the app's port. -
If you rely on
X-Forwarded-Forvalues, expect the fail-closed behavior: an unparseable entry makes the resolved client IP the peer, and the warninghelmet.forwarded_for_rejectedis logged withevent(forwarded_for_rejected),reason(unparseable_entry) andpeerin the context. The Origin value, cookies, Authorization, the header and the bad entry are never logged.
How to verify#
composer ci
That runs composer validate --strict, the test suite, Pint, PHPStan, the agent-ready check, composer docs:check
and composer audit. Then ask the load balancer's question:
curl -sI http://localhost/up
Expected: 200 ok, the baseline headers, and Strict-Transport-Security on a request that was really HTTPS. The
suite pins the new behavior in tests/Proxy/ClientIpTest.php, tests/Proxy/HttpsDetectorTest.php,
tests/Proxy/TrustedProxiesTest.php, tests/Headers/SecurityHeadersTest.php and
tests/Laravel/SecurityHeadersTest.php.
0.2.6 (2026-11-02)#
No breaks: HTML rewrites (CSP nonce meta, environment ribbon) keep the response's view object, so assertInertia()
and assertViewHas() work in apps that use HELMET, and the rest of the release is fixes, deploy-layer refreshes and
docs. Upgrade with:
composer update waynestate/helmet
0.2.5 (2026-10-29)#
No breaks: adds the agent-ready site (prompt blocks, Markdown twins, llms.txt) from Squire's kit and
HELMET_SESSION_SECURE=always, which keeps the session cookie Secure even when HTTPS is not detected, and moves the
proof page to a private site that needs a Wayne State sign-in. Upgrade with:
composer update waynestate/helmet
0.2.4 (2026-10-29)#
No breaks: HELMET_PRESET (Laravel) now also comes from the private waynestate/playbook package when the
environment has no value; the config reads it through Waynestate\Helmet\Support\Setting, which checks the
environment first, then Playbook, then gives up. A missing preset is still not an error, and waynestate/playbook
is suggested, never required. Upgrade with:
composer update waynestate/helmet
0.2.3 (2026-10-29)#
No breaks: a fix. helmet_boot() removed X-Powered-By only after it had already answered the health check path, so
a GET or HEAD to /up (the default health_path) still sent the header; the removal now runs before the health
answer and the request-forgery rejection, so both early exits leave the response clean. Upgrade with:
composer update waynestate/helmet
0.2.2 (2026-10-29)#
No breaks: every JSON log record now carries a request ID in extra.request_id, 16 lowercase hex characters
generated by HELMET (Logging::requestId()), stamped by a processor pushed before the redaction processor. An
inbound header is never trusted for the ID. Logging::create() takes an optional fourth argument for a
caller-supplied ID (8 to 64 letters, digits, dots, underscores or dashes; anything else is ignored), and
helmet_boot() accepts it as log.request_id, which must be a string, and now also returns the ID as request_id.
Upgrade with:
composer update waynestate/helmet
0.2.1 (2026-10-29)#
Breaking#
A raw-PHP app that still used the default PHPSESSID cookie gets the new WSUSESSID name (__Host-WSUSESSID on
HTTPS), so signed-in users sign in once more after deploying. Laravel apps are unaffected (Laravel names its own
cookie). The rename happens only while the name is still the PHP default PHPSESSID; an app-chosen name is left
alone, and SESAME reads whatever session_name() returns. SessionHardening::apply() also sets
cookie_lifetime=0 (a browser-session cookie), cache_limiter=nocache, cookie_path=/, an empty cookie_domain
(host-only) and the cookie name.
Deprecated#
None listed in the changelog for this release.
Steps#
-
Update the package:
composer update waynestate/helmet -
If your app chose its own session cookie name, nothing changes for it.
-
Expect one more sign-in for raw-PHP users after the deploy; tell app owners it is expected.
How to verify#
curl -sI http://localhost/up
Expected: the Set-Cookie name is __Host-WSUSESSID on HTTPS and WSUSESSID on HTTP. Pinned in
tests/Session/SessionHardeningTest.php.
0.2.0 (2026-10-29)#
Breaking#
- An unknown
HELMET_PRESETnow throws even whenHELMET_PROXIESis set. A typo in the preset name fails loudly at boot instead of going silently unused. - The trusted-header set in the Laravel adapter was narrowed.
X-Forwarded-Host,X-Forwarded-Prefixand nowX-Forwarded-Portare no longer trusted by default. Port is out because Citadel's HA-Proxy never sends it, so a client-sent value would pass through untouched and change the port Laravel reports and the URLs it generates. Apps that need any of them choose the set themselves withtrustProxies(at: ..., headers: ...)orTrustProxies::withHeaders(). - Laravel now agrees with HELMET on multi-value
X-Forwarded-Proto. Citadel's HA-Proxy appends the header instead of replacing it, so a client can producehttps, http. Symfony reads the first value of such a list (the client's), while HELMET reads the last (the one the closest proxy appended). A new middleware prepended to the global chain rewrites the header to its last value when the request comes from a trusted proxy; a single value is left alone, and requests from untrusted peers are ignored as before. - The
<head>match when injecting the csp-nonce meta tag was fixed: the old pattern also matched<header>and<headline>and injected the tag after the wrong element. Responses that carry aContent-Lengthheader now have it refreshed to the new body length after injection, so a stale length cannot cut the page off mid-tag.
Deprecated#
None listed in the changelog for this release.
Steps#
-
Update the package:
composer update waynestate/helmet -
Check
HELMET_PRESETfor typos; the only names arecitadelanddocker. -
If your app relied on
X-Forwarded-Host,X-Forwarded-PrefixorX-Forwarded-Port, choose the set yourself withtrustProxies(at: ..., headers: ...)orTrustProxies::withHeaders()inbootstrap/app.php.
How to verify#
composer ci
Then check the headers:
curl -sI http://localhost/up
Expected: 200 ok, the baseline headers, and the csp-nonce meta tag in the real <head> of an HTML response. The
suite pins the adapter behavior in tests/Laravel/SecurityHeadersTest.php and the core in
tests/Headers/SecurityHeadersTest.php.
0.1.0 (2026-10-27)#
First tagged release. There is nothing below it to upgrade from: install with
composer require waynestate/helmet
and follow the Quick start.