commit 17c031f7876a7d8b4ab156ac8be922004754efa5 Author: Robert Rothenberg Date: Mon Aug 17 07:58:08 2026 +0100 Allow Cache-Control to be configured, and fix the Expires header The Cache-Control header was always set to public, with no means of overriding it. (CVE-2026-15743) An expires header of 0 was ignored. This change allows the Cache-Control header to be overridden, and the Expires header to be set to 0. Note: this patch was originally written by Claude Opus 5 but was modified by RRWO to keep the default Cache-Control as "public", to support configuring the default Cache-Control, and to simplify the logic for handling the configured Expires header. RRWO also added documentation. Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Robert Rothenberg diff --git a/lib/Catalyst/Plugin/Static/Simple.pm b/lib/Catalyst/Plugin/Static/Simple.pm index d8ee9c3..e657ca4 100755 --- a/lib/Catalyst/Plugin/Static/Simple.pm +++ b/lib/Catalyst/Plugin/Static/Simple.pm @@ -198,19 +198,32 @@ sub _serve_static { my $config = $c->config->{'Plugin::Static::Simple'}; my $full_path = shift || $c->_static_file; + my $requested_cache_control = shift; my $type = $c->_ext_to_type( $full_path ); my $stat = stat $full_path; $c->res->headers->content_type( $type ); $c->res->headers->content_length( $stat->size ); $c->res->headers->last_modified( $stat->mtime ); - # Tell Firefox & friends its OK to cache, even over SSL: - $c->res->headers->header('Cache-control' => 'public'); - # Optionally, set a fixed expiry time: - if ($config->{expires}) { - $c->res->headers->expires(time() + $config->{expires}); + + my $cache_control = $c->res->headers->header('Cache-Control') + || $requested_cache_control + || $config->{cache_control} + || 'public'; + + if ( defined $config->{expires} ) { + + if ( $config->{expires} <= 0 ) { + $cache_control = 'no-store'; + $c->res->headers->expires(0); + } + else { + $c->res->headers->expires(time() + $config->{expires}); + } } + $c->res->headers->header('Cache-Control' => $cache_control); + my $fh = IO::File->new( $full_path, 'r' ); if ( defined $fh ) { binmode $fh; @@ -225,7 +238,7 @@ sub _serve_static { } sub serve_static_file { - my ( $c, $full_path ) = @_; + my ( $c, $full_path, $cache_control ) = @_; my $config = $c->config->{'Plugin::Static::Simple'}; @@ -241,7 +254,7 @@ sub serve_static_file { return; } - $c->_serve_static( $full_path ); + $c->_serve_static( $full_path, $cache_control ); } # looks up the correct MIME type for the current file extension @@ -480,6 +493,17 @@ module, you may enter your own extension to MIME type mapping. }, ); +=head2 Controlling caching with the Cache-Control header + +By default, the Cache-Control header will be set to "public". +That can be changed: + + MyApp->config( + 'Plugin::Static::Simple' => { + cache_control => 'private', + }, + ); + =head2 Controlling caching with Expires header The files served by Static::Simple will have a Last-Modified header set, @@ -557,7 +581,7 @@ L. =head1 PUBLIC METHODS -=head2 serve_static_file $file_path +=head2 serve_static_file $file_path $cache_control Will serve the file located in $file_path statically. This is useful when you need to autogenerate them if they don't exist, or they are stored in a model. @@ -570,6 +594,11 @@ you need to autogenerate them if they don't exist, or they are stored in a mode $c->serve_static_file($file_path); } +The $cache_control option will default to what is configured (or to +"public" if not configured). It can be overridden for specific files: + + $c->serve_static_file( $file_path, "private" ); + =head1 INTERNAL EXTENDED METHODS Static::Simple extends the following steps in the Catalyst process.