]> git.ipfire.org Git - thirdparty/bugzilla.git/commitdiff
Bug 1196743 - Fix information disclosure vulnerability that allows attacker to obtain...
authorDylan Hardison <dylan@mozilla.com>
Thu, 5 Nov 2015 05:28:14 +0000 (00:28 -0500)
committerDylan Hardison <dylan@mozilla.com>
Thu, 5 Nov 2015 05:28:14 +0000 (00:28 -0500)
12 files changed:
Bugzilla.pm
Bugzilla/Auth/Persist/Cookie.pm
Bugzilla/CGI.pm
Bugzilla/Token.pm
extensions/GitHubAuth/Extension.pm
extensions/GitHubAuth/lib/Client.pm
extensions/GitHubAuth/lib/Login.pm
extensions/GitHubAuth/lib/Util.pm [deleted file]
extensions/GitHubAuth/template/en/default/hook/account/auth/login-additional_methods.html.tmpl
extensions/GitHubAuth/template/en/default/hook/account/auth/login-small-additional_methods.html.tmpl
extensions/GitHubAuth/template/en/default/hook/global/code-error-errors.html.tmpl
github.cgi [new file with mode: 0755]

index 96f7cd0d22803f38ec745ba064ad4309e4f2582c..a219d5bde8dfc8614727555baa81daabf143e3f1 100644 (file)
@@ -353,6 +353,16 @@ sub page_requires_login {
     return $_[0]->request_cache->{page_requires_login};
 }
 
