From bcbaf00ae5f334c6874d65c5c6dc5e9da14148d2 Mon Sep 17 00:00:00 2001
From: Andrew Ruthven <andrew@etc.gen.nz>
Date: Sun, 11 Aug 2024 19:04:01 +1200
Subject: Add $WebStrictBrowserCache option to disable browser cache

Cherry-picked from 5.0-trunk

RT systems that store sensitive data may want to disable all
browser cache and back button behavior. This option enables
that and moves these headers to a separate Mason template
for easy override.

See: https://owasp.org/www-project-web-security-testing-guide/v42/4-Web_Application_Security_Testing/04-Authentication_Testing/06-Testing_for_Browser_Cache_Weaknesses

Patch-Name: fix_CVE-2024-3262.diff
Applied-Upstream: 5.0.6, commit:ea07e767eaef5b202e8883051616d09806b8b48a
Origin: vendor
Forwarded: not-needed
Bug-Debian: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1068453
---
 etc/RT_Config.pm.in                     | 14 ++++
 share/html/Elements/Header              |  3 +-
 share/html/Elements/HttpResponseHeaders | 99 +++++++++++++++++++++++++
 share/html/m/_elements/header           |  3 +-
 4 files changed, 115 insertions(+), 4 deletions(-)
 create mode 100644 share/html/Elements/HttpResponseHeaders

diff --git a/etc/RT_Config.pm.in b/etc/RT_Config.pm.in
index 6b0284dd..c9eb309d 100644
--- a/etc/RT_Config.pm.in
+++ b/etc/RT_Config.pm.in
@@ -2750,6 +2750,20 @@ connections.
 
 Set($WebSecureCookies, 0);
 
+=item C<$WebStrictBrowserCache>
+
+As part of normal operation, browsers typically store some browsing
+history, enabling the Back button to work. Browsers also often
+cache pages in the browsing history to improve performance.
+
+Enable this option if you are using RT with highly sensitive
+information and want to signal the browser to not store any history
+or cache any data. The default is disabled.
+
+=cut
+
+Set($WebStrictBrowserCache, 0);
+
 =item C<$WebHttpOnlyCookies>
 
 Default RT's session cookie to not being directly accessible to
diff --git a/share/html/Elements/Header b/share/html/Elements/Header
index ad17a431..9281342d 100644
--- a/share/html/Elements/Header
+++ b/share/html/Elements/Header
@@ -118,8 +118,7 @@ $lang = $session{'CurrentUser'}->LanguageHandle->language_tag
      && $session{'CurrentUser'}->LanguageHandle
      && $session{'CurrentUser'}->LanguageHandle->language_tag;
 
-$r->headers_out->{'Pragma'} = 'no-cache';
-$r->headers_out->{'Cache-control'} = 'no-cache';
+$m->comp('/Elements/HttpResponseHeaders');
 
 my $id = $m->request_comp->path;
 $id =~ s|^/||g;
diff --git a/share/html/Elements/HttpResponseHeaders b/share/html/Elements/HttpResponseHeaders
new file mode 100644
index 00000000..3b452f01
--- /dev/null
+++ b/share/html/Elements/HttpResponseHeaders
@@ -0,0 +1,99 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2023 Best Practical Solutions, LLC
+%#                                          <sales@bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<%INIT>
+
+# Since data in the DB can change at any time, the default headers
+# for dynamic content (content generated from most Mason templates) is:
+#
+# Cache-control: no-cache
+# Pragma: no-cache
+# Expires: [a short time in the past to account for any time drift]
+
+# Pragma is deprecated and usually ignored if Cache-control is sent.
+# Should only be used by HTTP/1.0 clients.
+$r->headers_out->{'Pragma'} = 'no-cache';
+
+my $cache_control = 'no-cache';
+
+my $expires = RT::Date->new(RT->SystemUser);
+$expires->SetToNow;
+
+if ( $MaxAgeSeconds && !RT->Config->Get('WebStrictBrowserCache') ) {
+    $expires->AddSeconds($MaxAgeSeconds);
+
+    # Expires is an older header and has been superseded by Cache-control
+    # and max-age, so set that also. New browsers will use max-age and
+    # ignore Expires.
+
+    # We're allowing a short cache, so replace no-cache with max-age.
+
+    $cache_control = "max-age=$MaxAgeSeconds, private"
+}
+else {
+    # Setting Expires to 0, a common approach to "immediately expired"
+    # doesn't send an Expires header from Mason, so set a little in the past.
+
+    $expires->AddSeconds(-30);
+    $cache_control .= ', max-age=0';
+}
+
+$r->headers_out->{'Expires'} = $expires->RFC2616;
+
+if ( RT->Config->Get('WebStrictBrowserCache') ) {
+
+    # Instruct the browser not to cache or store anything
+    $cache_control .= ', no-store, must-revalidate, s-maxage=0';
+}
+
+$r->headers_out->{'Cache-control'} = $cache_control;
+
+$m->callback( %ARGS, CallbackName => 'End' );
+</%INIT>
+<%ARGS>
+$MaxAgeSeconds => undef  # Time in seconds to allow for cache
+</%ARGS>
diff --git a/share/html/m/_elements/header b/share/html/m/_elements/header
index 0bb72e28..2a192dc1 100644
--- a/share/html/m/_elements/header
+++ b/share/html/m/_elements/header
@@ -50,8 +50,7 @@ $title => loc('RT for [_1]', RT->Config->Get('rtname'))
 $show_home_button => 1
 </%args>
 <%init>
-$r->headers_out->{'Pragma'} = 'no-cache';
-$r->headers_out->{'Cache-control'} = 'no-cache';
+$m->comp('/Elements/HttpResponseHeaders');
 </%init>
 <html>
 <head>
