NAME
Punk::Headers - security response headers
SYNOPSIS
package MyApp;
use Punk;
headers; # the safe default set
headers 'Content-Security-Policy' => "default-src 'self'",
'Strict-Transport-Security' => 'max-age=31536000',
'X-Frame-Options' => 'DENY';
headers 'X-Frame-Options' => undef; # keep the rest, drop this one
DESCRIPTION
Adds a frozen set of response headers to everything the application sends, from inside the C dispatcher.
The decoration happens on the way out of the dispatcher rather than in an after-dispatch hook, for the reason Punk::CORS gives: 404 and 405 never build a context, and a browser reads a Content-Security-Policy off an error page exactly as it would off a real one. CORS preflight replies are covered too.
Set-if-absent
A header the response already carries wins, case-insensitively. A handler (or a hook, or CORS) that sets its own X-Frame-Options for one route has said something more specific than the application-wide policy, and the policy does not repeat or contradict it.
The defaults
The bare keyword ships only headers that are safe on any application:
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Referrer-Policy: strict-origin-when-cross-origin
Content-Security-Policy and Strict-Transport-Security are deliberately not defaults. A CSP invented by a framework breaks every inline script and style it has never seen, and HSTS is a commitment to serve HTTPS for as long as its max-age promises - months, on any value worth sending. Both belong to the application, spelled out.
THE KEYWORD
headers; # the default set
headers %pairs; # defaults, overridden and extended
headers \%pairs; # the same, as a hashref
headers 0; # off
Keys are literal header names, values are literal header strings. A value of undef removes that header from the default set. A reference value croaks at keyword time. headers 0 turns the feature off.
It also reads a headers: block from config/punk.yml; headers: true is the bare form.
SCOPED POLICIES
my $admin = under '/admin' => $guard;
$admin->headers('X-Frame-Options' => 'DENY',
'Referrer-Policy' => undef);
An under scope takes the same pairs and applies them only to requests under its prefix - ahead of the application-wide policy, so a scope can tighten a header, add one, or (with undef) drop one for its subtree. Scopes nest, and the longest prefix wins a name. Because the policy rides the response path rather than the route, a 404 under the prefix carries it too.
Order of precedence for one header name, most specific first: whatever the response already carries (a handler's own $c->header), the longest-prefix scope that mentions it, then the application-wide policy.
SEE ALSO
AUTHOR
LNATION <email@lnation.org>
LICENSE AND COPYRIGHT
This software is Copyright (c) 2026 by LNATION <email@lnation.org>.
This is free software, licensed under:
The Artistic License 2.0 (GPL Compatible)