Index: debian/control =================================================================== --- debian/control (revision 1032) +++ debian/control (working copy) @@ -40,6 +40,7 @@ libgraphviz-perl, libgnupg-interface-perl, libjs-scriptaculous, libjs-prototype, libipc-run-safehandles-perl, perl (>= 5.10.0) | libencode-perl (>= 2.21), + perl (>= 5.10.0) | libdigest-sha-perl, ${misc:Depends} Recommends: speedy-cgi-perl, libdatetime-locale-perl, libdatetime-perl Suggests: rt3.8-rtfm Index: debian/patches/76_security-2011-04-14-3.8.6.dpatch =================================================================== --- debian/patches/76_security-2011-04-14-3.8.6.dpatch (revision 0) +++ debian/patches/76_security-2011-04-14-3.8.6.dpatch (revision 1048) @@ -0,0 +1,593 @@ +#! /bin/sh /usr/share/dpatch/dpatch-run +## 76_security-2011-04-14-3.8.6.dpatch +## +## DP: * Multiple security fixes for: +## DP: - Remote code execution in external custom fields (CVE-2011-1685) +## DP: - Information disclosure via SQL injection (CVE-2011-1686) +## DP: - Information disclosure via search interface (CVE-2011-1687) +## DP: - Information disclosure via directory traversal (CVE-2011-1688) +## DP: - User javascript execution via XSS vulnerability (CVE-2011-1689) +## DP: - Authentication credentials theft (CVE-2011-1690) +## DP: +## DP: Patch supplied by Best Practical with only file paths adjusted +diff --git a/bin/mason_handler.fcgi b/bin/mason_handler.fcgi +index 48155f2..756311b 100755 +--- a/bin/mason_handler.fcgi.in ++++ b/bin/mason_handler.fcgi.in +@@ -75,6 +75,17 @@ while ( my $cgi = CGI::Fast->new ) { + Module::Refresh->refresh if RT->Config->Get('DevelMode'); + RT::ConnectToDatabase(); + ++ # Each environment has its own way of handling .. and so on in paths, ++ # so RT consistently forbids such paths. ++ if ( $cgi->path_info =~ m{/\.} ) { ++ $RT::Logger->crit("Invalid request for ".$cgi->path_info." aborting"); ++ print STDOUT "HTTP/1.0 400\r\n\r\n"; ++ ++ RT::Interface::Web::Handler->CleanupRequest(); ++ ++ next; ++ } ++ + if ( ( !$Handler->interp->comp_exists( $cgi->path_info ) ) + && ( $Handler->interp->comp_exists( $cgi->path_info . "/index.html" ) ) ) { + $cgi->path_info( $cgi->path_info . "/index.html" ); +diff --git a/bin/mason_handler.scgi b/bin/mason_handler.scgi +index a853529..20340b4 100755 +--- a/bin/mason_handler.scgi.in ++++ b/bin/mason_handler.scgi.in +@@ -63,6 +63,18 @@ $Handler ||= RT::Interface::Web::Handler->new( + + + my $cgi = CGI->new; ++ ++# Each environment has its own way of handling .. and so on in paths, ++# so RT consistently forbids such paths. ++if ( $cgi->path_info =~ m{/\.} ) { ++ $RT::Logger->crit("Invalid request for ".$cgi->path_info." aborting"); ++ print STDOUT "HTTP/1.0 400\r\n\r\n"; ++ ++ RT::Interface::Web::Handler->CleanupRequest(); ++ ++ return 0; ++} ++ + if ( ( !$Handler->interp->comp_exists( $cgi->path_info ) ) + && ( $Handler->interp->comp_exists( $cgi->path_info . "/index.html" ) ) ) { + $cgi->path_info( $cgi->path_info . "/index.html" ); +diff --git a/bin/mason_handler.svc b/bin/mason_handler.svc +index d7e68b3..ed8dbf0 100755 +--- a/bin/mason_handler.svc.in ++++ b/bin/mason_handler.svc.in +@@ -234,6 +234,17 @@ $Handler ||= RT::Interface::Web::Handler->new( + while( my $cgi = CGI::Fast->new ) { + my $comp = $ENV{'PATH_INFO'}; + ++ # Each environment has its own way of handling .. and so on in paths, ++ # so RT consistently forbids such paths. ++ if ( $cgi->path_info =~ m{/\.} ) { ++ $RT::Logger->crit("Invalid request for ".$cgi->path_info." aborting"); ++ print STDOUT "HTTP/1.0 400\r\n\r\n"; ++ ++ RT::Interface::Web::Handler->CleanupRequest(); ++ ++ next; ++ } ++ + $comp = $1 if ($comp =~ /^(.*)$/); + my $web_path = RT->Config->Get('WebPath'); + $comp =~ s|^\Q$web_path\E\b||i; +diff --git a/bin/webmux.pl b/bin/webmux.pl +index 7e61b27..aab7e70 100755 +--- a/bin/webmux.pl.in ++++ b/bin/webmux.pl.in +@@ -157,6 +157,19 @@ sub handler { + + RT::Init(); + ++ # none of the methods in $r gives us the information we want (most ++ # canonicalize /foo/../bar to /bar which is exactly what we want to avoid) ++ my $uri = URI->new("http://".$r->hostname.$r->unparsed_uri); ++ my $path = URI::Escape::uri_unescape($uri->path); ++ ++ ## Each environment has its own way of handling .. and so on in paths, ++ ## so RT consistently forbids such paths. ++ if ( $path =~ m{/\.} ) { ++ $RT::Logger->crit("Invalid request for ".$path." aborting"); ++ RT::Interface::Web::Handler->CleanupRequest(); ++ return 400; ++ } ++ + $Handler ||= RT::Interface::Web::Handler->new( + RT->Config->Get('MasonParameters') + ); +diff --git a/lib/RT/CustomFieldValues/External.pm b/lib/RT/CustomFieldValues/External.pm +index 645f136..b94d8fd 100644 +--- a/lib/RT/CustomFieldValues/External.pm ++++ b/lib/RT/CustomFieldValues/External.pm +@@ -123,57 +123,49 @@ sub __BuildLimitCheck { + my ($self, %args) = (@_); + return undef unless $args{'FIELD'} =~ /^(?:Name|Description)$/; + +- $args{'OPERATOR'} ||= '='; +- my $quoted_value = $args{'VALUE'}; +- if ( $quoted_value ) { +- $quoted_value =~ s/'/\\'/g; +- $quoted_value = "'$quoted_value'"; +- } +- +- my $code = <$args{'FIELD'}; +-my \$condition = $quoted_value; +-END +- +- if ( $args{'OPERATOR'} =~ /^(?:=|!=|<>)$/ ) { +- $code .= 'return 0 unless defined $value;'; +- my %h = ( '=' => ' eq ', '!=' => ' ne ', '<>' => ' ne ' ); +- $code .= 'return 0 unless $value'. $h{ $args{'OPERATOR'} } .'$condition;'; +- $code .= 'return 1;' +- } +- elsif ( $args{'OPERATOR'} =~ /^(?:LIKE|NOT LIKE)$/i ) { +- $code .= 'return 0 unless defined $value;'; +- my %h = ( 'LIKE' => ' =~ ', 'NOT LIKE' => ' !~ ' ); +- $code .= 'return 0 unless $value'. $h{ uc $args{'OPERATOR'} } .'/\Q$condition/i;'; +- $code .= 'return 1;' +- } +- else { +- $code .= 'return 0;' +- } +- $code = "sub {$code}"; +- my $cb = eval "$code"; +- $RT::Logger->error( "Couldn't build callback '$code': $@" ) if $@; +- return $cb; ++ my $condition = $args{VALUE}; ++ my $op = $args{'OPERATOR'} || '='; ++ my $field = $args{FIELD}; ++ ++ return sub { ++ my $record = shift; ++ my $value = $record->$field; ++ return 0 unless defined $value; ++ if ($op eq "=") { ++ return 0 unless $value eq $condition; ++ } elsif ($op eq "!=" or $op eq "<>") { ++ return 0 unless $value ne $condition; ++ } elsif (uc($op) eq "LIKE") { ++ return 0 unless $value =~ /\Q$condition\E/i; ++ } elsif (rc($op) eq "NOT LIKE") { ++ return 0 unless $value !~ /\Q$condition\E/i; ++ } else { ++ return 0; ++ } ++ return 1; ++ }; + } + + sub __BuildAggregatorsCheck { + my $self = shift; ++ my @cbs = grep {$_->{CALLBACK}} @{ $self->{'__external_cf_limits'} }; ++ return undef unless @cbs; + +- my %h = ( OR => ' || ', AND => ' && ' ); +- +- my $code = ''; +- for( my $i = 0; $i < @{ $self->{'__external_cf_limits'} }; $i++ ) { +- next unless $self->{'__external_cf_limits'}->[$i]->{'CALLBACK'}; +- $code .= $h{ uc($self->{'__external_cf_limits'}->[$i]->{'ENTRYAGGREGATOR'} || 'OR') } if $code; +- $code .= '$sb->{\'__external_cf_limits\'}->['. $i .']->{\'CALLBACK\'}->($record)'; +- } +- return unless $code; ++ my %h = ( ++ OR => sub { defined $_[0] ? ($_[0] || $_[1]) : $_[1] }, ++ AND => sub { defined $_[0] ? ($_[0] && $_[1]) : $_[1] }, ++ ); + +- $code = "sub { my (\$sb,\$record) = (\@_); return $code }"; +- my $cb = eval "$code"; +- $RT::Logger->error( "Couldn't build callback '$code': $@" ) if $@; +- return $cb; ++ return sub { ++ my ($sb, $record) = @_; ++ my $ok; ++ for my $limit ( @cbs ) { ++ $ok = $h{$limit->{ENTRYAGGREGATOR} || 'OR'}->( ++ $ok, $limit->{CALLBACK}->($record), ++ ); ++ } ++ return $ok; ++ }; + } + + sub _DoSearch { +diff --git a/lib/RT/Interface/Web.pm b/lib/RT/Interface/Web.pm +index 549d24c..55ec6bf 100755 +--- a/lib/RT/Interface/Web.pm ++++ b/lib/RT/Interface/Web.pm +@@ -192,6 +192,8 @@ sub HandleRequest { + SendSessionCookie(); + $HTML::Mason::Commands::session{'CurrentUser'} = RT::CurrentUser->new() unless _UserLoggedIn(); + ++ MaybeRejectPrivateComponentRequest(); ++ + MaybeShowNoAuthPage($ARGS); + + AttemptExternalAuth($ARGS) unless ( ! RT->Config->Get('WebExternalAuthContinuous') && _UserLoggedIn() ); +@@ -284,6 +286,37 @@ sub MaybeShowNoAuthPage { + $m->abort; + } + ++=head2 MaybeRejectPrivateComponentRequest ++ ++This function will reject calls to private components, like those under ++C. If the requested path is a private component then we will ++abort with a C<403> error. ++ ++=cut ++ ++sub MaybeRejectPrivateComponentRequest { ++ my $m = $HTML::Mason::Commands::m; ++ my $path = $m->request_comp->path; ++ ++ # We do not check for dhandler here, because requesting our dhandlers ++ # directly is okay. Mason will invoke the dhandler with a dhandler_arg of ++ # 'dhandler'. ++ ++ if ($path =~ m{ ++ / # leading slash ++ ( Elements | ++ _elements | # mobile UI ++ Widgets | ++ autohandler | # requesting this directly is suspicious ++ l ) # loc component ++ ( $ | / ) # trailing slash or end of path ++ }xi) { ++ $m->abort(403); ++ } ++ ++ return; ++} ++ + =head2 ShowRequestedPage \%ARGS + + This function, called exclusively by RT's autohandler, dispatches +diff --git a/lib/RT/Interface/Web/Standalone.pm b/lib/RT/Interface/Web/Standalone.pm +index 12bd276..58382b1 100755 +--- a/lib/RT/Interface/Web/Standalone.pm ++++ b/lib/RT/Interface/Web/Standalone.pm +@@ -77,6 +77,15 @@ sub handle_request { + + Module::Refresh->refresh if RT->Config->Get('DevelMode'); + RT::ConnectToDatabase() unless RT->InstallMode; ++ ++ # Each environment has its own way of handling .. and so on in paths, ++ # so RT consistently forbids such paths. ++ if ( $cgi->path_info =~ m{/\.} ) { ++ $RT::Logger->crit("Invalid request for ".$cgi->path_info." aborting"); ++ print STDOUT "HTTP/1.0 400\r\n\r\n"; ++ return RT::Interface::Web::Handler->CleanupRequest(); ++ } ++ + $self->SUPER::handle_request($cgi); + $RT::Logger->crit($@) if $@ && $RT::Logger; + warn $@ if $@ && !$RT::Logger; +diff --git a/lib/RT/SearchBuilder.pm b/lib/RT/SearchBuilder.pm +index f6b3571..770d942 100755 +--- a/lib/RT/SearchBuilder.pm ++++ b/lib/RT/SearchBuilder.pm +@@ -85,6 +85,17 @@ sub _Init { + $self->SUPER::_Init( 'Handle' => $RT::Handle); + } + ++sub OrderByCols { ++ my $self = shift; ++ my @sort; ++ for my $s (@_) { ++ next if defined $s->{FIELD} and $s->{FIELD} =~ /\W/; ++ $s->{FIELD} = $s->{FUNCTION} if $s->{FUNCTION}; ++ push @sort, $s; ++ } ++ return $self->SUPER::OrderByCols( @sort ); ++} ++ + =head2 LimitToEnabled + + Only find items that haven't been disabled +@@ -278,14 +289,47 @@ This Limit sub calls SUPER::Limit, but defaults "CASESENSITIVE" to 1, thus + making sure that by default lots of things don't do extra work trying to + match lower(colname) agaist lc($val); + ++We also force VALUE to C when the OPERATOR is C or C. ++This ensures that we don't pass invalid SQL to the database or allow SQL ++injection attacks when we pass through user specified values. ++ + =cut + + sub Limit { + my $self = shift; +- my %args = ( CASESENSITIVE => 1, +- @_ ); ++ my %ARGS = ( ++ CASESENSITIVE => 1, ++ OPERATOR => '=', ++ @_, ++ ); + +- return $self->SUPER::Limit(%args); ++ # We use the same regex here that DBIx::SearchBuilder uses to exclude ++ # values from quoting ++ if ( $ARGS{'OPERATOR'} =~ /IS/i ) { ++ # Don't pass anything but NULL for IS and IS NOT ++ $ARGS{'VALUE'} = 'NULL'; ++ } ++ ++ if ($ARGS{FUNCTION}) { ++ ($ARGS{ALIAS}, $ARGS{FIELD}) = split /\./, delete $ARGS{FUNCTION}, 2; ++ $self->SUPER::Limit(%ARGS); ++ } elsif ($ARGS{FIELD} =~ /\W/ ++ or $ARGS{OPERATOR} !~ /^(=|<|>|!=|<>|<=|>= ++ |(NOT\s*)?LIKE ++ |(NOT\s*)?(STARTS|ENDS)WITH ++ |(NOT\s*)?MATCHES ++ |IS(\s*NOT)? ++ |IN)$/ix) { ++ $RT::Logger->crit("Possible SQL injection attack: $ARGS{FIELD} $ARGS{OPERATOR}"); ++ $self->SUPER::Limit( ++ %ARGS, ++ FIELD => 'id', ++ OPERATOR => '<', ++ VALUE => '0', ++ ); ++ } else { ++ $self->SUPER::Limit(%ARGS); ++ } + } + + =head2 ItemsOrderBy +diff --git a/lib/RT/Tickets_Overlay.pm b/lib/RT/Tickets_Overlay.pm +index b8f9756..8d98ee9 100755 +--- a/lib/RT/Tickets_Overlay.pm ++++ b/lib/RT/Tickets_Overlay.pm +@@ -145,6 +145,13 @@ our %FIELD_METADATA = ( + WatcherGroup => [ 'MEMBERSHIPFIELD', ], #loc_left_pair + ); + ++our %SEARCHABLE_SUBFIELDS = ( ++ User => [qw( ++ EmailAddress Name RealName Nickname Organization Address1 Address2 ++ WorkPhone HomePhone MobilePhone PagerPhone id ++ )], ++); ++ + # Mapping of Field Type to Function + our %dispatch = ( + ENUM => \&_EnumLimit, +@@ -801,6 +808,13 @@ sub _WatcherLimit { + my $type = $meta->[1] || ''; + my $class = $meta->[2] || 'Ticket'; + ++ # Bail if the subfield is not allowed ++ if ( $rest{SUBKEY} ++ and not grep { $_ eq $rest{SUBKEY} } @{$SEARCHABLE_SUBFIELDS{'User'}}) ++ { ++ die "Invalid watcher subfield: '$rest{SUBKEY}'"; ++ } ++ + # Owner was ENUM field, so "Owner = 'xxx'" allowed user to + # search by id and Name at the same time, this is workaround + # to preserve backward compatibility +@@ -1199,7 +1213,7 @@ Try and turn a CF descriptor into (cfid, cfname) object pair. + sub _CustomFieldDecipher { + my ($self, $string) = @_; + +- my ($queue, $field, $column) = ($string =~ /^(?:(.+?)\.)?{(.+)}(?:\.(.+))?$/); ++ my ($queue, $field, $column) = ($string =~ /^(?:(.+?)\.)?{(.+)}(?:\.(Content|LargeContent))?$/); + $field ||= ($string =~ /^{(.*?)}$/)[0] || $string; + + my $cf; +@@ -1653,9 +1667,20 @@ sub OrderByCols { + foreach my $uid ( $self->CurrentUser->Id, $RT::Nobody->Id ) { + if ( RT->Config->Get('DatabaseType') eq 'Oracle' ) { + my $f = ($row->{'ALIAS'} || 'main') .'.Owner'; +- push @res, { %$row, ALIAS => '', FIELD => "CASE WHEN $f=$uid THEN 1 ELSE 0 END", ORDER => $order } ; ++ push @res, { ++ %$row, ++ FIELD => undef, ++ ALIAS => '', ++ FUNCTION => "CASE WHEN $f=$uid THEN 1 ELSE 0 END", ++ ORDER => $order ++ }; + } else { +- push @res, { %$row, FIELD => "Owner=$uid", ORDER => $order } ; ++ push @res, { ++ %$row, ++ FIELD => undef, ++ FUNCTION => "Owner=$uid", ++ ORDER => $order ++ }; + } + } + +diff --git a/html/Elements/Header b/share/html/Elements/Header +index 5b7abe1..f9bd27f 100755 +--- a/share/html/Elements/Header ++++ b/share/html/Elements/Header +@@ -54,7 +54,8 @@ + + + % if ($Refresh && $Refresh =~ /^(\d+)/ && $1 > 0) { +- ++% my $URL = $m->notes->{LogoutURL}; $URL = $URL ? ";URL=$URL" : ""; ++ " /> + % } + + +diff --git a/share/html/NoAuth/RichText/autohandler b/share/html/NoAuth/RichText/autohandler +new file mode 100644 +index 0000000..fd48b59 +--- /dev/null ++++ b/share/html/NoAuth/RichText/autohandler +@@ -0,0 +1,56 @@ ++%# BEGIN BPS TAGGED BLOCK {{{ ++%# ++%# COPYRIGHT: ++%# ++%# This software is Copyright (c) 1996-2011 Best Practical Solutions, LLC ++%# ++%# ++%# (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> ++my $file = $m->base_comp->source_file; ++if ($file =~ m{RichText/+FCKeditor}) { ++ $RT::Logger->crit("Invalid request directly to the rich text editor: $file"); ++ $m->abort(403); ++} else { ++ $m->call_next(); ++} ++ +diff --git a/share/html/Search/Chart b/share/html/Search/Chart +index 59e9fc6..f21265a 100644 +--- a/share/html/Search/Chart ++++ b/share/html/Search/Chart +@@ -66,6 +66,8 @@ if ($ChartStyle eq 'pie') { + + use RT::Report::Tickets; + my $tix = RT::Report::Tickets->new( $session{'CurrentUser'} ); ++my %AllowedGroupings = reverse $tix->Groupings( Query => $Query ); ++$PrimaryGroupBy = 'Queue' unless exists $AllowedGroupings{$PrimaryGroupBy}; + my ($count_name, $value_name) = $tix->SetupGroupings( + Query => $Query, GroupBy => $PrimaryGroupBy, + ); +diff --git a/share/html/Search/Elements/Chart b/share/html/Search/Elements/Chart +index 4f8c1e1..eb438a0 100644 +--- a/share/html/Search/Elements/Chart ++++ b/share/html/Search/Elements/Chart +@@ -54,6 +54,8 @@ $ChartStyle => 'bars' + <%init> + use RT::Report::Tickets; + my $tix = RT::Report::Tickets->new( $session{'CurrentUser'} ); ++my %AllowedGroupings = reverse $tix->Groupings( Query => $Query ); ++$PrimaryGroupBy = 'Queue' unless exists $AllowedGroupings{$PrimaryGroupBy}; + my ($count_name, $value_name) = $tix->SetupGroupings( + Query => $Query, GroupBy => $PrimaryGroupBy, + ); +diff --git a/share/html/Search/Elements/SelectPersonType b/share/html/Search/Elements/SelectPersonType +index 5f6ac73..0c50c9b 100644 +--- a/share/html/Search/Elements/SelectPersonType ++++ b/share/html/Search/Elements/SelectPersonType +@@ -72,7 +72,7 @@ else { + @types = qw(Requestor Cc AdminCc Watcher Owner QueueCc QueueAdminCc QueueWatcher); + } + +-my @subtypes = qw(EmailAddress Name RealName Nickname Organization Address1 Address2 WorkPhone HomePhone MobilePhone PagerPhone id); ++my @subtypes = @{ $RT::Tickets::SEARCHABLE_SUBFIELDS{'User'} }; + + + <%ARGS> +diff --git a/share/html/NoAuth/Logout.html b/share/html/NoAuth/Logout.html +index 103ae4f..fa21100 100755 +--- a/share/html/NoAuth/Logout.html ++++ b/share/html/NoAuth/Logout.html +@@ -45,7 +45,7 @@ + %# those contributions and any derivatives thereof. + %# + %# END BPS TAGGED BLOCK }}} +-<& /Elements/Header, Title => loc('Logout'), Refresh => "1;URL=$URL" &> ++<& /Elements/Header, Title => loc('Logout'), Refresh => 1 &> + + +