If set to a false value, RT will allow the user to log in from any link
or request, merely by passing in C<user> and C<pass> parameters; setting
it to a true value forces all logins to come from the login box, so the
-user us aware that they are being logged in. The default is off, for
+user is aware that they are being logged in. The default is off, for
backwards compatability.
=cut
Set($RestrictLoginReferrer, 0);
+=item C<$ReferrerWhitelist>
+
+This is a list of hostname:port combinations that RT will treat as being
+part of RT's domain. This is particularly useful if you access RT as
+multiple hostnames or have an external auth system that needs to
+redirect back to RT once authentication is complete.
+
+ Set(@ReferrerWhitelist, qw(www.example.com:443 www3.example.com:80));
+
+If the "RT has detected a possible cross-site request forgery" error is triggered
+by a host:port sent by your browser that you believe should be valid, you can copy
+the host:port from the error message into this list.
+
+=cut
+
+Set(@ReferrerWhitelist, qw());
+
=back
Set($Framebusting, 1);
+=item C<$RestrictReferrer>
+
+If set to a false value, the HTTP C<Referer> (sic) header will not be
+checked to ensure that requests come from RT's own domain. As RT allows
+for GET requests to alter state, disabling this opens RT up to
+cross-site request forgery (CSRF) attacks.
+
+=cut
+
+Set($RestrictReferrer, 1);
+
+=item C<$RestrictLoginReferrer>
+
+If set to a false value, RT will allow the user to log in from any link
+or request, merely by passing in C<user> and C<pass> parameters; setting
+it to a true value forces all logins to come from the login box, so the
+user us aware that they are being logged in. The default is off, for
+backwards compatability.
+
+=cut
+
+Set($RestrictLoginReferrer, 0);
+
=back
+
+
=head1 Authorization and user configuration
=over 4
$MIMEObj->head->delete('RT-Attach-Message');
- my $attachments = RT::Attachments->new( $self->TransactionObj->CreatorObj );
+ my $attachments = RT::Attachments->new( RT->SystemUser );
$attachments->Limit(
FIELD => 'TransactionId',
VALUE => $self->TransactionObj->Id
ValidateWebConfig();
DecodeARGS($ARGS);
+ local $HTML::Mason::Commands::DECODED_ARGS = $ARGS;
PreprocessTimeUpdates($ARGS);
InitializeMenu();
my $m = $HTML::Mason::Commands::m;
+ # Ensure that the cookie that we send is up-to-date, in case the
+ # session-id has been modified in any way
+ SendSessionCookie();
+
# precache all system level rights for the current user
$HTML::Mason::Commands::session{CurrentUser}->PrincipalObj->HasRights( Object => RT->System );
InstantiateNewSession();
$HTML::Mason::Commands::session{'CurrentUser'} = $user_obj;
- SendSessionCookie();
$m->callback( %$ARGS, CallbackName => 'SuccessfulLogin', CallbackPage => '/autohandler' );
sub InstantiateNewSession {
tied(%HTML::Mason::Commands::session)->delete if tied(%HTML::Mason::Commands::session);
tie %HTML::Mason::Commands::session, 'RT::Interface::Web::Session', undef;
+ SendSessionCookie();
}
sub SendSessionCookie {
return @roots;
}
-my %is_whitelisted_path = (
+our %is_whitelisted_component = (
# The RSS feed embeds an auth token in the path, but query
# information for the search. Because it's a straight-up read, in
# addition to embedding its own auth, it's fine.
my $comp = shift;
my $ARGS = shift;
- return 1 if $is_whitelisted_path{$comp};
+ return 1 if $is_whitelisted_component{$comp};
my %args = %{ $ARGS };
delete $args{results} if $args{results}
and $HTML::Mason::Commands::session{"Actions"}->{$args{results}};
+ # The homepage refresh, which uses the Refresh header, doesn't send
+ # a referer in most browsers; whitelist the one parameter it reloads
+ # with, HomeRefreshInterval, which is safe
+ delete $args{HomeRefreshInterval};
+
# If there are no arguments, then it's likely to be an idempotent
# request, which are not susceptible to CSRF
return 1 if !%args;
sub IsRefererCSRFWhitelisted {
my $referer = _NormalizeHost(shift);
- my $config = _NormalizeHost(RT->Config->Get('WebBaseURL'));
+ my $base_url = _NormalizeHost(RT->Config->Get('WebBaseURL'));
+ $base_url = $base_url->host_port;
- return (1,$referer,$config) if $referer->host_port eq $config->host_port;
+ my $configs;
+ for my $config ( $base_url, RT->Config->Get('ReferrerWhitelist') ) {
+ push @$configs,$config;
+ return 1 if $referer->host_port eq $config;
+ }
- return (0,$referer,$config);
+ return (0,$referer,$configs);
}
=head3 _NormalizeHost
"your browser did not supply a Referrer header", # loc
) if !$ENV{HTTP_REFERER};
- my ($whitelisted, $browser, $config) = IsRefererCSRFWhitelisted($ENV{HTTP_REFERER});
+ my ($whitelisted, $browser, $configs) = IsRefererCSRFWhitelisted($ENV{HTTP_REFERER});
return 0 if $whitelisted;
+ if ( @$configs > 1 ) {
+ return (1,
+ "the Referrer header supplied by your browser ([_1]) is not allowed by RT's configured hostname ([_2]) or whitelisted hosts ([_3])", # loc
+ $browser->host_port,
+ shift @$configs,
+ join(', ', @$configs) );
+ }
+
return (1,
"the Referrer header supplied by your browser ([_1]) is not allowed by RT's configured hostname ([_2])", # loc
- $browser->host_port, $config->host_port);
+ $browser->host_port,
+ $configs->[0]);
}
sub ExpandCSRFToken {
return unless $user->ValidateAuthString( $data->{auth}, $token );
%{$ARGS} = %{$data->{args}};
+ $HTML::Mason::Commands::DECODED_ARGS = $ARGS;
# We explicitly stored file attachments with the request, but not in
# the session yet, as that would itself be an attack. Put them into
return 1;
}
-sub MaybeShowInterstitialCSRFPage {
+sub StoreRequestToken {
my $ARGS = shift;
- return unless RT->Config->Get('RestrictReferrer');
-
- # Deal with the form token provided by the interstitial, which lets
- # browsers which never set referer headers still use RT, if
- # painfully. This blows values into ARGS
- return if ExpandCSRFToken($ARGS);
-
- my ($is_csrf, $msg, @loc) = IsPossibleCSRF($ARGS);
- return if !$is_csrf;
-
- $RT::Logger->notice("Possible CSRF: ".RT::CurrentUser->new->loc($msg, @loc));
-
my $token = Digest::MD5::md5_hex(time . {} . $$ . rand(1024));
my $user = $HTML::Mason::Commands::session{'CurrentUser'}->UserObj;
my $data = {
$HTML::Mason::Commands::session{'CSRF'}->{$token} = $data;
$HTML::Mason::Commands::session{'i'}++;
+ return $token;
+}
+
+sub MaybeShowInterstitialCSRFPage {
+ my $ARGS = shift;
+
+ return unless RT->Config->Get('RestrictReferrer');
+
+ # Deal with the form token provided by the interstitial, which lets
+ # browsers which never set referer headers still use RT, if
+ # painfully. This blows values into ARGS
+ return if ExpandCSRFToken($ARGS);
+
+ my ($is_csrf, $msg, @loc) = IsPossibleCSRF($ARGS);
+ return if !$is_csrf;
+
+ $RT::Logger->notice("Possible CSRF: ".RT::CurrentUser->new->loc($msg, @loc));
+ my $token = StoreRequestToken($ARGS);
$HTML::Mason::Commands::m->comp(
'/Elements/CSRF',
- OriginalURL => $HTML::Mason::Commands::r->path_info,
+ OriginalURL => RT->Config->Get('WebPath') . $HTML::Mason::Commands::r->path_info,
Reason => HTML::Mason::Commands::loc( $msg, @loc ),
Token => $token,
);
],
default_escape_flags => 'h',
data_dir => "$RT::MasonDataDir",
- allow_globals => [qw(%session)],
+ allow_globals => [qw(%session $DECODED_ARGS)],
# Turn off static source if we're in developer mode.
static_source => (RT->Config->Get('DevelMode') ? '0' : '1'),
use_object_files => (RT->Config->Get('DevelMode') ? '0' : '1'),
<& /Elements/Framekiller &>
% if ($Refresh && $Refresh =~ /^(\d+)/ && $1 > 0) {
-% my $URL = $m->notes->{LogoutURL}; $URL = $URL ? ";URL=$URL" : "";
+% my $URL = $m->notes->{RefreshURL}; $URL = $URL ? ";URL=$URL" : "";
<meta http-equiv="refresh" content="<% "$1$URL" %>" />
% }
my $section;
if ( $request_path =~ m|^/Admin/$type/?(?:index.html)?$|
|| ( $request_path =~ m|^/Admin/$type/(?:Modify.html)$|
- && $m->request_args->{'Create'} )
+ && $DECODED_ARGS->{'Create'} )
)
{
$section = $tabs;
}
if ( $request_path =~ m{^/Admin/Queues} ) {
- if ( $m->request_args->{'id'} && $m->request_args->{'id'} =~ /^\d+$/
+ if ( $DECODED_ARGS->{'id'} && $DECODED_ARGS->{'id'} =~ /^\d+$/
||
- $m->request_args->{'Queue'} && $m->request_args->{'Queue'} =~ /^\d+$/
+ $DECODED_ARGS->{'Queue'} && $DECODED_ARGS->{'Queue'} =~ /^\d+$/
) {
- my $id = $m->request_args->{'Queue'} || $m->request_args->{'id'};
+ my $id = $DECODED_ARGS->{'Queue'} || $DECODED_ARGS->{'id'};
my $queue_obj = RT::Queue->new( $session{'CurrentUser'} );
$queue_obj->Load($id);
}
}
if ( $request_path =~ m{^/Admin/Users} ) {
- if ( $m->request_args->{'id'} && $m->request_args->{'id'} =~ /^\d+$/ ) {
- my $id = $m->request_args->{'id'};
+ if ( $DECODED_ARGS->{'id'} && $DECODED_ARGS->{'id'} =~ /^\d+$/ ) {
+ my $id = $DECODED_ARGS->{'id'};
my $obj = RT::User->new( $session{'CurrentUser'} );
$obj->Load($id);
}
if ( $request_path =~ m{^/Admin/Groups} ) {
- if ( $m->request_args->{'id'} && $m->request_args->{'id'} =~ /^\d+$/ ) {
- my $id = $m->request_args->{'id'};
+ if ( $DECODED_ARGS->{'id'} && $DECODED_ARGS->{'id'} =~ /^\d+$/ ) {
+ my $id = $DECODED_ARGS->{'id'};
my $obj = RT::Group->new( $session{'CurrentUser'} );
$obj->Load($id);
}
if ( $request_path =~ m{^/Admin/CustomFields/} ) {
- if ( $m->request_args->{'id'} && $m->request_args->{'id'} =~ /^\d+$/ ) {
- my $id = $m->request_args->{'id'};
+ if ( $DECODED_ARGS->{'id'} && $DECODED_ARGS->{'id'} =~ /^\d+$/ ) {
+ my $id = $DECODED_ARGS->{'id'};
my $obj = RT::CustomField->new( $session{'CurrentUser'} );
$obj->Load($id);
if ( $request_path =~ m{^/Admin/Articles/Classes/} ) {
my $tabs = PageMenu();
- if ( my $id = $m->request_args->{'id'} ) {
+ if ( my $id = $DECODED_ARGS->{'id'} ) {
my $obj = RT::CustomField->new( $session{'CurrentUser'} );
$obj->Load($id);
$about_me->child( logout => title => loc('Logout'), path => '/NoAuth/Logout.html' );
}
if ( $request_path =~ m{^/Dashboards/(\d+)?}) {
- if ( my $id = ( $1 || $m->request_args->{'id'} ) ) {
+ if ( my $id = ( $1 || $DECODED_ARGS->{'id'} ) ) {
my $obj = RT::Dashboard->new( $session{'CurrentUser'} );
$obj->LoadById($id);
if ( $obj and $obj->id ) {
if ( $request_path =~ m{^/Ticket/} ) {
- if ( ( $m->request_args->{'id'} || '' ) =~ /^(\d+)$/ ) {
+ if ( ( $DECODED_ARGS->{'id'} || '' ) =~ /^(\d+)$/ ) {
my $id = $1;
my $obj = RT::Ticket->new( $session{'CurrentUser'} );
$obj->Load($id);
&& $request_path !~ m{^/Search/Simple\.html}
)
|| ( $request_path =~ m{^/Search/Simple\.html}
- && $m->request_args->{'q'} )
+ && $DECODED_ARGS->{'q'} )
)
{
my $search = Menu()->child('search');
my $args = '';
my $has_query = '';
my $current_search = $session{"CurrentSearchHash"} || {};
- my $search_id = $m->request_args->{'SavedSearchLoad'} || $m->request_args->{'SavedSearchId'} || $current_search->{'SearchId'} || '';
- my $chart_id = $m->request_args->{'SavedChartSearchId'} || $current_search->{SavedChartSearchId};
+ my $search_id = $DECODED_ARGS->{'SavedSearchLoad'} || $DECODED_ARGS->{'SavedSearchId'} || $current_search->{'SearchId'} || '';
+ my $chart_id = $DECODED_ARGS->{'SavedChartSearchId'} || $current_search->{SavedChartSearchId};
- $has_query = 1 if ( $m->request_args->{'Query'} or $current_search->{'Query'} );
+ $has_query = 1 if ( $DECODED_ARGS->{'Query'} or $current_search->{'Query'} );
my %query_args;
my %fallback_query_args = (
(
map {
my $p = $_;
- $p => $m->request_args->{$p} || $current_search->{$p}
+ $p => $DECODED_ARGS->{$p} || $current_search->{$p}
} qw(Query Format OrderBy Order Page)
),
RowsPerPage => (
- defined $m->request_args->{'RowsPerPage'}
- ? $m->request_args->{'RowsPerPage'}
+ defined $DECODED_ARGS->{'RowsPerPage'}
+ ? $DECODED_ARGS->{'RowsPerPage'}
: $current_search->{'RowsPerPage'}
),
);
}
if ( $request_path =~ m{^/Article/} ) {
- if ( $m->request_args->{'id'} && $m->request_args->{'id'} =~ /^\d+$/ ) {
- my $id = $m->request_args->{'id'};
+ if ( $DECODED_ARGS->{'id'} && $DECODED_ARGS->{'id'} =~ /^\d+$/ ) {
+ my $id = $DECODED_ARGS->{'id'};
my $tabs = PageMenu();
$tabs->child( display => title => loc('Display'), path => "/Articles/Article/Display.html?id=".$id );
my $tabs = PageMenu();
$tabs->child( search => title => loc("Search"), path => "/Articles/Article/Search.html" );
$tabs->child( create => title => loc("New Article" ), path => "/Articles/Article/PreCreate.html" );
- if ( $request_path =~ m{^/Articles/Article/} and ( $m->request_args->{'id'} || '' ) =~ /^(\d+)$/ ) {
+ if ( $request_path =~ m{^/Articles/Article/} and ( $DECODED_ARGS->{'id'} || '' ) =~ /^(\d+)$/ ) {
my $id = $1;
my $obj = RT::Article->new( $session{'CurrentUser'} );
$obj->Load($id);
}
$m->callback( %ARGS, CallbackName => 'AfterSessionDelete' );
-$m->notes->{LogoutURL} = $URL;
+$m->notes->{RefreshURL} = $URL;
</%INIT>
%#
%# END BPS TAGGED BLOCK }}}
<& /Elements/Header, Title => $title,
- Refresh => $session{'tickets_refresh_interval'} || RT->Config->Get('SearchResultsRefreshInterval', $session{'CurrentUser'} ),
+ Refresh => $refresh,
LinkRel => \%link_rel &>
<& /Elements/Tabs &>
<& /Elements/CollectionList,
$session{'tickets_refresh_interval'} = $ARGS{'TicketsRefreshInterval'};
}
+my $refresh = $session{'tickets_refresh_interval'}
+ || RT->Config->Get('SearchResultsRefreshInterval', $session{'CurrentUser'} );
+
+if (RT->Config->Get('RestrictReferrer') and $refresh and not $m->request_args->{CSRF_Token}) {
+ my $token = RT::Interface::Web::StoreRequestToken( $session{'CurrentSearchHash'} );
+ $m->notes->{RefreshURL} = RT->Config->Get('WebURL')
+ . "Search/Results.html?CSRF_Token="
+ . $token;
+}
+
my %link_rel;
my $genpage = sub {
return $m->comp(