Commit 0ac63904 authored by Xavier Guimard's avatar Xavier Guimard

* New parameter for XSS protection : trustedDomains

* parameters test to avoid warnings
* debian/control : missing dependencies
* perltidy
* tests update
parent 455c7c65
......@@ -4,7 +4,7 @@ Priority: extra
Maintainer: Xavier Guimard <x.guimard@free.fr>
DM-Upload-Allowed: yes
Build-Depends: debhelper (>= 4.1.16), po-debconf
Build-Depends-Indep:libapache-session-perl, libnet-ldap-perl, libdbi-perl, libwww-perl, libcache-cache-perl, libxml-simple-perl, libcgi-session-perl, libcrypt-rijndael-perl, libxml-libxslt-perl, libio-string-perl, libregexp-assemble-perl, liburi-perl, libstring-random-perl
Build-Depends-Indep:libapache-session-perl, libnet-ldap-perl, libdbi-perl, libwww-perl, libcache-cache-perl, libxml-simple-perl, libcgi-session-perl, libcrypt-rijndael-perl, libxml-libxslt-perl, libio-string-perl, libregexp-assemble-perl, liburi-perl, libstring-random-perl, libmime-lite-perl
Standards-Version: 3.8.0
Package: lemonldap-ng
......@@ -61,6 +61,7 @@ Description: Lemonldap::NG manager part
Package: liblemonldap-ng-portal-perl
Architecture: all
Depends: ${misc:Depends}, libapache-session-perl, libnet-ldap-perl, liblemonldap-ng-conf-perl (= ${binary:Version}), libhtml-template-perl, libjs-jquery, liblemonldap-ng-handler-perl (= ${binary:Version}), libxml-libxml-perl, libxml-libxslt-perl, libstring-random-perl
Recommends: libcgi-session-perl, libmime-lite-perl
Suggests: liblasso-perl, libcgi-session-perl, slapd
Description: Lemonldap::NG authentication portal part
Lemonldap::NG is a complete Web-SSO system that can run with reverse-proxies
......
......@@ -473,7 +473,7 @@ sub locationRulesInit {
$locationCount = 0;
# Pre compilation : both regexp and conditions
foreach ( keys sort %{ $args->{locationRules} } ) {
foreach ( sort keys %{ $args->{locationRules} } ) {
if ( $_ eq 'default' ) {
$defaultCondition =
$class->conditionSub( $args->{locationRules}->{$_} );
......
......@@ -131,18 +131,20 @@ sub new {
( $ENV{REQUEST_METHOD} eq 'POST' and not $self->param('newpassword') )
or $self->param('logout')
) ? 1 : 0;
$self->{SMTPServer} ||= 'localhost';
$self->{mailLDAPFilter} ||= '(&(mail=$mail)(objectClass=inetOrgPerson))';
$self->{SMTPServer} ||= 'localhost';
$self->{mailLDAPFilter} ||= '(&(mail=$mail)(objectClass=inetOrgPerson))';
$self->{randomPasswordRegexp} ||= '[A-Z]{3}[a-z]{5}.\d{2}';
$self->{mailFrom} ||= "noreply@".$domain;
$self->{mailFrom} ||= "noreply@" . $domain;
$self->{mailSubject} ||= "Change password request";
$self->{mailBody} ||= 'Your new password is $password';
# Authentication and userDB module are required and have to be in @ISA
foreach (qw(authentication userDB passwordDB)) {
my $tmp =
'Lemonldap::NG::Portal::'
. ( $_ eq 'userDB' ? 'UserDB' : ( $_ eq 'passwordDB' ? 'PasswordDB' : 'Auth' ) )
'Lemonldap::NG::Portal::'
. ( $_ eq 'userDB'
? 'UserDB'
: ( $_ eq 'passwordDB' ? 'PasswordDB' : 'Auth' ) )
. $self->{$_};
$tmp =~ s/\s.*$//;
eval "require $tmp";
......@@ -196,6 +198,13 @@ sub new {
push @ISA, 'Lemonldap::NG::Portal::_SOAP';
$self->startSoapServices();
}
unless ( defined( $self->{trustedDomains} ) ) {
$self->{trustedDomains} = $self->{domain};
}
if ( $self->{trustedDomains} ) {
$self->{trustedDomains} = '|(?:[^/]*)?' . join '|',
map { s/\./\\\./g; $_ } split /\s+/, $self->{trustedDomains};
}
return $self;
}
......@@ -447,8 +456,11 @@ sub get_url {
sub get_user {
my $self = shift;
return "" unless $self->{user};
return $self->{user} unless ( $self->{user} =~ m/(?:\0|<|'|"|`|\%(?:00|25|3C|22|27|2C))/ );
$self->lmLog("XSS attack detected (param: user | value: ".$self->{user}.")", "warn");
return $self->{user}
unless ( $self->{user} =~ m/(?:\0|<|'|"|`|\%(?:00|25|3C|22|27|2C))/ );
$self->lmLog(
"XSS attack detected (param: user | value: " . $self->{user} . ")",
"warn" );
return "";
}
......@@ -538,7 +550,7 @@ sub process {
$self->{error} = $self->_subProcess(
qw(controlUrlOrigin checkNotifBack controlExistingSession
SAMLForUnAuthUser authInit extractFormInfo userDBInit getUser
setAuthSessionInfo passwordDBInit modifyPassword setSessionInfo
setAuthSessionInfo passwordDBInit modifyPassword setSessionInfo
resetPasswordByMail setMacros setLocalGroups setGroups authenticate
store buildCookie checkNotification SAMLForAuthUser autoRedirect)
);
......@@ -556,7 +568,8 @@ sub controlUrlOrigin {
# REJECT NON BASE64 URL
if ( $url =~ m#[^A-Za-z0-9\+/=]# ) {
$self->lmLog("XSS attack detected (param: url | value: $url)", "warn");
$self->lmLog( "XSS attack detected (param: url | value: $url)",
"warn" );
return PE_BADURL;
}
......@@ -567,11 +580,15 @@ sub controlUrlOrigin {
if (
$self->{urldc} =~ /(?:\0|<|'|"|`|\%(?:00|25|3C|22|27|2C))/
or ( $self->{urldc} !~
m#^https?://(?:$self->{reVHosts}|(?:[^/]*)?$self->{domain})(?::\d+)?(?:/.*)?$#
m#^https?://(?:$self->{reVHosts}$self->{trustedDomains})(?::\d+)?(?:/.*)?$#o
and not $self->param('logout') )
)
{
$self->lmLog("XSS attack detected (param: urldc | value: ".$self->{urldc}.")", "warn");
$self->lmLog(
"XSS attack detected (param: urldc | value: "
. $self->{urldc} . ")",
"warn"
);
delete $self->{urldc};
return PE_BADURL;
}
......@@ -640,7 +657,7 @@ sub controlExistingSession {
# Delete session in global storage
$self->_deleteSession($h);
$self->{error} = PE_REDIRECT;
$self->SAMLLogout() if($self->{SAMLIssuer});
$self->SAMLLogout() if ( $self->{SAMLIssuer} );
$self->_sub( 'userNotice',
$self->{sessionInfo}->{ $self->{whatToTrace} }
. " has been disconnected" );
......@@ -703,15 +720,26 @@ sub setSessionInfo {
# Store IP address and start time
$self->{sessionInfo}->{ipAddr} = $ENV{REMOTE_ADDR};
# Extract client IP from X-FORWARDED-FOR header
my $xheader = $ENV{HTTP_X_FORWARDED_FOR};
$xheader =~ s/(.*?)(\,)+.*/$1/ if $xheader;
$self->{sessionInfo}->{xForwardedForAddr} = $xheader || $ENV{REMOTE_ADDR};
$self->{sessionInfo}->{startTime} =
&POSIX::strftime( "%Y%m%d%H%M%S", localtime() );
$self->lmLog("Store ipAddr: ".$self->{sessionInfo}->{ipAddr}." in session",'debug');
$self->lmLog("Store xForwardedForAddr: ".$self->{sessionInfo}->{xForwardedForAddr}." in session",'debug');
$self->lmLog("Store startTime: ".$self->{sessionInfo}->{startTime}." in session",'debug');
$self->lmLog(
"Store ipAddr: " . $self->{sessionInfo}->{ipAddr} . " in session",
'debug' );
$self->lmLog(
"Store xForwardedForAddr: "
. $self->{sessionInfo}->{xForwardedForAddr}
. " in session",
'debug'
);
$self->lmLog(
"Store startTime: " . $self->{sessionInfo}->{startTime} . " in session",
'debug'
);
return $self->SUPER::setSessionInfo();
}
......
......@@ -31,15 +31,17 @@ sub getUser {
# @return Lemonldap::NG::Portal constant
sub formateFilter {
my $self = shift;
$self->{LDAPFilter} = $self->{mail} ?
$self->{mailLDAPFilter} :
$self->{AuthLDAPFilter}
$self->{LDAPFilter} =
$self->{mail}
? $self->{mailLDAPFilter}
: $self->{AuthLDAPFilter}
|| $self->{LDAPFilter};
$self->lmLog( "LDAP submitted filter: ".$self->{LDAPFilter}, 'debug' );
$self->lmLog( "LDAP submitted filter: " . $self->{LDAPFilter}, 'debug' )
if ( $self->{LDAPFilter} );
$self->{LDAPFilter} ||= '(&(uid=$user)(objectClass=inetOrgPerson))';
$self->{LDAPFilter} =~ s/\$(user|_?password|mail)/$self->{$1}/g;
$self->{LDAPFilter} =~ s/\$(\w+)/$self->{sessionInfo}->{$1}/g;
$self->lmLog( "LDAP transformed filter: ".$self->{LDAPFilter}, 'debug' );
$self->lmLog( "LDAP transformed filter: " . $self->{LDAPFilter}, 'debug' );
PE_OK;
}
......@@ -56,14 +58,20 @@ sub search {
scope => 'sub',
filter => $self->{LDAPFilter},
);
$self->lmLog( "LDAP Search with base: ".$self->{ldapBase}." and filter: ".$self->{LDAPFilter}, 'debug' );
$self->lmLog(
"LDAP Search with base: "
. $self->{ldapBase}
. " and filter: "
. $self->{LDAPFilter},
'debug'
);
if ( $mesg->code() != 0 ) {
$self->lmLog( "LDAP Search error: ".$mesg->error, 'error' );
$self->lmLog( "LDAP Search error: " . $mesg->error, 'error' );
return PE_LDAPERROR;
}
unless ( $self->{entry} = $mesg->entry(0) ) {
$user = $self->{mail} || $self->{user};
$self->_sub('userError',"$user was not found in LDAP directory");
$self->_sub( 'userError', "$user was not found in LDAP directory" );
return PE_BADCREDENTIALS;
}
$self->{dn} = $self->{entry}->dn();
......@@ -110,37 +118,48 @@ sub setGroups {
my ($self) = @_;
my $groups = $self->{sessionInfo}->{groups};
$self->{ldapGroupObjectClass} ||= "groupOfNames";
$self->{ldapGroupAttributeName} ||= "member";
$self->{ldapGroupAttributeNameUser} ||= "dn";
$self->{ldapGroupAttributeNameSearch} ||= ["cn"];
$self->{ldapGroupObjectClass} ||= "groupOfNames";
$self->{ldapGroupAttributeName} ||= "member";
$self->{ldapGroupAttributeNameUser} ||= "dn";
$self->{ldapGroupAttributeNameSearch} ||= ["cn"];
if ( $self->{ldapGroupBase} && $self->{sessionInfo}->{$self->{ldapGroupAttributeNameUser}} )
if ( $self->{ldapGroupBase}
&& $self->{sessionInfo}->{ $self->{ldapGroupAttributeNameUser} } )
{
my $searchFilter = "(&(objectClass=" . $self->{ldapGroupObjectClass} . ")(|";
foreach ( split( /[,;]/, $self->{sessionInfo}->{$self->{ldapGroupAttributeNameUser}} ) )
my $searchFilter =
"(&(objectClass=" . $self->{ldapGroupObjectClass} . ")(|";
foreach (
split(
/[,;]/,
$self->{sessionInfo}->{ $self->{ldapGroupAttributeNameUser} }
)
)
{
$searchFilter .= "(" . $self->{ldapGroupAttributeName} . "=" . $_ . ")";
$searchFilter .=
"(" . $self->{ldapGroupAttributeName} . "=" . $_ . ")";
}
$searchFilter .= "))";
my $mesg = $self->{ldap}->search(
base => $self->{ldapGroupBase},
filter => $searchFilter,
attrs => $self->{ldapGroupAttributeNameSearch},
);
if ( $mesg->code() == 0 )
{
foreach my $entry ( $mesg->all_entries )
{
my $nbAttrs = @{$self->{ldapGroupAttributeNameSearch}};
for (my $i = 0; $i < $nbAttrs; $i++)
{
my @data = $entry->get_value($self->{ldapGroupAttributeNameSearch}[$i]);
if (@data)
{
base => $self->{ldapGroupBase},
filter => $searchFilter,
attrs => $self->{ldapGroupAttributeNameSearch},
);
if ( $mesg->code() == 0 ) {
foreach my $entry ( $mesg->all_entries ) {
my $nbAttrs = @{ $self->{ldapGroupAttributeNameSearch} };
for ( my $i = 0 ; $i < $nbAttrs ; $i++ ) {
my @data =
$entry->get_value(
$self->{ldapGroupAttributeNameSearch}[$i] );
if (@data) {
$groups .= $data[0];
$groups .= "|"
if ($i+1 < $nbAttrs && $entry->get_value($self->{ldapGroupAttributeNameSearch}[$i+1]));
if (
$i + 1 < $nbAttrs
&& $entry->get_value(
$self->{ldapGroupAttributeNameSearch}[ $i + 1 ]
)
);
}
}
$groups .= "; ";
......
......@@ -25,11 +25,28 @@ sub extractFormInfo {
return PE_FIRSTACCESS
unless ( $self->param('user') || $self->param('mail') );
return PE_FORMEMPTY
unless ( ( ( length( $self->{'user'} = $self->param('user') ) > 0 )
&& ( ( length( $self->{'password'} = $self->param('password') ) > 0 )
|| ( length( $self->{'newpassword'} = $self->param('newpassword') ) > 0 ) ) )
|| ( length( $self->{'mail'} = $self->param('mail') ) > 0 ) );
$self->{'oldpassword'} = $self->param('oldpassword');
unless (
(
(
$self->{'user'}
and length( $self->{'user'} = $self->param('user') ) > 0
)
&& (
(
length( $self->{'password'} = $self->param('password') ) > 0
)
|| (
$self->{'newpassword'}
and length(
$self->{'newpassword'} = $self->param('newpassword')
) > 0
)
)
)
|| ( $self->{'mail'}
and length( $self->{'mail'} = $self->param('mail') ) > 0 )
);
$self->{'oldpassword'} = $self->param('oldpassword');
$self->{'confirmpassword'} = $self->param('confirmpassword');
PE_OK;
}
......@@ -49,7 +66,8 @@ sub setAuthSessionInfo {
# Store submitted password if set in configuration
# WARNING: it can be a security hole
if ( $self->{storePassword} ) {
$self->{sessionInfo}->{'_password'} = $self->{'newpassword'} || $self->{'password'};
$self->{sessionInfo}->{'_password'} = $self->{'newpassword'}
|| $self->{'password'};
}
PE_OK;
}
......
......@@ -38,6 +38,8 @@ ok(
globalStorage => 'Apache::Session::File',
domain => 'example.com',
authentication => 'LDAP test=1',
user => '',
password => '',
}
),
'Portal object'
......
......@@ -7,7 +7,7 @@
package My::Portal;
use strict;
use Test::More tests => 15;
use Test::More tests => 16;
BEGIN { use_ok( 'Lemonldap::NG::Portal::Simple', ':all' ) }
#use Lemonldap::NG::Portal::Simple;
......@@ -25,10 +25,6 @@ my @h = (
# http://test.example.com
'aHR0cDovL3Rlc3QuZXhhbXBsZS5jb20v' => PE_OK, 'Missing / in URL',
# http://t.example.com/test
'aHR0cDovL3QuZXhhbXBsZS5jb20vdGVzdA==' => PE_OK,
'Undeclared virtual host in protected domain',
# http://test.example.com:8000/test
'aHR0cDovL3Rlc3QuZXhhbXBsZS5jb206ODAwMC90ZXN0' => PE_OK, 'Non default port',
......@@ -36,6 +32,14 @@ my @h = (
'aHR0cDovL3Rlc3QuZXhhbXBsZS5jb206ODAwMA==' => PE_OK,
'Non default port with missing /',
# http://t.example2.com/test
'aHR0cDovL3QuZXhhbXBsZTIuY29tL3Rlc3Q=' => PE_OK,
'Undeclared virtual host in trusted domain',
# http://t.example.com/test
'aHR0cDovL3QuZXhhbXBsZS5jb20vdGVzdA==' => PE_BADURL,
'Undeclared virtual host in (untrusted) protected domain',
'http://test.com/' => PE_BADURL, 'Non base64 encoded characters',
# http://test.example.com:8000V
......@@ -88,6 +92,7 @@ ok(
domain => 'example.com',
authentication => 'LDAP test=1',
domain => 'example.com',
trustedDomains => 'example2.com',
}
),
'Portal object'
......@@ -104,3 +109,4 @@ while ( defined( $url = shift(@h) ) ) {
#print ($p->controlUrlOrigin() == $result ? "OK" : "NOK");
#print " $url\n";
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment