Version in base suite: 5.0.7+dfsg-4+deb13u2 Base version: request-tracker5_5.0.7+dfsg-4+deb13u2 Target version: request-tracker5_5.0.7+dfsg-4+deb13u3 Base file: /srv/ftp-master.debian.org/ftp/pool/main/r/request-tracker5/request-tracker5_5.0.7+dfsg-4+deb13u2.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/r/request-tracker5/request-tracker5_5.0.7+dfsg-4+deb13u3.dsc .git-dpm | 4 changelog | 36 patches/series | 3 patches/upstream_5.0.7_cve:_patchset_2025-04-08-RT_Config.diff | 65 + patches/upstream_5.0.7_cve:_patchset_2026-05-05-RT_Config.diff | 47 + patches/upstream_5.0.7_cve:_patchset_2026-05-05.diff | 450 ++++++++++ 6 files changed, 603 insertions(+), 2 deletions(-) dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpclen2ded/request-tracker5_5.0.7+dfsg-4+deb13u2.dsc: no acceptable signature found dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpclen2ded/request-tracker5_5.0.7+dfsg-4+deb13u3.dsc: no acceptable signature found diff -Nru request-tracker5-5.0.7+dfsg/debian/.git-dpm request-tracker5-5.0.7+dfsg/debian/.git-dpm --- request-tracker5-5.0.7+dfsg/debian/.git-dpm 2026-03-02 09:17:44.000000000 +0000 +++ request-tracker5-5.0.7+dfsg/debian/.git-dpm 2026-06-01 01:10:39.000000000 +0000 @@ -1,6 +1,6 @@ # see git-dpm(1) from git-dpm package -72d03f2d58a19d24c22aaf30fe8a2290fa26fc29 -72d03f2d58a19d24c22aaf30fe8a2290fa26fc29 +7303c128c840cc9046320ec0591aaaeca953155c +7303c128c840cc9046320ec0591aaaeca953155c 7ffdc76a3d7dde5bc3954f1c874ec200bdc3310a 7ffdc76a3d7dde5bc3954f1c874ec200bdc3310a request-tracker5_5.0.7+dfsg.orig.tar.gz diff -Nru request-tracker5-5.0.7+dfsg/debian/changelog request-tracker5-5.0.7+dfsg/debian/changelog --- request-tracker5-5.0.7+dfsg/debian/changelog 2026-03-02 09:17:44.000000000 +0000 +++ request-tracker5-5.0.7+dfsg/debian/changelog 2026-06-01 01:10:39.000000000 +0000 @@ -1,3 +1,39 @@ +request-tracker5 (5.0.7+dfsg-4+deb13u3) trixie-security; urgency=high + + * Include missing default configuration items for security vulnerability + fixes included in 5.0.7+dfsg-3. Namely: RestrictLinkDomains and Cipher + in %SMIME. + * Apply upstream patch which fixes several security vulnerabilities: + - [CVE-2026-6841] Reflected cross-site scripting via the search "Page" URL + parameter. + - [CVE-2026-41073] Spreadsheet (CSV/formula) injection via ticket values + that are exported to a spreadsheet from search results. User-controlled + data is not sanitized before being written to the output file, which can + cause spreadsheet applications such as Microsoft Excel to interpret + crafted values as formulas or macros when the file is opened. + - [CVE-2026-41075] SQL injection via the entry_aggregator parameter in JSON + search. An authenticated user can craft input that is incorporated into + database queries without proper validation, potentially allowing them to + read or modify data in the RT database. + - [CVE-2026-41076] LDAP authentication bypass when RT is configured to + authenticate users against an LDAP or Active Directory server. Under + certain LDAP server configurations, an attacker may be able to + authenticate as any LDAP-backed RT user without supplying valid + credentials. + - [CVE-2026-44229] Cross-site scripting via uploaded content that is served + inline rather than as an attachment. + - [CVE-2026-44230] Reflected cross-site scripting on search-results chart + pages. + - [CVE-2026-44231] Privilege escalation and information disclosure via the + REST 2.0 user collection endpoint. A Privileged RT user can obtain + authentication credentials belonging to other users, including + administrators, and use those credentials to read data via RT's RSS and + iCal feed endpoints. The same request that exposes the credentials also + rotates them, which invalidates previously-distributed feed URLs across + the instance. + + -- Andrew Ruthven Mon, 01 Jun 2026 13:10:39 +1200 + request-tracker5 (5.0.7+dfsg-4+deb13u2) trixie; urgency=medium * Set a version for ckeditor when we build it to allow Firefox v148 to diff -Nru request-tracker5-5.0.7+dfsg/debian/patches/series request-tracker5-5.0.7+dfsg/debian/patches/series --- request-tracker5-5.0.7+dfsg/debian/patches/series 2026-03-02 09:17:44.000000000 +0000 +++ request-tracker5-5.0.7+dfsg/debian/patches/series 2026-06-01 01:10:39.000000000 +0000 @@ -31,3 +31,6 @@ debianize_UPGRADING-5.0.diff debianize_UPGRADING-4.4.diff upstream_5.0.7_cve:_patchset_2025-10-07.diff +upstream_5.0.7_cve:_patchset_2026-05-05.diff +upstream_5.0.7_cve:_patchset_2026-05-05-RT_Config.diff +upstream_5.0.7_cve:_patchset_2025-04-08-RT_Config.diff diff -Nru request-tracker5-5.0.7+dfsg/debian/patches/upstream_5.0.7_cve:_patchset_2025-04-08-RT_Config.diff request-tracker5-5.0.7+dfsg/debian/patches/upstream_5.0.7_cve:_patchset_2025-04-08-RT_Config.diff --- request-tracker5-5.0.7+dfsg/debian/patches/upstream_5.0.7_cve:_patchset_2025-04-08-RT_Config.diff 1970-01-01 00:00:00.000000000 +0000 +++ request-tracker5-5.0.7+dfsg/debian/patches/upstream_5.0.7_cve:_patchset_2025-04-08-RT_Config.diff 2026-06-01 01:10:39.000000000 +0000 @@ -0,0 +1,65 @@ +From 7303c128c840cc9046320ec0591aaaeca953155c Mon Sep 17 00:00:00 2001 +From: Andrew Ruthven +Date: Wed, 27 May 2026 20:47:49 +1200 +Subject: Config settings for RestrictLinkDomains, and Cipher in SMIME. + +Add the new configuration options to RT_Config.pm.in as we regenerate the +RT_Config.pm file. + +Patch-Name: upstream_5.0.7_cve:_patchset_2025-04-08-RT_Config.diff +Author: Andrew Ruthven +Forwarded: not-needed +Applied: 5.0.8 +--- + etc/RT_Config.pm.in | 24 ++++++++++++++++++++++++ + 1 file changed, 24 insertions(+) + +diff --git a/etc/RT_Config.pm.in b/etc/RT_Config.pm.in +index 24e0331e..c49fc03f 100644 +--- a/etc/RT_Config.pm.in ++++ b/etc/RT_Config.pm.in +@@ -1284,6 +1284,26 @@ higher numbers denoting greater effort. + + Set($BcryptCost, 12); + ++=item C<@RestrictLinkDomains> ++ ++This sets a list of external domains that RT is allowed to link to. If this ++setting is empty, no external domains are allowed. ++ ++Currently, this restriction only applies to links in Format parameter for ++search results. All external links whose domains are not in the list will ++be removed. ++ ++E.g. ++ ++ Set(@RestrictLinkDomains, ("example.com", "*.trusted.com")); ++ ++ example.com # Allow links to "example.com" ++ *.trusted.com # Allow links to any one-level subdomain of "trusted.com" ++ ++=cut ++ ++Set(@RestrictLinkDomains, ()); ++ + =back + + =head2 Internationalization +@@ -4299,6 +4319,9 @@ Set C to the timeout in seconds for + downloading a CRL or an issuer certificate (the latter is used when + checking against OCSP). The default timeout is 30 seconds. + ++Set C to the encryption algorithm to use. By default, it's ++C. ++ + See L for details. + + =back +@@ -4317,6 +4340,7 @@ Set( %SMIME, + CheckCRL => 0, + CheckOCSP => 0, + CheckRevocationDownloadTimeout => 30, ++ Cipher => 'aes-128-cbc', + ); + + =head3 GnuPG configuration diff -Nru request-tracker5-5.0.7+dfsg/debian/patches/upstream_5.0.7_cve:_patchset_2026-05-05-RT_Config.diff request-tracker5-5.0.7+dfsg/debian/patches/upstream_5.0.7_cve:_patchset_2026-05-05-RT_Config.diff --- request-tracker5-5.0.7+dfsg/debian/patches/upstream_5.0.7_cve:_patchset_2026-05-05-RT_Config.diff 1970-01-01 00:00:00.000000000 +0000 +++ request-tracker5-5.0.7+dfsg/debian/patches/upstream_5.0.7_cve:_patchset_2026-05-05-RT_Config.diff 2026-06-01 01:10:39.000000000 +0000 @@ -0,0 +1,47 @@ +From d5e631243d71f068cdce29a727fd84660fbfce42 Mon Sep 17 00:00:00 2001 +From: Andrew Ruthven +Date: Wed, 27 May 2026 20:37:45 +1200 +Subject: Config settings for EnableRSS and EnableICal + +Add the new configuration options to RT_Config.pm.in as we regenerate the +RT_Config.pm file. + +Patch-Name: upstream_5.0.7_cve:_patchset_2026-05-05-RT_Config.diff +Author: Andrew Ruthven +Forwarded: not-needed +Applied: 5.0.10 +--- + etc/RT_Config.pm.in | 20 ++++++++++++++++++++ + 1 file changed, 20 insertions(+) + +diff --git a/etc/RT_Config.pm.in b/etc/RT_Config.pm.in +index 01ecf21f..24e0331e 100644 +--- a/etc/RT_Config.pm.in ++++ b/etc/RT_Config.pm.in +@@ -181,6 +181,26 @@ Example: + + Set( @StaticRoots, () ); + ++=item C<$EnableRSS> ++ ++RT's per-user RSS feed endpoint at C is enabled by ++default. Set this option to 0 to disable it: requests to that path ++return 404, and search-results pages stop rendering RSS feed links. ++ ++=cut ++ ++Set($EnableRSS, 1); ++ ++=item C<$EnableICal> ++ ++RT's per-user iCal feed endpoint at C is enabled by ++default. Set this option to 0 to disable it: requests to that path ++return 404, and search-results pages stop rendering iCal feed links. ++ ++=cut ++ ++Set($EnableICal, 1); ++ + =back + + diff -Nru request-tracker5-5.0.7+dfsg/debian/patches/upstream_5.0.7_cve:_patchset_2026-05-05.diff request-tracker5-5.0.7+dfsg/debian/patches/upstream_5.0.7_cve:_patchset_2026-05-05.diff --- request-tracker5-5.0.7+dfsg/debian/patches/upstream_5.0.7_cve:_patchset_2026-05-05.diff 1970-01-01 00:00:00.000000000 +0000 +++ request-tracker5-5.0.7+dfsg/debian/patches/upstream_5.0.7_cve:_patchset_2026-05-05.diff 2026-06-01 01:10:39.000000000 +0000 @@ -0,0 +1,450 @@ +From ff22da9bbe33543f8dfd46a2c3de153a81abfc24 Mon Sep 17 00:00:00 2001 +From: Andrew Ruthven +Date: Fri, 22 May 2026 22:14:01 +1200 +Subject: Fix seven security issues in RT. + +Resolve vulnerabilities: + + - [CVE-2026-6841] Reflected cross-site scripting via the search "Page" URL + parameter. + - [CVE-2026-41073] Spreadsheet (CSV/formula) injection via ticket values that + are exported to a spreadsheet from search results. User-controlled data is + not sanitized before being written to the output file, which can cause + spreadsheet applications such as Microsoft Excel to interpret crafted + values as formulas or macros when the file is opened. + - [CVE-2026-41075] SQL injection via the entry_aggregator parameter + in JSON search. An authenticated user can craft input that is + incorporated into database queries without proper validation, + potentially allowing them to read or modify data in the RT database. + - [CVE-2026-41076] LDAP authentication bypass when RT is configured to + authenticate users against an LDAP or Active Directory server. Under + certain LDAP server configurations, an attacker may be able to authenticate + as any LDAP-backed RT user without supplying valid credentials. + - [CVE-2026-44229] Cross-site scripting via uploaded content that is served + inline rather than as an attachment. + - [CVE-2026-44230] Fix a reflected cross-site scripting on search-results + chart pages. + - [CVE-2026-44231] Privilege escalation and information disclosure via the + REST 2.0 user collection endpoint. A Privileged RT user can obtain + authentication credentials belonging to other users, including + administrators, and use those credentials to read data via RT's RSS and + iCal feed endpoints. The same request that exposes the credentials also + rotates them, which invalidates previously-distributed feed URLs across the + instance. + +Patch-Name: upstream_5.0.7_cve:_patchset_2026-05-05.diff +Author: Best Practical +Forwarded: not-needed +Applied: 5.0.10 +--- + etc/RT_Config.pm | 20 +++++ + lib/RT/Authen/ExternalAuth.pm | 5 ++ + lib/RT/Config.pm | 6 ++ + lib/RT/Interface/Web/MenuBuilder.pm | 45 ++++++----- + .../REST2/Resource/ObjectCustomFieldValue.pm | 8 +- + lib/RT/SearchBuilder.pm | 11 +++ + lib/RT/User.pm | 74 +++++++++++-------- + share/html/Download/CustomFieldValue/dhandler | 5 +- + share/html/Elements/CollectionList | 2 +- + share/html/Elements/CollectionListPaging | 3 +- + share/html/Elements/TSVExport | 3 +- + share/html/NoAuth/iCal/dhandler | 2 + + share/html/NoAuth/rss/dhandler | 2 + + share/html/Search/JSChart | 2 +- + share/html/Search/Results.html | 2 +- + 15 files changed, 131 insertions(+), 59 deletions(-) + +diff --git a/etc/RT_Config.pm b/etc/RT_Config.pm +index e358cec3..c5fb38f7 100644 +--- a/etc/RT_Config.pm ++++ b/etc/RT_Config.pm +@@ -181,6 +181,26 @@ Example: + + Set( @StaticRoots, () ); + ++=item C<$EnableRSS> ++ ++RT's per-user RSS feed endpoint at C is enabled by ++default. Set this option to 0 to disable it: requests to that path ++return 404, and search-results pages stop rendering RSS feed links. ++ ++=cut ++ ++Set($EnableRSS, 1); ++ ++=item C<$EnableICal> ++ ++RT's per-user iCal feed endpoint at C is enabled by ++default. Set this option to 0 to disable it: requests to that path ++return 404, and search-results pages stop rendering iCal feed links. ++ ++=cut ++ ++Set($EnableICal, 1); ++ + =back + + +diff --git a/lib/RT/Authen/ExternalAuth.pm b/lib/RT/Authen/ExternalAuth.pm +index 9a6aedbe..cfbc1498 100644 +--- a/lib/RT/Authen/ExternalAuth.pm ++++ b/lib/RT/Authen/ExternalAuth.pm +@@ -631,6 +631,11 @@ sub GetAuth { + + my ($service,$username,$password) = @_; + ++ unless ( defined $password && length $password ) { ++ $RT::Logger->debug("External auth ($service) rejecting empty password for $username"); ++ return 0; ++ } ++ + my $success = 0; + + # Get the full configuration for that service as a hashref +diff --git a/lib/RT/Config.pm b/lib/RT/Config.pm +index d4845ad0..11c22a7e 100644 +--- a/lib/RT/Config.pm ++++ b/lib/RT/Config.pm +@@ -2291,6 +2291,12 @@ our %META; + SelfServiceRegex => { + Immutable => 1, + }, ++ EnableRSS => { ++ Immutable => 1, ++ }, ++ EnableICal => { ++ Immutable => 1, ++ }, + ); + my %OPTIONS = (); + my @LOADED_CONFIGS = (); +diff --git a/lib/RT/Interface/Web/MenuBuilder.pm b/lib/RT/Interface/Web/MenuBuilder.pm +index 0dc2f77b..2e6917d0 100644 +--- a/lib/RT/Interface/Web/MenuBuilder.pm ++++ b/lib/RT/Interface/Web/MenuBuilder.pm +@@ -772,26 +772,31 @@ sub BuildMainNav { + if ( $class eq 'RT::Tickets' ) { + my %rss_data + = map { $_ => $query_args->{$_} || $fallback_query_args{$_} || '' } qw(Query Order OrderBy); +- my $RSSQueryString = "?" +- . QueryString( +- $short_query{sc} +- ? ( sc => $short_query{sc} ) +- : ( Query => $rss_data{Query}, +- Order => $rss_data{Order}, +- OrderBy => $rss_data{OrderBy} +- ) +- ); +- my $RSSPath = join '/', map $HTML::Mason::Commands::m->interp->apply_escapes( $_, 'u' ), +- $current_user->UserObj->Name, +- $current_user->UserObj->GenerateAuthString( $short_query{sc} +- || ( $rss_data{Query} . $rss_data{Order} . $rss_data{OrderBy} ) ); +- +- $more->child( rss => title => loc('RSS'), path => "/NoAuth/rss/$RSSPath/$RSSQueryString" ); +- my $ical_path = join '/', map $HTML::Mason::Commands::m->interp->apply_escapes( $_, 'u' ), +- $current_user->UserObj->Name, +- $current_user->UserObj->GenerateAuthString( $rss_data{Query} ), +- $short_query{sc} ? "sc-$short_query{sc}" : $rss_data{Query}; +- $more->child( ical => title => loc('iCal'), path => '/NoAuth/iCal/' . $ical_path ); ++ if ( RT->Config->Get('EnableRSS') ) { ++ my $RSSQueryString = "?" ++ . QueryString( ++ $short_query{sc} ++ ? ( sc => $short_query{sc} ) ++ : ( Query => $rss_data{Query}, ++ Order => $rss_data{Order}, ++ OrderBy => $rss_data{OrderBy} ++ ) ++ ); ++ my $RSSPath = join '/', map $HTML::Mason::Commands::m->interp->apply_escapes( $_, 'u' ), ++ $current_user->UserObj->Name, ++ $current_user->UserObj->GenerateAuthString( $short_query{sc} ++ || ( $rss_data{Query} . $rss_data{Order} . $rss_data{OrderBy} ) ); ++ ++ $more->child( rss => title => loc('RSS'), path => "/NoAuth/rss/$RSSPath/$RSSQueryString" ); ++ } ++ ++ if ( RT->Config->Get('EnableICal') ) { ++ my $ical_path = join '/', map $HTML::Mason::Commands::m->interp->apply_escapes( $_, 'u' ), ++ $current_user->UserObj->Name, ++ $current_user->UserObj->GenerateAuthString( $rss_data{Query} ), ++ $short_query{sc} ? "sc-$short_query{sc}" : $rss_data{Query}; ++ $more->child( ical => title => loc('iCal'), path => '/NoAuth/iCal/' . $ical_path ); ++ } + + #XXX TODO better abstraction of SuperUser right check + if ( $current_user->HasRight( Right => 'SuperUser', Object => RT->System ) ) { +diff --git a/lib/RT/REST2/Resource/ObjectCustomFieldValue.pm b/lib/RT/REST2/Resource/ObjectCustomFieldValue.pm +index 69bdaae9..51b7efc6 100644 +--- a/lib/RT/REST2/Resource/ObjectCustomFieldValue.pm ++++ b/lib/RT/REST2/Resource/ObjectCustomFieldValue.pm +@@ -80,13 +80,15 @@ sub to_binary { + } + + my $content_type = $self->record->ContentType || 'text/plain; charset=utf-8'; +- if (RT->Config->Get('AlwaysDownloadAttachments')) { +- $self->response->headers_out->{'Content-Disposition'} = "attachment"; ++ if (RT->Config->Get('AlwaysDownloadAttachments') || $content_type =~ m{^(image/svg\+xml|application/pdf)}i) { ++ $self->response->header('Content-Disposition' => 'attachment'); + } + elsif (!RT->Config->Get('TrustHTMLAttachments')) { +- $content_type = 'text/plain; charset=utf-8' if ($content_type =~ /^text\/html/i); ++ $content_type = 'text/plain; charset=utf-8' ++ if $content_type =~ m{^(text/html|application/xhtml\+xml|text/xml|application/xml)}i; + } + ++ $self->response->header('X-Content-Type-Options' => 'nosniff') if RT->Config->Get('StrictContentTypes'); + $self->response->content_type($content_type); + + my $content = $self->record->LargeContent; +diff --git a/lib/RT/SearchBuilder.pm b/lib/RT/SearchBuilder.pm +index c87eddd2..aee6f836 100644 +--- a/lib/RT/SearchBuilder.pm ++++ b/lib/RT/SearchBuilder.pm +@@ -1010,6 +1010,17 @@ sub Limit { + ); + } + ++ if ( $ARGS{ENTRYAGGREGATOR} && $ARGS{ENTRYAGGREGATOR} !~ /^(AND|OR|none)$/i ) { ++ $RT::Logger->crit("Possible SQL injection attack via ENTRYAGGREGATOR: $ARGS{ENTRYAGGREGATOR}"); ++ %ARGS = ( ++ %ARGS, ++ FIELD => 'id', ++ OPERATOR => '<', ++ VALUE => '0', ++ ENTRYAGGREGATOR => 'AND', ++ ); ++ } ++ + my $table; + ($table) = $ARGS{'ALIAS'} && $ARGS{'ALIAS'} ne 'main' + ? ($ARGS{'ALIAS'} =~ /^(.*)_\d+$/) +diff --git a/lib/RT/User.pm b/lib/RT/User.pm +index 03357a78..f4fbd273 100644 +--- a/lib/RT/User.pm ++++ b/lib/RT/User.pm +@@ -93,6 +93,7 @@ sub _OverlayAccessible { + + Name => { public => 1, admin => 1 }, # loc_left_pair + Password => { read => 0 }, ++ AuthToken => { read => 0 }, + EmailAddress => { public => 1 }, # loc_left_pair + Organization => { public => 1, admin => 1 }, # loc_left_pair + RealName => { public => 1 }, # loc_left_pair +@@ -106,6 +107,15 @@ sub _OverlayAccessible { + } + } + ++# AuthToken is settable internally (GenerateAuthToken / GenerateAuthString / ++# SetCanonicalUserInfo go through SetAuthToken), but no client-facing entry ++# point should let a caller write it directly. Drop it from the allow-list ++# used by REST 2 update_record, the web UI autocreate path, and rt-config. ++sub WritableAttributes { ++ my $self = shift; ++ return grep { $_ ne 'AuthToken' } $self->SUPER::WritableAttributes(@_); ++} ++ + + + =head2 Create { PARAMHASH } +@@ -1355,32 +1365,6 @@ sub CurrentUserRequireToSetPassword { + return %res; + } + +-=head3 AuthToken +- +-Returns an authentication string associated with the user. This +-string can be used to generate passwordless URLs to integrate +-RT with services and programs like calendar managers, RSS +-readers and other. +- +-=cut +- +-sub AuthToken { +- my $self = shift; +- my $secret = $self->_Value( AuthToken => @_ ); +- return $secret if $secret; +- +- $secret = substr(Digest::MD5::md5_hex(time . {} . rand()),0,16); +- +- my $tmp = RT::User->new( RT->SystemUser ); +- $tmp->Load( $self->id ); +- my ($status, $msg) = $tmp->SetAuthToken( $secret ); +- unless ( $status ) { +- $RT::Logger->error( "Couldn't set auth token: $msg" ); +- return undef; +- } +- return $secret; +-} +- + =head3 GenerateAuthToken + + Generate a random authentication string for the user. +@@ -1404,7 +1388,22 @@ sub GenerateAuthString { + my $self = shift; + my $protect = shift; + +- my $str = Encode::encode( "UTF-8", $self->AuthToken . $protect ); ++ my $token = $self->_Value('AuthToken'); ++ unless ($token) { ++ # Mint on demand via SystemUser: signing RSS/iCal/CSRF URLs is a ++ # system action, but ModifySelf isn't granted to Privileged users ++ # by default, so SetAuthToken under the current user would fail. ++ $token = substr(Digest::MD5::md5_hex(time . {} . rand()), 0, 16); ++ my $writer = RT::User->new( RT->SystemUser ); ++ $writer->Load( $self->id ); ++ my ($ok, $msg) = $writer->SetAuthToken($token); ++ unless ($ok) { ++ $RT::Logger->error("Couldn't set auth token: $msg"); ++ return; ++ } ++ } ++ ++ my $str = Encode::encode( "UTF-8", $token . $protect ); + + return substr(Digest::MD5::md5_hex($str),0,16); + } +@@ -1421,10 +1420,27 @@ sub ValidateAuthString { + my $auth_string_to_validate = shift; + my $protected = shift; + +- my $str = Encode::encode( "UTF-8", $self->AuthToken . $protected ); ++ # Always compute the hash and run the constant-time compare so the ++ # "no stored token" path looks like a regular failed validation rather ++ # than returning a precomputable md5_hex($protected) or short-circuiting ++ # on a measurable timing differential. ++ my $token = $self->_Value('AuthToken'); ++ my $has_token = defined $token && length $token; ++ $token //= ''; ++ ++ my $str = Encode::encode( "UTF-8", $token . $protected ); + my $valid_auth_string = substr(Digest::MD5::md5_hex($str),0,16); + +- return RT::Util::constant_time_eq( $auth_string_to_validate, $valid_auth_string ); ++ # constant_time_eq dies on undef or length mismatch. Catch so the ++ # rss/iCal dhandlers return 404 instead of 500 when callers send a ++ # malformed auth string, and log so the failure isn't silent. ++ my $eq = do { ++ local $@; ++ my $r = eval { RT::Util::constant_time_eq( $auth_string_to_validate, $valid_auth_string ) }; ++ RT->Logger->warning("ValidateAuthString: $@") if $@; ++ $r // 0; ++ }; ++ return $has_token && $eq; + } + + =head2 SetDisabled +diff --git a/share/html/Download/CustomFieldValue/dhandler b/share/html/Download/CustomFieldValue/dhandler +index 9dede844..743229c2 100644 +--- a/share/html/Download/CustomFieldValue/dhandler ++++ b/share/html/Download/CustomFieldValue/dhandler +@@ -65,11 +65,12 @@ Abort( loc('Permission Denied'), Code => HTTP::Status::HTTP_FORBIDDEN ) unless $ + + my $content_type = $OCFV->ContentType || 'text/plain; charset=utf-8'; + +-if (RT->Config->Get('AlwaysDownloadAttachments')) { ++if (RT->Config->Get('AlwaysDownloadAttachments') || $content_type =~ m{^(image/svg\+xml|application/pdf)}i) { + $r->headers_out->{'Content-Disposition'} = "attachment"; + } + elsif (!RT->Config->Get('TrustHTMLAttachments')) { +- $content_type = 'text/plain; charset=utf-8' if ($content_type =~ /^text\/html/i); ++ $content_type = 'text/plain; charset=utf-8' ++ if $content_type =~ m{^(text/html|application/xhtml\+xml|text/xml|application/xml)}i; + } + + $r->headers_out->{'X-Content-Type-Options'} = 'nosniff' if RT->Config->Get('StrictContentTypes'); +diff --git a/share/html/Elements/CollectionList b/share/html/Elements/CollectionList +index 322b0e9a..6a392e15 100644 +--- a/share/html/Elements/CollectionList ++++ b/share/html/Elements/CollectionList +@@ -73,7 +73,7 @@ if ( @OrderBy && ($AllowSorting || $PreferOrderBy || !$Collection->{'order_by'}) + } + + $Collection->RowsPerPage( $Rows ) if $Rows; +-$Page = 1 unless $Page && $Page > 0; # workaround problems with $Page = '' or undef ++$Page = 1 unless $Page && $Page =~ /\A\d+\z/ && $Page > 1; # workaround problems with $Page = '' or undef + $Collection->GotoPage( $Page - 1 ); # SB uses page 0 as the first page + + # $Collection might not have CombineSearchAndCount method, like dashboards and saved searches. +diff --git a/share/html/Elements/CollectionListPaging b/share/html/Elements/CollectionListPaging +index 550eb53f..ba62fc9a 100644 +--- a/share/html/Elements/CollectionListPaging ++++ b/share/html/Elements/CollectionListPaging +@@ -96,9 +96,10 @@ else{ + $PageParam => $page, ), + 'h', + ); ++ my $page_display = $m->interp->apply_escapes($page, 'h'); + + $m->out(qq{
  • }); +- $m->out(qq{$page}); ++ $m->out(qq{$page_display}); + $m->out(qq{
  • }); + } + } +diff --git a/share/html/Elements/TSVExport b/share/html/Elements/TSVExport +index ecb19640..5cc2d736 100644 +--- a/share/html/Elements/TSVExport ++++ b/share/html/Elements/TSVExport +@@ -130,7 +130,8 @@ else { + } + + for (@columns) { +- $m->out(join("\t", map { $_->{header} } @$_)."\n"); ++ # To prevent injection, add a leading space to make sure excel-ish applications treat it like a literal ++ $m->out(join("\t", map { s/^(?=-|\+|=|\@|")/ /; $_ } map { $_->{header} } @$_)."\n"); + } + + my $i = 0; +diff --git a/share/html/NoAuth/iCal/dhandler b/share/html/NoAuth/iCal/dhandler +index af20cacf..04b42922 100644 +--- a/share/html/NoAuth/iCal/dhandler ++++ b/share/html/NoAuth/iCal/dhandler +@@ -56,6 +56,8 @@ my $notfound = sub { + $m->clear_and_abort; + }; + ++$notfound->() unless RT->Config->Get('EnableICal'); ++ + $notfound->() unless $path =~ m!^([^/]+)/([^/]+)/(.*)(\.(ical|ics))?!; + + my ($name, $auth, $search) = ($1, $2, $3); +diff --git a/share/html/NoAuth/rss/dhandler b/share/html/NoAuth/rss/dhandler +index 75e39200..a6058969 100644 +--- a/share/html/NoAuth/rss/dhandler ++++ b/share/html/NoAuth/rss/dhandler +@@ -55,6 +55,8 @@ my $notfound = sub { + $m->clear_and_abort; + }; + ++$notfound->("RSS feeds disabled by configuration") unless RT->Config->Get('EnableRSS'); ++ + $notfound->("Invalid path: $path") unless $path =~ m!^([^/]+)/([^/]+)/?!; + + my ( $name, $auth ) = ( $1, $2 ); +diff --git a/share/html/Search/JSChart b/share/html/Search/JSChart +index 6820d9aa..1358c440 100644 +--- a/share/html/Search/JSChart ++++ b/share/html/Search/JSChart +@@ -164,7 +164,7 @@ var searchChart = new Chart(ctx, { + var group_by = <% JSON( \@GroupBy ) |n %>; + var data_queries = <% JSON( \@data_queries ) |n %>; + +-jQuery('[id="<% $id |n %>"]').click(function(e) { ++jQuery(document.getElementById(<% $id |n,j %>)).click(function(e) { + var slice = searchChart.getElementAtEvent(e); + if ( !slice[0] ) return; + +diff --git a/share/html/Search/Results.html b/share/html/Search/Results.html +index 9438ca30..746af51b 100644 +--- a/share/html/Search/Results.html ++++ b/share/html/Search/Results.html +@@ -166,7 +166,7 @@ else { + # We call it RowsPerPage everywhere else. + + $Rows //= $ARGS{'RowsPerPage'} // $prefs->{'RowsPerPage'} // RT->Config->Get('DefaultSearchResultRowsPerPage') // 50; +-$Page = 1 unless $Page && $Page > 0; ++$Page = 1 unless $Page && $Page =~ /\A\d+\z/ && $Page > 1; + + my $hash_name = join '-', 'CurrentSearchHash', $Class, $ObjectType || (); + my $session_name = join '-', 'collection', $Class, $ObjectType || ();