+sub github_secret {
+    my ($class) = @_;
+    my $cache = $class->request_cache;
+    my $cgi   = $class->cgi;
+
+    $cache->{github_secret} //= $cgi->cookie('github_secret') // generate_random_password(16);
+
+    return $cache->{github_secret};
+}
+
 sub login {
     my ($class, $type) = @_;
 
index 4adb00f96d852fc9ffa47e07779b705cab983e90..fd910b118c576e34022c6c7234be2100eeb5f39e 100644 (file)
@@ -90,6 +90,8 @@ sub persist_login {
         $cookieargs{'-secure'} = 1;
     }
 
+    $cgi->remove_cookie('github_secret');
+    $cgi->remove_cookie('Bugzilla_login_request_cookie');
     $cgi->send_cookie(-name => 'Bugzilla_login',
                       -value => $user->id,
                       %cookieargs);
index a5a1afc5cfa0923c9288d67324e3d107967fd2b6..4deb5aa528e6eb121b249b08abce1fe4b0ce8877 100644 (file)
@@ -31,6 +31,7 @@ use Bugzilla::Util;
 use Bugzilla::Search::Recent;
 
 use File::Basename;
+use URI;
 
 BEGIN {
     if (ON_WINDOWS) {
@@ -125,6 +126,21 @@ sub new {
     return $self;
 }
 
+sub target_uri {
+    my ($self) = @_;
+
+    my $base = correct_urlbase();
+    if (my $request_uri = $self->request_uri) {
+        my $base_uri = URI->new($base);
+        $base_uri->path('');
+        $base_uri->query(undef);
+        return $base_uri . $request_uri;
+    }
+    else {
+        return $base . ($self->url(-relative => 1, -query => 1) || 'index.cgi');
+    }
+}
+
 # We want this sorted plus the ability to exclude certain params
 sub canonicalise_query {
     my ($self, @exclude) = @_;
@@ -355,6 +371,16 @@ sub header {
                            %args);
     }
 
+    # We generate a cookie and store it in the request cache
+    # To initiate github login, a form POSTs to github.cgi with the
+    # github_secret as a parameter. It must match the github_secret cookie.
+    # this prevents some types of redirection attacks.
+    unless ($user->id) {
+        $self->send_cookie(-name     => 'github_secret',
+                           -value    => Bugzilla->github_secret,
+                           -httponly => 1);
+    }
+
     # Add the cookies in if we have any
     if (scalar(@{$self->{Bugzilla_cookie_list}})) {
         unshift(@_, '-cookie' => $self->{Bugzilla_cookie_list});
@@ -475,6 +501,8 @@ sub send_cookie {
     $paramhash{'-path'} = Bugzilla->params->{'cookiepath'};
     $paramhash{'-domain'} = Bugzilla->params->{'cookiedomain'}
         if Bugzilla->params->{'cookiedomain'};
+    $paramhash{'-secure'} = 1
+        if Bugzilla->params->{'ssl_redirect'};
 
     # Move the param list back into an array for the call to cookie().
     foreach (keys(%paramhash)) {
index 3c5261821e0f4270b494a5923104d981963f26bc..6e30955497fad7817a03fca1b6dc2b03a9e12cda 100644 (file)
@@ -237,7 +237,7 @@ sub issue_short_lived_session_token {
     # the token to the caller.
 
     $user //= Bugzilla->user;
-    return _create_token($user->id, 'session.short', $data);
+    return _create_token($user->id ? $user->id : undef, 'session.short', $data);
 }
 
 sub issue_hash_token {
index d68934be42dd7b22fc4ed1d8e75c37e128bbbef4..b813689cd9eaa42f0ea69327e74c841321a34d05 100644 (file)
@@ -12,7 +12,6 @@ use strict;
 use parent qw(Bugzilla::Extension);
 
 use Bugzilla::Extension::GitHubAuth::Client;
-use Bugzilla::Extension::GitHubAuth::Util qw(target_uri);
 
 use Bugzilla::Error;
 use Bugzilla::Util qw(trick_taint);
@@ -30,7 +29,7 @@ BEGIN {
         my ($stack, $method) = @_;
 
         return undef if $method eq 'fail_nodata';
-        return $stack->UNIVERSAL::can($method);
+        return $stack->SUPER::can($method);
     };
 }
 
@@ -42,18 +41,6 @@ sub install_before_final_checks {
     }) unless Bugzilla::Group->new({ name => 'no-github-auth' });
 }
 
-sub template_before_create {
-    my ($self, $args) = @_;
-
-    return if Bugzilla->user->id && !Bugzilla->cgi->param('logout');
-
-    $args->{config}{VARIABLES}{github_auth} = {
-        login => sub {
-            return Bugzilla::Extension::GitHubAuth::Client->login_uri(target_uri());
-        },
-    };
-}
-
 sub attachment_should_redirect_login {
     my ($self, $args) = @_;
     my $cgi = Bugzilla->cgi;
index 338a5b6392546c782511576e2372a68c926a08d4..77c8a6c6155bf84a9d65de3046c74189df5e0826 100644 (file)
@@ -15,7 +15,7 @@ use URI::QueryParam;
 use Digest;
 
 use Bugzilla::Extension::GitHubAuth::Client::Error qw(ThrowUserError ThrowCodeError);
-use Bugzilla::Util qw(remote_ip);
+use Bugzilla::Util qw(remote_ip correct_urlbase);
 
 use constant DIGEST_HASH => 'SHA1';
 
@@ -35,19 +35,22 @@ sub new {
 }
 
 sub login_uri {
-    my ($self, $target) = @_;
+    my ($class, $target_uri) = @_;
 
-    $target->query_param(GoAheadAndLogIn => 1);
-    $target->query_param(github_login => 1);
-    $target->query_param_delete('logout');
+    my $uri = URI->new(correct_urlbase() . "github.cgi");
+    $uri->query_form(target_uri => $target_uri);
+    return $uri;
+}
 
-    my $uri = URI->new(GH_AUTHORIZE_URI);
+sub authorize_uri {
+    my ($class, $state) = @_;
 
+    my $uri = URI->new(GH_AUTHORIZE_URI);
     $uri->query_form(
         client_id    => Bugzilla->params->{github_client_id},
         scope        => 'user:email',
-        state        => $self->get_state($target),
-        redirect_uri => $target,
+        state        => $state,
+        redirect_uri => correct_urlbase() . "github.cgi",
     );
 
     return $uri;
@@ -65,31 +68,6 @@ sub get_email_key {
     return $digest->hexdigest;
 }
 
-sub get_state {
-    my ($class, $target) = @_;
-    my $sorted_target = $target->clone;
-    $sorted_target->query_form({});
-
-    foreach my $key (sort $target->query_param) {
-        $sorted_target->query_param($key, $target->query_param($key));
-    }
-
-    $sorted_target->query_param_delete("code");
-    $sorted_target->query_param_delete("state");
-    $sorted_target->query_param_delete('github_email_key');
-    $sorted_target->query_param_delete('github_email');
-    $sorted_target->query_param_delete('GoAheadAndLogIn');
-    $sorted_target->query_param_delete('github_login');
-
-    my $cgi    = Bugzilla->cgi;
-    my $digest = Digest->new(DIGEST_HASH);
-    $digest->add($sorted_target->as_string);
-    $digest->add(remote_ip());
-    $digest->add($cgi->cookie('Bugzilla_github_token') // Bugzilla->request_cache->{github_token} // '');
-    $digest->add(Bugzilla->localconfig->{site_wide_secret});
-    return $digest->hexdigest;
-}
-
 sub _handle_response {
     my ($self, $response) = @_;
     my $data = eval {
index 6e105c33a8ed59f4213db874da06ae5e877e52dc..f1effb4e1efb6beaa7359566dac0465a8ae36443 100644 (file)
@@ -13,11 +13,12 @@ use fields qw(github_failure);
 
 use Scalar::Util qw(blessed);
 
-use Bugzilla::Constants qw(AUTH_NODATA AUTH_ERROR USAGE_MODE_BROWSER );
+use Bugzilla::Constants qw(AUTH_NODATA AUTH_ERROR USAGE_MODE_BROWSER);
 use Bugzilla::Util qw(trick_taint correct_urlbase generate_random_password);
+use Bugzilla::Token qw(issue_short_lived_session_token set_token_extra_data);
+use List::MoreUtils qw(any);
 use Bugzilla::Extension::GitHubAuth::Client;
 use Bugzilla::Extension::GitHubAuth::Client::Error ();
-use Bugzilla::Extension::GitHubAuth::Util qw(target_uri);
 use Bugzilla::Error;
 
 use constant { requires_verification   => 1,
@@ -27,30 +28,16 @@ use constant { requires_verification   => 1,
 sub get_login_info {
     my ($self) = @_;
     my $cgi              = Bugzilla->cgi;
-    my $github_login     = $cgi->param('github_login');
-    my $github_email     = $cgi->param('github_email');
-    my $github_email_key = $cgi->param('github_email_key');
-
-    my $cookie = $cgi->cookie('Bugzilla_github_token');
-    unless ($cookie) {
-        my $token = generate_random_password();
-        $cgi->send_cookie(-name     => 'Bugzilla_github_token',
-                          -value    => $token,
-                          Bugzilla->params->{'ssl_redirect'} ? ( -secure => 1 ) : (),
-                          -httponly => 1);
-        Bugzilla->request_cache->{github_token} = $token;
-    }
+    my $github_action    = Bugzilla->request_cache->{github_action};
 
-    return { failure => AUTH_NODATA } unless $github_login;
+    return { failure => AUTH_NODATA } unless $github_action;
 
     my $response;
-    if ($github_email_key && $github_email) {
-        trick_taint($github_email);
-        trick_taint($github_email_key);
-        $response = $self->_get_login_info_from_email($github_email, $github_email_key);
+    if ($github_action eq 'login') {
+        $response = $self->_get_login_info_from_github();
     }
-    else {
-       $response = $self->_get_login_info_from_github();
+    elsif ($github_action eq 'email') {
+        $response = $self->_get_login_info_from_email();
     }
 
     if (!exists $response->{failure}) {
@@ -84,20 +71,13 @@ sub _get_login_info_from_github {
     my ($self) = @_;
     my $cgi      = Bugzilla->cgi;
     my $template = Bugzilla->template;
-    my $state    = $cgi->param('state');
     my $code     = $cgi->param('code');
 
     return { failure => AUTH_ERROR, error => 'github_missing_code' } unless $code;
-    return { failure => AUTH_ERROR, error => 'github_invalid_state' } unless $state;
 
     trick_taint($code);
-    trick_taint($state);
 
-    my $target = target_uri();
     my $client = Bugzilla::Extension::GitHubAuth::Client->new;
-    if ($state ne $client->get_state($target)) {
-        return { failure => AUTH_ERROR, error => 'github_invalid_state' };
-    }
 
     my ($access_token, $emails);
     eval {
@@ -119,15 +99,6 @@ sub _get_login_info_from_github {
     my @emails = map { $_->{email} }
                  grep { $_->{verified} && $_->{email} !~ /\@users\.noreply\.github\.com$/ } @$emails;
 
-    my $choose_email = sub {
-        my ($email) = @_;
-        my $uri  = $target->clone;
-        my $key = Bugzilla::Extension::GitHubAuth::Client->get_email_key($email);
-        $uri->query_param(github_email => $email);
-        $uri->query_param(github_email_key => $key);
-        return $uri;
-    };
-
     my @bugzilla_users;
     my @github_emails;
     foreach my $email (@emails) {
@@ -143,7 +114,6 @@ sub _get_login_info_from_github {
 
     if (@allowed_bugzilla_users == 1) {
         my ($user) = @allowed_bugzilla_users;
-        $cgi->remove_cookie('Bugzilla_github_token');
         return { user => $user };
     }
     elsif (@allowed_bugzilla_users > 1) {
@@ -151,7 +121,7 @@ sub _get_login_info_from_github {
             template => 'account/auth/github-verify-account.html.tmpl',
             vars     => {
                 bugzilla_users => \@allowed_bugzilla_users,
-                choose_email   => $choose_email,
+                choose_email   => _mk_choose_email(\@emails),
             },
         };
         return { failure => AUTH_NODATA };
@@ -165,7 +135,7 @@ sub _get_login_info_from_github {
             template => 'account/auth/github-verify-account.html.tmpl',
             vars     => {
                 github_emails => \@github_emails,
-                choose_email  => $choose_email,
+                choose_email   => _mk_choose_email(\@emails),
             },
         };
         return { failure => AUTH_NODATA };
@@ -176,19 +146,22 @@ sub _get_login_info_from_github {
 }
 
 sub _get_login_info_from_email {
-    my ($self, $github_email, $github_email_key) = @_;
+    my ($self) = @_;
     my $cgi = Bugzilla->cgi;
+    my $email = $cgi->param('email') or return { failure => AUTH_ERROR,
+                                                 user_error => 'github_invalid_email',
+                                                 details => { email => '' } };
+    trick_taint($email);
 
-    my $key = Bugzilla::Extension::GitHubAuth::Client->get_email_key($github_email);
-    unless ($github_email_key eq $key) {
+    unless (any { $_ eq $email } @{ Bugzilla->request_cache->{github_emails} }) {
         return { failure    => AUTH_ERROR,
                  user_error => 'github_invalid_email',
-                 details    => { email => $github_email }};
+                 details    => { email => $email }};
     }
 
-    my $user = Bugzilla::User->new({name => $github_email, cache => 1});
+    my $user = Bugzilla::User->new({name => $email, cache => 1});
     $cgi->remove_cookie('Bugzilla_github_token');
-    return $user ? { user => $user } : { email => $github_email };
+    return $user ? { user => $user } : { email => $email };
 }
 
 sub fail_nodata {
@@ -206,5 +179,29 @@ sub fail_nodata {
     exit;
 }
 
+sub _store_emails {
+    my ($emails) = @_;
+    my $state = issue_short_lived_session_token("github_email");
+    set_token_extra_data($state, { type       => 'github_email',
+                                   emails     => $emails,
+                                   target_uri => Bugzilla->request_cache->{github_target_uri} });
+
+    Bugzilla->cgi->send_cookie(-name     => 'github_state',
+                               -value    => $state,
+                               -httponly => 1);
+    return $state;
+}
+
+sub _mk_choose_email {
+    my ($emails) = @_;
+    my $state = _store_emails($emails);
+
+    return sub {
+        my $email = shift;
+        my $uri = URI->new(correct_urlbase() . "github.cgi");
+        $uri->query_form( state => $state, email => $email );
+        return $uri;
+    };
+}
 
 1;
diff --git a/extensions/GitHubAuth/lib/Util.pm b/extensions/GitHubAuth/lib/Util.pm
deleted file mode 100644 (file)
index bda76a4..0000000
+++ /dev/null
@@ -1,35 +0,0 @@
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
-#
-# This Source Code Form is "Incompatible With Secondary Licenses", as
-# defined by the Mozilla Public License, v. 2.0.
-
-package Bugzilla::Extension::GitHubAuth::Util;
-
-use strict;
-use warnings;
-
-use Bugzilla::Util qw(correct_urlbase);
-use URI;
-
-use base qw(Exporter);
-our @EXPORT = qw( target_uri );
-
-
-# this is like correct_urlbase() except it returns the *requested* uri, before http url rewrites have been applied.
-# needed to generate github's redirect_uri.
-sub target_uri {
-    my $cgi = Bugzilla->cgi;
-    my $base = URI->new(correct_urlbase());
-    if (my $request_uri = $cgi->request_uri) {
-        $base->path('');
-        $request_uri =~ s!^/+!!;
-        return URI->new($base . "/" . $request_uri);
-    }
-    else {
-        return URI->new(correct_urlbase() . $cgi->url(-relative => 1, query => ));
-    }
-}
-
-1;
index 26eb8d63b103aa6d69f103b9549bbba19ee2a9e6..609b8615992296c38016911791745f621e8b67b2 100644 (file)
@@ -5,14 +5,16 @@
   # This Source Code Form is "Incompatible With Secondary Licenses", as
   # defined by the Mozilla Public License, v. 2.0.
   #%]
-
+[% USE Bugzilla %]
 [% IF Param('user_info_class').split(',').contains('GitHubAuth') %]
   <p>
-    <a href="[% github_auth.login FILTER html %]">
-      <img src="extensions/GitHubAuth/web/images/github_sign_in.png"
+    <form method="post" action="[% urlbase FILTER html %]github.cgi">
+      <input type="hidden" name="github_secret" value="[% Bugzilla.github_secret FILTER html %]">
+      <input type="hidden" name="target_uri" value="[% Bugzilla.cgi.target_uri FILTER html %]">
+      <input type="image" src="extensions/GitHubAuth/web/images/github_sign_in.png"
            alt="Sign in with GitHub"
            title="Sign in with GitHub"
            width="185" height="25">
-    </a>
+    </form>
   </p>
 [% END %]
index 6c4582b70f04a8088ef78200c08cae4decdad613..f32b34a59c24fa81e653c740025fc5c83a5ef2e4 100644 (file)
@@ -5,6 +5,7 @@
   # This Source Code Form is "Incompatible With Secondary Licenses", as
   # defined by the Mozilla Public License, v. 2.0.
   #%]
+[% USE Bugzilla %]
 
 [% IF Param('user_info_class').split(',').contains('GitHubAuth') %]
   <script type="text/javascript">
    });
   </script>
   <span id="github_mini_login[% qs_suffix FILTER html %]" class="bz_default_hidden">
-    <a href="[% github_auth.login FILTER html %]">
-      <img src="extensions/GitHubAuth/web/images/sign_in.png" height="22" width="75" align="absmiddle"
-           alt="Sign in with GitHub"
-           title="Sign in with GitHub"></a> or
+    <form method="post" action="[% urlbase FILTER html %]github.cgi">
+      <input type="hidden" name="github_secret" value="[% Bugzilla.github_secret FILTER html %]">
+      <input type="hidden" name="target_uri" value="[% Bugzilla.cgi.target_uri FILTER html %]">
+        <input type="image" src="extensions/GitHubAuth/web/images/sign_in.png" height="22" width="75" align="absmiddle"
+             alt="Sign in with GitHub"
+             title="Sign in with GitHub"></a> or
+    </form>
   </span>
 [% END %]
index 5f6672e2b7bf65d88a59be1d26ea7580d6cdba09..aaf9b6fa3ec5acc79503684668cfdfc1023c8b92 100644 (file)
@@ -6,11 +6,7 @@
   # defined by the Mozilla Public License, v. 2.0.
   #%]
 
-[% IF error == "github_invalid_state" %]
-  [% title = "Invalid State Parameter" %]
-  An invalid <em>state</em> parameter was passed to the GitHub OAuth2 callback.
-
-[% ELSIF error == "github_missing_code" %]
+[% IF error == "github_missing_code" %]
   [% title = "Missing GitHub Auth Code" %]
   Expected a <em>code</em> parameter in the GitHub OAuth2 callback.
 
   [% title = "GitHub Error" %]
   GitHub returned an error: [% response.message FILTER html %]
 
+[% ELSIF error == "github_invalid_target" %]
+  [% terms.Bugzilla %] cannot log you into an external site via GitHub.
+
+[% ELSIF error == "github_invalid_request" %]
+  Invalid GitHub log in attempt (reason: [% reason FILTER html %])
+
 [% END %]
diff --git a/github.cgi b/github.cgi
new file mode 100755 (executable)
index 0000000..e2955e2
--- /dev/null
@@ -0,0 +1,110 @@
+#!/usr/bin/perl -wT
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use 5.10.1;
+use strict;
+use lib qw(. lib);
+
+use Bugzilla;
+use Bugzilla::Util qw( correct_urlbase );
+use Bugzilla::Error;
+use Bugzilla::Constants;
+use Bugzilla::Token qw( issue_short_lived_session_token
+                        set_token_extra_data
+                        get_token_extra_data
+                        delete_token );
+use URI;
+use URI::QueryParam;
+BEGIN { Bugzilla->extensions }
+use Bugzilla::Extension::GitHubAuth::Client;
+
+my $cgi     = Bugzilla->cgi;
+my $urlbase = correct_urlbase();
+
+if (lc($cgi->request_method) eq 'post') {
+    # POST requests come from Bugzilla itself and begin the GitHub login process
+    # by redirecting the user to GitHub's authentication endpoint.
+
+    my $user           = Bugzilla->login(LOGIN_OPTIONAL);
+    my $target_uri     = $cgi->param('target_uri')    or ThrowCodeError("github_invalid_target");
+    my $github_secret  = $cgi->param('github_secret') or ThrowCodeError("github_invalid_request", { reason => 'invalid secret' });
+    my $github_secret2 = Bugzilla->github_secret      or ThrowCodeError("github_invalid_request", { reason => 'invalid secret' });
+
+    ThrowCodeError("github_invalid_request", { reason => 'invalid secret' })
+      unless $github_secret eq $github_secret2;
+
+    ThrowCodeError("github_invalid_target", { target_uri => $target_uri })
+      unless $target_uri =~ /^\Q$urlbase\E/;
+
+    if ($user->id) {
+        print $cgi->redirect($target_uri);
+        exit;
+    }
+
+    my $state = issue_short_lived_session_token("github_state");
+    set_token_extra_data($state, { type => 'github_login', target_uri => $target_uri });
+
+    $cgi->send_cookie(-name     => 'github_state',
+                      -value    => $state,
+                      -httponly => 1);
+    print $cgi->redirect(Bugzilla::Extension::GitHubAuth::Client->authorize_uri($state));
+}
+elsif (lc($cgi->request_method) eq 'get') {
+    # GET requests come from GitHub, with this script acting as the OAuth2 callback.
+    my $state_param  = $cgi->param('state');
+    my $state_cookie = $cgi->cookie('github_state');
+
+    # If the state or params are missing, or the github_state cookie is missing
+    # we just redirect to index.cgi.
+    unless ($state_param && $state_cookie && $cgi->param('code')) {
+        print $cgi->redirect($urlbase . "index.cgi");
+        exit;
+    }
+
+    ThrowCodeError("github_invalid_request", { reason => 'invalid state param' })
+      unless $state_param eq $state_cookie;
+
+    my $state_data = get_token_extra_data($state_param);
+    ThrowCodeError("github_invalid_request", { reason => 'invalid state param' } )
+      unless $state_data && $state_data->{type};
+
+
+    $cgi->remove_cookie('github_state');
+    delete_token($state_param);
+
+    if ($state_data->{type} eq 'github_login') {
+        Bugzilla->request_cache->{github_action}     = 'login';
+        Bugzilla->request_cache->{github_target_uri} = $state_data->{target_uri};
+    }
+    elsif ($state_data->{type} eq 'github_email') {
+        Bugzilla->request_cache->{github_action} = 'email';
+        Bugzilla->request_cache->{github_emails} = $state_data->{emails};
+    }
+    else {
+        ThrowCodeError("github_invalid_request", { reason => "invalid state param" })
+    }
+    my $user = Bugzilla->login(LOGIN_REQUIRED);
+
+    my $target_uri = URI->new($state_data->{target_uri});
+
+    # It makes very little sense to login to a page with the logout parameter.
+    # doing so would be a no-op, so we ignore the logout param here.
+    $target_uri->query_param_delete('logout');
+
+    if ($target_uri->path =~ /attachment\.cgi/) {
+        my $attachment_uri = URI->new($urlbase . "attachment.cgi");
+        $attachment_uri->query_param(id => scalar $target_uri->query_param('id'));
+        if ($target_uri->query_param('action')) {
+            $attachment_uri->query_param(action => scalar $target_uri->query_param('action'));
+        }
+        print $cgi->redirect($attachment_uri);
+    }
+    else {
+        print $cgi->redirect($target_uri);
+    }
+}