Skip to content

Commit 5185a1a

Browse files
authored
Merge pull request #85 from waterkip/GH-redirect-verify_fixes
BREAKING CHANGE: Verify the SAMLResponse based on the raw query string
2 parents 6f2f3d1 + f43727d commit 5185a1a

File tree

4 files changed

+45
-74
lines changed

4 files changed

+45
-74
lines changed

Makefile.PL

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ my %WriteMakefileArgs = (
4545
"Types::Serialiser" => 0,
4646
"URI" => 0,
4747
"URI::Encode" => 0,
48+
"URI::Escape" => 0,
4849
"URI::QueryParam" => 0,
4950
"URN::OASIS::SAML2" => "0.002",
5051
"XML::Enc" => "0.05",
@@ -124,6 +125,7 @@ my %FallbackPrereqs = (
124125
"Types::Serialiser" => 0,
125126
"URI" => 0,
126127
"URI::Encode" => 0,
128+
"URI::Escape" => 0,
127129
"URI::QueryParam" => 0,
128130
"URI::URL" => 0,
129131
"URN::OASIS::SAML2" => "0.002",

cpanfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ requires "MooseX::Types::URI" => "0";
2929
requires "Types::Serialiser" => "0";
3030
requires "URI" => "0";
3131
requires "URI::Encode" => "0";
32+
requires "URI::Escape" => "0";
3233
requires "URI::QueryParam" => "0";
3334
requires "URN::OASIS::SAML2" => "0.002";
3435
requires "XML::Enc" => "0.05";

lib/Net/SAML2/Binding/Redirect.pm

Lines changed: 39 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,24 @@
1-
use strict;
2-
use warnings;
31
package Net::SAML2::Binding::Redirect;
2+
use Moose;
3+
44
# VERSION
55

6-
use Moose;
6+
use Carp qw(croak);
7+
use Crypt::OpenSSL::RSA;
8+
use Crypt::OpenSSL::X509;
9+
use File::Slurper qw/ read_text /;
10+
use IO::Compress::RawDeflate qw/ rawdeflate /;
11+
use IO::Uncompress::RawInflate qw/ rawinflate /;
12+
use MIME::Base64 qw/ encode_base64 decode_base64 /;
713
use MooseX::Types::URI qw/ Uri /;
814
use Net::SAML2::Types qw(signingAlgorithm SAMLRequestType);
9-
use Carp qw(croak);
15+
use URI::Encode qw/uri_decode/;
16+
use URI::Escape qw(uri_unescape);
17+
use URI::QueryParam;
18+
use URI;
1019

1120
# ABSTRACT: Net::SAML2::Binding::Redirect - HTTP Redirect binding for SAML
1221

13-
=head1 NAME
14-
15-
Net::SAML2::Binding::Redirect
16-
1722
=head1 SYNOPSIS
1823
1924
my $redirect = Net::SAML2::Binding::Redirect->new(
@@ -32,16 +37,6 @@ Net::SAML2::Binding::Redirect
3237
3338
=cut
3439

35-
use MIME::Base64 qw/ encode_base64 decode_base64 /;
36-
use IO::Compress::RawDeflate qw/ rawdeflate /;
37-
use IO::Uncompress::RawInflate qw/ rawinflate /;
38-
use URI;
39-
use URI::QueryParam;
40-
use Crypt::OpenSSL::RSA;
41-
use Crypt::OpenSSL::X509;
42-
use File::Slurper qw/ read_text /;
43-
use URI::Encode qw/uri_decode/;
44-
4540
=head2 new( ... )
4641
4742
Constructor. Creates an instance of the Redirect binding.
@@ -220,7 +215,7 @@ sub sign {
220215
return $u->as_string;
221216
}
222217

223-
sub _verified {
218+
sub _verify {
224219
my ($self, $sigalg, $signed, $sig) = @_;
225220

226221
foreach my $crt (@{$self->cert}) {
@@ -237,84 +232,60 @@ sub _verified {
237232
$rsa_pub->use_sha512_hash;
238233
} elsif ($sigalg eq 'http://www.w3.org/2000/09/xmldsig#rsa-sha1') {
239234
$rsa_pub->use_sha1_hash;
240-
} else {
241-
warn "Unsupported Signature Algorithim: $sigalg" if ($self->debug);
242235
}
243-
244-
if ($rsa_pub->verify($signed, $sig)) {
245-
return 1;
236+
else {
237+
warn "Unsupported Signature Algorithim: $sigalg, defaulting to sha256" if $self->debug;
246238
}
247239

248-
warn "Unable to verify with " . $cert->subject if ($self->debug);
240+
return 1 if $rsa_pub->verify($signed, $sig);
241+
242+
warn "Unable to verify with " . $cert->subject if $self->debug;
249243
}
250244

251-
die "bad sig";
245+
croak("Unable to verify the XML signature");
252246
}
253247

254-
=head2 verify( $url )
248+
=head2 verify( $query_string )
255249
256250
Decode a Redirect binding URL.
257251
258252
Verifies the signature on the response.
259253
254+
Requires the *raw* query string to be passed, because L<URI> parses and
255+
re-encodes URI-escapes in uppercase (C<%3f> becomes C<%3F>, for instance),
256+
which leads to signature verification failures if the other party uses lower
257+
case (or mixed case).
258+
260259
=cut
261260

262261
sub verify {
263262
my ($self, $url) = @_;
264-
my $u = URI->new($url);
265263

266-
# verify the response
267-
my $sigalg = $u->query_param('SigAlg');
264+
# This now becomes the query string
265+
$url =~ s#^https?://.+\?##;
268266

269-
my $signed;
270-
my $saml_request;
271-
my $sig = $u->query_param_delete('Signature');
267+
my %params = map { split(/=/, $_, 2) } split(/&/, $url);
272268

273-
# During the verify the only query parameters that should be in the query are
274-
# 'SAMLRequest', 'RelayState', 'Sig', 'SigAlg' the other parameter values are
275-
# deleted from the URI query that was created from the URL that was passed
276-
# to the verify function
277-
my @signed_params = ('SAMLRequest', 'SAMLResponse', 'RelayState', 'Sig', 'SigAlg');
278-
279-
for my $key ($u->query_param) {
280-
if (grep /$key/, @signed_params ) {
281-
next;
282-
}
283-
$u->query_param_delete($key);
284-
}
269+
my $sigalg = uri_unescape($params{SigAlg});
285270

286-
# Some IdPs (PingIdentity) seem to double encode the LogoutResponse URL
287-
if ($self->sls_double_encoded_response) {
288-
#if ($sigalg =~ m/%/) {
289-
$signed = uri_decode($u->query);
290-
$sig = uri_decode($sig);
291-
$sigalg = uri_decode($sigalg);
292-
$saml_request = uri_decode($u->query_param($self->param));
293-
} else {
294-
$signed = $u->query;
295-
$saml_request = $u->query_param($self->param);
296-
}
271+
my $encoded_sig = uri_unescape($params{Signature});
272+
my $sig = decode_base64($encoded_sig);
297273

298-
# What can we say about this one Microsoft Azure uses lower case in the
299-
# URL encoding %2f not %2F. As it is signed as %2f the resulting signed
300-
# needs to change it to lowercase if the application layer reencoded the URL.
301-
if ($self->sls_force_lcase_url_encoding) {
302-
# TODO: This is a hack.
303-
$signed =~ s/(%..)/lc($1)/ge;
274+
my @signed_parts;
275+
for my $p ($self->param, qw(RelayState SigAlg)) {
276+
push @signed_parts, join('=', $p, $params{$p}) if exists $params{$p};
304277
}
278+
my $signed = join('&', @signed_parts);
305279

306-
$sig = decode_base64($sig);
307-
308-
$self->_verified($sigalg, $signed, $sig);
280+
$self->_verify($sigalg, $signed, $sig);
309281

310282
# unpack the SAML request
311-
my $deflated = decode_base64($saml_request);
283+
my $deflated = decode_base64(uri_unescape($params{$self->param}));
312284
my $request = '';
313285
rawinflate \$deflated => \$request;
314286

315287
# unpack the relaystate
316-
my $relaystate = $u->query_param('RelayState');
317-
288+
my $relaystate = uri_unescape($params{'RelayState'});
318289
return ($request, $relaystate);
319290
}
320291

t/17-lowercase-url-escaping.t

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
use Test::Lib;
22
use Test::Net::SAML2;
3-
use Test::More tests => 2;
43

54
use Net::SAML2::Binding::Redirect;
65
use File::Slurper qw/read_text/;
@@ -15,13 +14,11 @@ my $redirect = Net::SAML2::Binding::Redirect->new(
1514
param => 'SAMLRequest',
1615
cert => read_text('t/net-saml2-cert.pem'),
1716
sig_hash => 'sha256',
18-
sls_force_lcase_url_encoding => 1,
1917
);
2018

2119
my ($request, $relaystate) = $redirect->verify($url);
2220

23-
like($request, qr/NETSAML/, 'Good Signature if sls_force_lcase_url_encoding = 1');
21+
like($request, qr/NETSAML/,
22+
"Good Signature because we now don't alter the input with URI anymore");
2423

25-
$redirect->{sls_force_lcase_url_encoding} = 0;
26-
27-
throws_ok( sub { $redirect->verify($url) }, qr/bad sig/, "Bad Signature if sls_force_lcase_url_encoding = 0");
24+
done_testing();

0 commit comments

Comments
 (0)