Skip to content
HELMET Source Back to HELMET
Menu

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": /up stays liveness-only, dependency checks go on a separate /ready path with a short timeout and a fixed 503 body, and Laravel apps keep them out of the DiagnosingHealth listener; BEST_PRACTICES H34 is Documented.

  • Added: a Grafana dashboard for the HELMET metrics (docs/grafana/helmet.json, uid helmet-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 a DS_PROMETHEUS datasource input, and tests/Support/GrafanaDashboardTest.php pins its shape. The metrics guide gains a "Graph it in Grafana" section with a sample scrape_configs stanza. 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) turns docs/ into site/docs/: the Overview page carries the README opening and a generated table of every HELMET_ environment variable the package reads. composer docs rebuilds it, composer ci checks it through composer docs:check, and the front page links it.

  • Docs: BEST_PRACTICES H53 is Documented (SESAME owns the logout headers, Clear-Site-Data without "cookies") and H41 is Partly 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, Laravel APP_DEBUG), points at Squire's deploy php.ini, and documents the helmet.warn_debug opt-out; BEST_PRACTICES H43 is now Documented.

  • 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 throws InvalidArgumentException, and nothing from the request or an exception reaches the body. An app calls it from its router; nothing wires it into helmet_boot() automatically.

0.3.0 (2026-11-03)#

  • 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 below make some requests resolve differently (an empty HELMET_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 at error level 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, with event, method, path, sec_fetch_site and origin_present in the context) and helmet.forwarded_for_rejected (warning, logged by ClientIp::resolve() when it fails closed on an unparseable X-Forwarded-For entry, with event, reason and peer in 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 with app.debug true in the production environment, with event in the context); opt out with helmet.warn_debug (env HELMET_WARN_DEBUG, default true), and a failing logger never breaks the boot.

  • Security: ClientIp fails closed on an unparseable X-Forwarded-For entry (for example 203.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-Proto now follows one rule everywhere, the last non-empty value in HttpsDetector and the Laravel normalizer, and the normalizer decides trust with the list Laravel itself uses (trustedproxy.proxies when set, else helmet.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 when HELMET_PRESET is 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 public changed to private and s-maxage dropped from its Cache-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 its default-src fallback (it is copied from default-src first, 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 Presets docblock now state the citadel preset's NAT assumption: 172.18.0.1 is 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() and assertViewHas() 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 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. The README gains a "WSU values (PLAYBOOK)" section with the fetch commands.

0.2.3 (2026-10-29)#

  • Fixed: 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.

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 a password context 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), and helmet_boot() accepts it as log.request_id, which must be a string. helmet_boot() now also returns the ID as request_id.

0.2.1 (2026-10-29)#

  • Upgrade note: 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).

  • 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 with php://stderr, and the hard-coded 172.18.0.1 now lives only in that table, with the other references pointing at the citadel preset.

  • 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 empty cookie_domain (host-only), and the name __Host-WSUSESSID on HTTPS or WSUSESSID on HTTP. 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.

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 a Content-Length header 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-Prefix and now X-Forwarded-Port are 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 with trustProxies(at: ..., headers: ...) or TrustProxies::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 produce https, 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_PRESET now throws even when HELMET_PROXIES is 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-src and style-src-elem. A browser ignores 'unsafe-inline' once a nonce is present, so overriding style-src with ["'unsafe-inline'"] loses inline styles instead of restoring them.

  • Added HSTS preload support. Set hsts_preload / HELMET_HSTS_PRELOAD to true to append the preload directive to the Strict-Transport-Security header. Preload requires a max-age of at least one year (31536000) and includeSubDomains; a shorter max-age or missing includeSubDomains throws. 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_ENABLED to true to append require-trusted-types-for 'script' and trusted-types <policies> to the Content-Security-Policy-Report-Only header only (never the enforcing header). Policy names default to ["default"]; set csp_trusted_types.policies / HELMET_CSP_TRUSTED_TYPES_POLICIES to override. Invalid policy names throw.

  • Added opt-in Cache-Control: no-store. Set no_store.enabled / HELMET_NO_STORE_ENABLED to true to send Cache-Control: no-store on matching responses. no_store.paths / HELMET_NO_STORE_PATHS limits the header to specific path prefixes; empty means every response. An existing Cache-Control header is never overridden.

  • Added helmet_up(): answers the /up health check with 200, Content-Type: text/plain; charset=utf-8, Cache-Control: no-store and the body ok, then exits. For a raw PHP app with no front controller, point the probe at a public/up.php that requires the autoloader and calls it. helmet_boot() now calls helmet_up() for the health path; behaviour is unchanged.

  • Added an opt-in request-forgery (CSRF) guard. helmet_boot() accepts a csrf option, the PSR-15 core exposes RequestForgeryGuard, and the Laravel adapter registers a RequestForgeryGuard middleware when helmet.csrf.enabled is true. Safe methods (GET, HEAD, OPTIONS) always pass. For state-changing requests, Sec-Fetch-Site is used when present; otherwise the Origin header is checked against the request's own origin and csrf.trusted_origins. A rejected request receives 403 with Content-Type: text/plain; charset=utf-8, Cache-Control: no-store and the body Forbidden. 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) and Permissions-Policy headers. COOP defaults to same-origin (set coop / HELMET_COOP to same-origin-allow-popups, noopener-allow-popups or unsafe-none, or null to send no header). CORP is opt-in (set corp / HELMET_CORP to same-site, same-origin or cross-origin). Permissions-Policy defaults to a deny list for camera, geolocation, microphone, payment and usb (set permissions_policy / HELMET_PERMISSIONS_POLICY to 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.json requires waynestate/pinstripe (dev-main, until 0.1.0 is tagged) as a dev dependency, phpstan.neon includes vendor/waynestate/pinstripe/phpstan/raw.neon (keeping this repo's paths, PHP version range and three commented exceptions), and pint.json keeps the per preset Pinstripe ships. No behaviour change; the code is just cut from the same cloth now.

0.1.0 (2026-10-27)#

  • First tagged release.