All notable changes to this project are documented in this file.
Unreleased#
-
Docs: README and the raw PHP guide gain "Readiness is a separate check":
/upstays liveness-only, dependency checks go on a separate/readypath with a short timeout and a fixed 503 body, and Laravel apps keep them out of theDiagnosingHealthlistener; BEST_PRACTICES H34 isDocumented. -
Added: a Grafana dashboard for the HELMET metrics (
docs/grafana/helmet.json, uidhelmet-fleet) with panels for apps reporting, HELMET and PHP versions in use, PHP support status, apps by instance, and scrape health; it imports into any Grafana through aDS_PROMETHEUSdatasource input, andtests/Support/GrafanaDashboardTest.phppins its shape. The metrics guide gains a "Graph it in Grafana" section with a samplescrape_configsstanza.docs/was never export-ignored, so the dashboard already ships in the dist archive. -
Added: the docs site.
bin/docs-site.php(Squire's shared builder) turnsdocs/intosite/docs/: the Overview page carries the README opening and a generated table of everyHELMET_environment variable the package reads.composer docsrebuilds it,composer cichecks it throughcomposer docs:check, and the front page links it. -
Docs: BEST_PRACTICES H53 is
Documented(SESAME owns the logout headers,Clear-Site-Datawithout"cookies") and H41 isPartly enforced(three events are logged, authentication failures belong to SESAME). -
Docs: README section "Debug output stays off in production" names the two settings that decide whether a visitor sees a trace (PHP
display_errors, LaravelAPP_DEBUG), points at Squire's deployphp.ini, and documents thehelmet.warn_debugopt-out; BEST_PRACTICES H43 is nowDocumented. -
Changed: the front-page deploy layer pins HELMET ^0.3, PROCTOR ^0.4 and CREED ^0.1 (lock: HELMET 0.3.0, PROCTOR 0.4.0, CREED 0.1.6).
-
Changed: CI pins Preflight 0.1.4 (GitLab component includes and the GitHub reusable workflow at its tag commit); the deploy layer sets minimum-stability to stable.
-
Docs: the sample metrics output shows 0.3.0.
-
Added:
GenericErrorResponse::clientError(int $status)returns a fixed 4xx page (status, headers, body) for client faults, with its own title and sentence for 400, 401, 403, 404, 405, 410 and 429 and the Bad request text for any other 4xx; a status outside the 400s throwsInvalidArgumentException, and nothing from the request or an exception reaches the body. An app calls it from its router; nothing wires it intohelmet_boot()automatically.
0.3.0 (2026-11-03)#
-
This is a pre-1.0 minor: apps that pin
^0.2stay on 0.2.x until they move their constraint to^0.3. The proxy and forwarded-header changes below make some requests resolve differently (an emptyHELMET_PROXIES=now trusts nobody), so read the Security lines first. -
Docs: BEST_PRACTICES H14, H13 and H41 say what is tested and what is not (error page headers on the raw PHP path, WayneUI assets, session hardening events).
-
Added: opt-in generic error page for raw PHP.
helmet_boot(['error_handler' => ['enabled' => true]])installs an exception handler that logs the full detail aterrorlevel and shows a fixed 500 page with no exception text;Waynestate\Helmet\Error\GenericErrorResponse::build()returns the same response as data. Off by default; Laravel keeps its own handler. -
Added: two structured security events,
helmet.request_refused(warning, logged when the request-forgery guard answers 403, withevent,method,path,sec_fetch_siteandorigin_presentin the context) andhelmet.forwarded_for_rejected(warning, logged byClientIp::resolve()when it fails closed on an unparseableX-Forwarded-Forentry, withevent,reasonandpeerin the context); the Origin value, cookies, Authorization, the header and the bad entry are never logged, and a failing logger never changes the 403 or the resolved IP. -
Added:
helmet.debug_enabled_in_production(warning, logged once by the Laravel adapter at boot when the app boots withapp.debugtrue in the production environment, witheventin the context); opt out withhelmet.warn_debug(envHELMET_WARN_DEBUG, default true), and a failing logger never breaks the boot. -
Security:
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. -
Security: 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. -
Security:
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. -
Security: 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. -
Security: 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. -
Security: 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. -
Fixed: the Vite dev origin must be a bare origin (no path, query, fragment or userinfo) and is normalized to
scheme://host[:port]; a CSP directive the app removed is no longer recreated by the dev origin, which dropped itsdefault-srcfallback (it is copied fromdefault-srcfirst, or skipped). -
Fixed: the Laravel adapter resets Vite's CSP nonce when the request ends (after the mismatch check and the nonce meta tag, on success and when the inner handler throws), so a long-lived worker (Octane, Swoole, FrankenPHP) does not carry one request's nonce into the next.
-
Changed: CI pulls its Docker Hub images through the GitLab Dependency Proxy (the group's CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX), so pipelines no longer hit Docker Hub's anonymous pull limit.
-
Docs: the README and the
Presetsdocblock now state thecitadelpreset'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. -
Changed: docs and package metadata name nougat as the canonical repository, with git.wayne.edu and GitHub as read-only mirrors, until git.wayne.edu has CI runners.
0.2.6 (2026-11-02)#
- Fixed: HTML rewrites (CSP nonce meta, environment ribbon) keep the response's view object, so
assertInertia()andassertViewHas()work in apps that use HELMET. - Changed: the deploy layer's Squire kit is refreshed to 0.3.3 (release options can now live in ./.env.squire).
- Changed: the deploy layer's Squire kit is refreshed to 0.3.4 (the local .env.squire file is now git-ignored).
- Docs: BEST_PRACTICES.md tags re-checked against the code; H22, H24 and H28 are now Enforced, H32 is Enforced, and the gap list line numbers are current.
- Changed: composer.json sets config.audit.abandoned to report, making the Composer audit policy explicit.
- Docs: README Docker test commands cover PHP 8.5, 8.4 and 8.3 on both PowerShell and sh, and the middleware error-path wording matches the default rethrow behaviour.
0.2.5 (2026-10-29)#
- Added: agent-ready site (prompt blocks, Markdown twins, llms.txt), from Squire's kit.
- Added: HELMET_SESSION_SECURE=always keeps the session cookie Secure even when HTTPS is not detected.
- Changed: the proof page moves to a private site that needs a Wayne State sign-in.
- Changed: project names written as words except acronyms.
- Fixed: stale capability claims in the site and docs.
0.2.4 (2026-10-29)#
HELMET_PRESET(Laravel) now also comes from the privatewaynestate/playbookpackage when the environment has no value: the config reads it throughWaynestate\Helmet\Support\Setting, which checks the environment first, then PLAYBOOK, then gives up. A missing preset is still not an error, andwaynestate/playbookis suggested, never required. The README gains a "WSU values (PLAYBOOK)" section with the fetch commands.
0.2.3 (2026-10-29)#
- Fixed:
helmet_boot()removedX-Powered-Byonly after it had already answered the health check path, so a GET or HEAD to/up(the defaulthealth_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.
0.2.2 (2026-10-29)#
- 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, so a record with apasswordcontext key still comes out redacted and keeps its ID. 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), andhelmet_boot()accepts it aslog.request_id, which must be a string.helmet_boot()now also returns the ID asrequest_id.
0.2.1 (2026-10-29)#
-
Upgrade note: a raw-PHP app that still used the default
PHPSESSIDcookie gets the newWSUSESSIDname (__Host-WSUSESSIDon HTTPS), so signed-in users sign in once more after deploying. Laravel apps are unaffected (Laravel names its own cookie). -
Docs: added a "Running off Citadel" section to the README with a table for Citadel, OpenShift, Coolify and raw Docker behind a compose proxy: what sits in front, what to put in
HELMET_PROXIES, and where logs go. The README log example now leads withphp://stderr, and the hard-coded172.18.0.1now lives only in that table, with the other references pointing at thecitadelpreset. -
SessionHardening::apply()now also sets the session cookie name, lifetime, path, domain and cache limiter:cookie_lifetime=0(a browser-session cookie),cache_limiter=nocache,cookie_path=/, an emptycookie_domain(host-only), and the name__Host-WSUSESSIDon HTTPS orWSUSESSIDon HTTP. The rename happens only while the name is still the PHP defaultPHPSESSID; an app-chosen name is left alone, and SESAME reads whateversession_name()returns.
0.2.0 (2026-10-29)#
-
Fixed the
<head>match when injecting the csp-nonce meta tag into HTML responses: 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. -
Narrowed the trusted-header set in the Laravel adapter.
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. -
An unknown
HELMET_PRESETnow throws even whenHELMET_PROXIESis set. A typo in the preset name fails loudly at boot instead of going silently unused. -
Docs: noted that the CSP nonce is added to
style-srcandstyle-src-elem. A browser ignores'unsafe-inline'once a nonce is present, so overridingstyle-srcwith["'unsafe-inline'"]loses inline styles instead of restoring them. -
Added HSTS
preloadsupport. Sethsts_preload/HELMET_HSTS_PRELOADtotrueto append thepreloaddirective to theStrict-Transport-Securityheader. Preload requires amax-ageof at least one year (31536000) andincludeSubDomains; a shorter max-age or missingincludeSubDomainsthrows. Preload is hard to undo: once a browser has preloaded the domain it refuses plain-HTTP connections until the entry is removed. -
Added Trusted Types directives on the report-only CSP header. Set
csp_trusted_types.enabled/HELMET_CSP_TRUSTED_TYPES_ENABLEDtotrueto appendrequire-trusted-types-for 'script'andtrusted-types <policies>to theContent-Security-Policy-Report-Onlyheader only (never the enforcing header). Policy names default to["default"]; setcsp_trusted_types.policies/HELMET_CSP_TRUSTED_TYPES_POLICIESto override. Invalid policy names throw. -
Added opt-in
Cache-Control: no-store. Setno_store.enabled/HELMET_NO_STORE_ENABLEDtotrueto sendCache-Control: no-storeon matching responses.no_store.paths/HELMET_NO_STORE_PATHSlimits the header to specific path prefixes; empty means every response. An existingCache-Controlheader is never overridden. -
Added
helmet_up(): answers the/uphealth check with200,Content-Type: text/plain; charset=utf-8,Cache-Control: no-storeand the bodyok, then exits. For a raw PHP app with no front controller, point the probe at apublic/up.phpthat requires the autoloader and calls it.helmet_boot()now callshelmet_up()for the health path; behaviour is unchanged. -
Added an opt-in request-forgery (CSRF) guard.
helmet_boot()accepts acsrfoption, the PSR-15 core exposesRequestForgeryGuard, and the Laravel adapter registers aRequestForgeryGuardmiddleware whenhelmet.csrf.enabledis true. Safe methods (GET,HEAD,OPTIONS) always pass. For state-changing requests,Sec-Fetch-Siteis used when present; otherwise theOriginheader is checked against the request's own origin andcsrf.trusted_origins. A rejected request receives403withContent-Type: text/plain; charset=utf-8,Cache-Control: no-storeand the bodyForbidden. Config keys:csrf.enabled,csrf.allow_same_site,csrf.trusted_origins,csrf.except; env vars:HELMET_CSRF_ENABLED,HELMET_CSRF_ALLOW_SAME_SITE,HELMET_CSRF_TRUSTED_ORIGINS,HELMET_CSRF_EXCEPT. -
Added configurable
Cross-Origin-Opener-Policy(COOP),Cross-Origin-Resource-Policy(CORP) andPermissions-Policyheaders. COOP defaults tosame-origin(setcoop/HELMET_COOPtosame-origin-allow-popups,noopener-allow-popupsorunsafe-none, ornullto send no header). CORP is opt-in (setcorp/HELMET_CORPtosame-site,same-originorcross-origin). Permissions-Policy defaults to a deny list forcamera,geolocation,microphone,paymentandusb(setpermissions_policy/HELMET_PERMISSIONS_POLICYto a feature map; an empty map sends no header). Invalid values throw. -
BEST_PRACTICES tags brought up to date. The Stack lines are now picture perfect.
-
PHPStan and Pint now run from Pinstripe, the stack's coding standard, instead of a hand copy.
composer.jsonrequireswaynestate/pinstripe(dev-main, until 0.1.0 is tagged) as a dev dependency,phpstan.neonincludesvendor/waynestate/pinstripe/phpstan/raw.neon(keeping this repo's paths, PHP version range and three commented exceptions), andpint.jsonkeeps theperpreset Pinstripe ships. No behaviour change; the code is just cut from the same cloth now.
0.1.0 (2026-10-27)#
- First tagged release.