diff --git a/lib/RT.pm b/lib/RT.pm
index ed23952..0eefc3f 100644
--- a/lib/RT.pm
+++ b/lib/RT.pm
@@ -74,6 +74,10 @@ use vars qw($BasePath
  $MasonDataDir
  $MasonSessionDir);
 
+# Set Email::Address module var before anything else loads.
+# This avoids an algorithmic complexity denial of service vulnerability.
+# See T#157608 and CVE-2015-7686 for more information.
+$Email::Address::COMMENT_NEST_LEVEL = 1;
 
 RT->LoadGeneratedData();
 
diff --git a/lib/RT/Config.pm b/lib/RT/Config.pm
index ba338bb..2ca2516 100644
--- a/lib/RT/Config.pm
+++ b/lib/RT/Config.pm
@@ -135,6 +135,14 @@ can be set for each config optin:
 
 our %META = (
     # General user overridable options
+    RestrictReferrerLogin => {
+        PostLoadCheck => sub {
+            my $self = shift;
+            if (defined($self->Get('RestrictReferrerLogin'))) {
+                RT::Logger->error("The config option 'RestrictReferrerLogin' is incorrect, and should be 'RestrictLoginReferrer' instead.");
+            }
+        },
+    },
     DefaultQueue => {
         Section         => 'General',
         Overridable     => 1,
diff --git a/lib/RT/Interface/Web.pm b/lib/RT/Interface/Web.pm
index 88e1df1..e6f9bce 100644
--- a/lib/RT/Interface/Web.pm
+++ b/lib/RT/Interface/Web.pm
@@ -1256,7 +1256,7 @@ sub IsCompCSRFWhitelisted {
     # golden.  This acts on the presumption that external forms may
     # hardcode a username and password -- if a malicious attacker knew
     # both already, CSRF is the least of your problems.
-    my $AllowLoginCSRF = not RT->Config->Get('RestrictReferrerLogin');
+    my $AllowLoginCSRF = not RT->Config->Get('RestrictLoginReferrer');
     if ($AllowLoginCSRF and defined($args{user}) and defined($args{pass})) {
         my $user_obj = RT::CurrentUser->new();
         $user_obj->Load($args{user});
@@ -1458,7 +1458,7 @@ sub MaybeShowInterstitialCSRFPage {
     my $token = StoreRequestToken($ARGS);
     $HTML::Mason::Commands::m->comp(
         '/Elements/CSRF',
-        OriginalURL => RT->Config->Get('WebPath') . $HTML::Mason::Commands::r->path_info,
+        OriginalURL => RT->Config->Get('WebBaseURL') . RT->Config->Get('WebPath') . $HTML::Mason::Commands::r->path_info,
         Reason => HTML::Mason::Commands::loc( $msg, @loc ),
         Token => $token,
     );
diff --git a/lib/RT/SavedSearch.pm b/lib/RT/SavedSearch.pm
index f7695d6..18f263b 100644
--- a/lib/RT/SavedSearch.pm
+++ b/lib/RT/SavedSearch.pm
@@ -115,6 +115,28 @@ sub UpdateAttribute {
     return ($status, $msg);
 }
 
+=head2 RT::SavedSearch->EscapeDescription STRING
+
+This is a class method because system-level saved searches aren't true
+C<RT::SavedSearch> objects but direct C<RT::Attribute> objects.
+
+Returns C<STRING> with all square brackets except those in C<[_1]> escaped,
+ready for passing as the first argument to C<loc()>.
+
+=cut
+
+sub EscapeDescription {
+    my $self = shift;
+    my $desc = shift;
+    if ($desc) {
+        # We only use [_1] in saved search descriptions, so let's escape other "["
+        # and "]" unless they are escaped already.
+        $desc =~ s/(?<!~)\[(?!_1\])/~[/g;
+        $desc =~ s/(?<!~)(?<!\[_1)\]/~]/g;
+    }
+    return $desc;
+}
+
 =head2 Type
 
 Returns the type of this search, e.g. 'Ticket'.  Useful for denoting the
diff --git a/lib/RT/User.pm b/lib/RT/User.pm
index 72d00f3..c38b3b3 100644
--- a/lib/RT/User.pm
+++ b/lib/RT/User.pm
@@ -83,6 +83,7 @@ use RT::ACE;
 use RT::Interface::Email;
 use Encode;
 use Text::Password::Pronounceable;
+use RT::Util;
 
 sub _OverlayAccessible {
     {
@@ -912,7 +913,10 @@ sub IsPassword {
         # If it's a new-style (>= RT 4.0) password, it starts with a '!'
         my (undef, $method, $salt, undef) = split /!/, $stored;
         if ($method eq "sha512") {
-            return $self->_GeneratePassword_sha512($value, $salt) eq $stored;
+            return 0 unless RT::Util::constant_time_eq(
+                $self->_GeneratePassword_sha512($value, $salt),
+                $stored
+            );
         } else {
             $RT::Logger->warn("Unknown hash method $method");
             return 0;
@@ -922,16 +926,28 @@ sub IsPassword {
         my $hash = MIME::Base64::decode_base64($stored);
         # Decoding yields 30 byes; first 4 are the salt, the rest are substr(SHA256,0,26)
         my $salt = substr($hash, 0, 4, "");
-        return 0 unless substr(Digest::SHA::sha256($salt . Digest::MD5::md5($value)), 0, 26) eq $hash;
+        return 0 unless RT::Util::constant_time_eq(
+            substr(Digest::SHA::sha256($salt . Digest::MD5::md5($value)), 0, 26),
+            $hash
+        );
     } elsif (length $stored == 32) {
         # Hex nonsalted-md5
-        return 0 unless Digest::MD5::md5_hex(encode_utf8($value)) eq $stored;
+        return 0 unless RT::Util::constant_time_eq(
+            Digest::MD5::md5_hex(encode_utf8($value)),
+            $stored
+        );
     } elsif (length $stored == 22) {
         # Base64 nonsalted-md5
-        return 0 unless Digest::MD5::md5_base64(encode_utf8($value)) eq $stored;
+        return 0 unless RT::Util::constant_time_eq(
+            Digest::MD5::md5_base64(encode_utf8($value)),
+            $stored
+        );
     } elsif (length $stored == 13) {
         # crypt() output
-        return 0 unless crypt(encode_utf8($value), $stored) eq $stored;
+        return 0 unless RT::Util::constant_time_eq(
+            crypt(encode_utf8($value), $stored),
+            $stored
+        );
     } else {
         $RT::Logger->warning("Unknown password form");
         return 0;
@@ -1028,20 +1044,21 @@ sub GenerateAuthString {
 
 =head3 ValidateAuthString
 
-Takes auth string and protected string. Returns true is protected string
+Takes auth string and protected string. Returns true if protected string
 has been protected by user's L</AuthToken>. See also L</GenerateAuthString>.
 
 =cut
 
 sub ValidateAuthString {
     my $self = shift;
-    my $auth_string = shift;
+    my $auth_string_to_validate = shift;
     my $protected = shift;
 
     my $str = $self->AuthToken . $protected;
     utf8::encode( $str );
+    my $valid_auth_string = substr(Digest::MD5::md5_hex($str),0,16);
 
-    return $auth_string eq substr(Digest::MD5::md5_hex($str),0,16);
+    return RT::Util::constant_time_eq( $auth_string_to_validate, $valid_auth_string );
 }
 
 =head2 SetDisabled
diff --git a/lib/RT/Util.pm b/lib/RT/Util.pm
index 24efe71..fe4eebb 100644
--- a/lib/RT/Util.pm
+++ b/lib/RT/Util.pm
@@ -54,6 +54,8 @@ use warnings;
 use base 'Exporter';
 our @EXPORT = qw/safe_run_child mime_recommended_filename/;
 
+use Encode qw/encode/;
+
 sub safe_run_child (&) {
     my $our_pid = $$;
 
@@ -130,6 +132,58 @@ sub mime_recommended_filename {
     return;
 }
 
+
+=head2 C<constant_time_eq($a, $b)>
+
+Compares two strings for equality in constant-time. Replacement for the C<eq>
+operator designed to avoid timing side-channel vulnerabilities. Returns zero
+or one.
+
+This is intended for use in cryptographic subsystems for comparing well-formed
+data such as hashes - not for direct use with user input or as a general
+replacement for the C<eq> operator.
+
+The two string arguments B<MUST> be of equal length. If the lengths differ,
+this function will call C<die()>, as proceeding with execution would create
+a timing vulnerability. Length is defined by characters, not bytes.
+
+This code has been tested to do what it claims. Do not change it without
+thorough statistical timing analysis to validate the changes.
+
+Added to resolve CVE-2017-5361
+
+For more on timing attacks, see this Wikipedia article:
+B<https://en.wikipedia.org/wiki/Timing_attack>
+
+=cut
+
+sub constant_time_eq {
+    my ($a, $b) = @_;
+
+    my $result = 0;
+
+    # generic error message avoids potential information leaks
+    my $generic_error = "Cannot compare values";
+    die $generic_error unless defined $a and defined $b;
+    die $generic_error unless length $a == length $b;
+    die $generic_error if ref($a) or ref($b);
+
+    for (my $i = 0; $i < length($a); $i++) {
+        my $a_char = substr($a, $i, 1);
+        my $b_char = substr($b, $i, 1);
+
+        # encode() is set to die on malformed
+        my @a_octets = unpack("C*", encode('UTF-8', $a_char, Encode::FB_CROAK));
+        my @b_octets = unpack("C*", encode('UTF-8', $b_char, Encode::FB_CROAK));
+        die $generic_error if (scalar @a_octets) != (scalar @b_octets);
+
+        for (my $j = 0; $j < scalar @a_octets; $j++) {
+            $result |= $a_octets[$j] ^ $b_octets[$j];
+        }
+    }
+    return 0 + not $result;
+}
+
 RT::Base->_ImportOverlays();
 
 1;
diff --git a/share/html/Dashboards/Subscription.html b/share/html/Dashboards/Subscription.html
index 3a57102..13a3bc0 100644
--- a/share/html/Dashboards/Subscription.html
+++ b/share/html/Dashboards/Subscription.html
@@ -75,7 +75,7 @@
 <ol class="dashboard-queries">
 %    for my $portlet (@portlets) {
         <li class="dashboard-query">
-            <% loc($portlet->{description}, $fields{'Rows'}) %>
+            <% loc( RT::SavedSearch->EscapeDescription($portlet->{description}), $fields{'Rows'}) %>
         </li>
 %    }
 </ol>
diff --git a/share/html/Ticket/Attachment/dhandler b/share/html/Ticket/Attachment/dhandler
index 8e9b4a2..a3417c7 100755
--- a/share/html/Ticket/Attachment/dhandler
+++ b/share/html/Ticket/Attachment/dhandler
@@ -69,11 +69,13 @@
 
      my $content_type = $AttachmentObj->ContentType || 'text/plain';
 
-     if (RT->Config->Get('AlwaysDownloadAttachments')) {
+     my $attachment_regex = qr{^(image/svg\+xml|application/pdf)}i;
+     if ( RT->Config->Get('AlwaysDownloadAttachments') || ($content_type =~ $attachment_regex) ) {
          $r->headers_out->{'Content-Disposition'} = "attachment";
      }
      elsif (!RT->Config->Get('TrustHTMLAttachments')) {
-         $content_type = 'text/plain' if ($content_type =~ /^text\/html/i);
+         my $text_plain_regex = qr{^(text/html|application/xhtml\+xml|text/xml|application/xml)}i;
+         $content_type = 'text/plain' if ( $content_type =~ $text_plain_regex );
      }
 
      my $enc = $AttachmentObj->OriginalEncoding || 'utf-8';
