commit 17c031f7876a7d8b4ab156ac8be922004754efa5
Author: Robert Rothenberg <perl@rhizomnic.com>
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) <noreply@anthropic.com>
    
    Signed-off-by: Robert Rothenberg <perl@rhizomnic.com>

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<Catalyst::Engine::Apache2::MP20>.
 
 =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.
