Bug#1053220: bullseye-pu: package lemonldap-ng/2.0.11+ds-4+deb11u5

Yadd yadd at debian.org
Fri Sep 29 14:45:15 BST 2023


Package: release.debian.org
Severity: normal
Tags: bullseye
User: release.debian.org at packages.debian.org
Usertags: pu
X-Debbugs-Cc: lemonldap-ng at packages.debian.org, yadd at debian.org
Control: affects -1 + src:lemonldap-ng

[ Reason ]
Two new vulnerabilities have been dicovered and fixed in lemonldap-ng:
 - an open redirection due to incorrect escape handling
 - an open redirection only when configuration is edited by hand and
   doesn't follow OIDC specifications
 - a server-side-request-forgery (CVE-2023-44469) in OIDC protocol:
   A little-know feature of OIDC allows the OpenID Provider to fetch the
   Authorization request parameters itself by indicating a request_uri
   parameter. This feature is now restricted to a white list using this
   patch

[ Impact ]
Two low and one medium security issue.

[ Tests ]
Patches includes test updates

[ Risks ]
Outside of test changes, patches are not so big and the test coverage
provided by upstream is good, so risk is moderate.

[ Checklist ]
  [X] *all* changes are documented in the d/changelog
  [X] I reviewed all changes and I approve them
  [X] attach debdiff against the package in (old)stable
  [X] the issue is verified as fixed in unstable

[ Changes ]
- open redirection patch: use `URI->new($url)->as_string` in each
  redirections
- OIDC open redirection patch: just rejects requests with `redirect_uri` if
  relying party configuration has no declared redirect URIs.
- SSRF patch:
  * add new configuration parameter to list authorized "request_uris"
  * change the algorithm that manage request_uri parameter

Cheers,
Yadd
-------------- next part --------------
diff --git a/debian/NEWS b/debian/NEWS
index c4d7ee951..ba4a14a12 100644
--- a/debian/NEWS
+++ b/debian/NEWS
@@ -1,3 +1,13 @@
+lemonldap-ng (2.0.11+ds-4+deb11u5) bullseye; urgency=medium
+
+  A little-know feature of OIDC allows the OpenID Provider to fetch the
+  Authorization request parameters itself by indicating a request_uri
+  parameter.
+  By default, this feature is now restricted to a white list. See
+  Relying-Party security option to fill this field.
+
+ -- Yadd <yadd at debian.org>  Fri, 29 Sep 2023 17:38:51 +0400
+
 lemonldap-ng (2.0.11+ds-4+deb11u4) bullseye; urgency=medium
 
   AuthBasic now enforces 2FA activation (CVE-2023-28862):
diff --git a/debian/changelog b/debian/changelog
index 5d2c62ac0..35d5599a4 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,11 @@
+lemonldap-ng (2.0.11+ds-4+deb11u5) bullseye; urgency=medium
+
+  * Fix open redirection when OIDC RP has no redirect uris
+  * Fix open redirection due to incorrect escape handling
+  * Fix Server-Side-Request-Forgery issue in OIDC (CVE-2023-44469)
+
+ -- Yadd <yadd at debian.org>  Fri, 29 Sep 2023 16:35:14 +0400
+
 lemonldap-ng (2.0.11+ds-4+deb11u4) bullseye; urgency=medium
 
   * Fix 2FA issue when using AuthBasic handler (CVE-2023-28862)
@@ -19,7 +27,7 @@ lemonldap-ng (2.0.11+ds-4+deb11u2) bullseye; urgency=medium
 
 lemonldap-ng (2.0.11+ds-4+deb11u1) bullseye; urgency=medium
 
-  * Fix auth process in password-testing plugins (Closes: CVE-2021-20874)
+  * Fix auth process in password-testing plugins (Closes: #1005302, CVE-2021-40874)
 
  -- Yadd <yadd at debian.org>  Thu, 24 Feb 2022 15:16:09 +0100
 
diff --git a/debian/clean b/debian/clean
index 73f167814..cdb4a5ae4 100644
--- a/debian/clean
+++ b/debian/clean
@@ -1,3 +1,4 @@
+doc/pages/documentation/current/.buildinfo
 lemonldap-ng-manager/site/htdocs/static/js/conftree.js
 lemonldap-ng-manager/site/htdocs/static/struct.json
 lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Attributes.pm
diff --git a/debian/patches/SSRF-issue.patch b/debian/patches/SSRF-issue.patch
new file mode 100644
index 000000000..dce756430
--- /dev/null
+++ b/debian/patches/SSRF-issue.patch
@@ -0,0 +1,627 @@
+Description: fix SSRF vulnerability
+ Issue described here: https://security.lauritz-holtmann.de/post/sso-security-ssrf/
+Author: Maxime Besson <maxime.besson at worteks.com>
+Origin: upstream, https://gitlab.ow2.org/lemonldap-ng/lemonldap-ng/-/merge_requests/383/diffs
+Bug: https://gitlab.ow2.org/lemonldap-ng/lemonldap-ng/-/issues/2998
+Forwarded: not-needed
+Applied-Upstream: 2.17.1, https://gitlab.ow2.org/lemonldap-ng/lemonldap-ng/-/merge_requests/383/diffs
+Reviewed-By: Yadd <yadd at debian.org>
+Last-Update: 2023-09-23
+
+--- a/doc/sources/admin/idpopenidconnect.rst
++++ b/doc/sources/admin/idpopenidconnect.rst
+@@ -278,6 +278,11 @@
+       the Session Browser.
+    - **Allow OAuth2.0 Password Grant** (since version ``2.0.8``): Allow the use of the :ref:`Resource Owner Password Credentials Grant <resource-owner-password-grant>` by this client. This feature only works if you have configured a form-based authentication module.
+    - **Allow OAuth2.0 Client Credentials Grant** (since version ``2.0.11``): Allow the use of the :ref:`Resource Owner Password Credentials Grant <client-credentials-grant>` by this client.
++   - **Allowed URLs for fetching Request Object**: (since version ``2.17.1``):
++     which URLs may be called by the portal to fetch the request object (see
++     `request_uri
++     <https://openid.net/specs/openid-connect-core-1_0.html#RequestUriParameter>`__
++     in OIDC specifications). These URLs may use wildcards (``https://app.example.com/*``).
+    - **Authentication Level**: required authentication level to access this application
+    - **Access Rule**: lets you specify a :doc:`Perl rule<rules_examples>` to restrict access to this client
+ 
+--- a/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Build/Attributes.pm
++++ b/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Build/Attributes.pm
+@@ -4202,6 +4202,7 @@
+         oidcRPMetaDataOptionsAuthorizationCodeExpiration => { type => 'int' },
+         oidcRPMetaDataOptionsOfflineSessionExpiration    => { type => 'int' },
+         oidcRPMetaDataOptionsRedirectUris                => { type => 'text', },
++        oidcRPMetaDataOptionsRequestUris                 => { type => 'text', },
+         oidcRPMetaDataOptionsExtraClaims                 => {
+             type    => 'keyTextContainer',
+             keyTest => qr/^[\x21\x23-\x5B\x5D-\x7E]+$/,
+--- a/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Build/CTrees.pm
++++ b/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Build/CTrees.pm
+@@ -225,6 +225,7 @@
+                             'oidcRPMetaDataOptionsAllowOffline',
+                             'oidcRPMetaDataOptionsAllowPasswordGrant',
+                             'oidcRPMetaDataOptionsAllowClientCredentialsGrant',
++                            'oidcRPMetaDataOptionsRequestUris',
+                             'oidcRPMetaDataOptionsAuthnLevel',
+                             'oidcRPMetaDataOptionsRule',
+                         ]
+--- a/lemonldap-ng-manager/site/htdocs/static/languages/ar.json
++++ b/lemonldap-ng-manager/site/htdocs/static/languages/ar.json
+@@ -626,6 +626,7 @@
+ "oidcRPMetaDataOptionsLogoutUrl":"?? ?? ??",
+ "oidcOPMetaDataOptionsProtocol":"????????",
+ "oidcRPMetaDataOptionsPublic":"Public client",
++"oidcRPMetaDataOptionsRequestUris":"Allowed URLs for fetching Request Object",
+ "oidcRPMetaDataOptionsRequirePKCE":"Require PKCE",
+ "oidcRPMetaDataOptionsAuthnLevel":"????? ????? ??????",
+ "oidcRPMetaDataOptionsRule":"????? ??????",
+@@ -1194,4 +1195,4 @@
+ "samlRelayStateTimeout":"????? ???? ???? ?????? ",
+ "samlUseQueryStringSpecific":"??????? ????? query_string ??????",
+ "samlOverrideIDPEntityID":"Override Entity ID when acting as IDP"
+-}
+\ No newline at end of file
++}
+--- a/lemonldap-ng-manager/site/htdocs/static/languages/de.json
++++ b/lemonldap-ng-manager/site/htdocs/static/languages/de.json
+@@ -626,6 +626,7 @@
+ "oidcRPMetaDataOptionsLogoutUrl":"URL",
+ "oidcOPMetaDataOptionsProtocol":"Protocol",
+ "oidcRPMetaDataOptionsPublic":"Public client",
++"oidcRPMetaDataOptionsRequestUris":"Allowed URLs for fetching Request Object",
+ "oidcRPMetaDataOptionsRequirePKCE":"Require PKCE",
+ "oidcRPMetaDataOptionsAuthnLevel":"Authentication level",
+ "oidcRPMetaDataOptionsRule":"Access rule",
+@@ -1194,4 +1195,4 @@
+ "samlRelayStateTimeout":"RelayState session timeout",
+ "samlUseQueryStringSpecific":"Use specific query_string method",
+ "samlOverrideIDPEntityID":"Override Entity ID when acting as IDP"
+-}
+\ No newline at end of file
++}
+--- a/lemonldap-ng-manager/site/htdocs/static/languages/en.json
++++ b/lemonldap-ng-manager/site/htdocs/static/languages/en.json
+@@ -626,6 +626,7 @@
+ "oidcRPMetaDataOptionsLogoutUrl":"URL",
+ "oidcOPMetaDataOptionsProtocol":"Protocol",
+ "oidcRPMetaDataOptionsPublic":"Public client",
++"oidcRPMetaDataOptionsRequestUris":"Allowed URLs for fetching Request Object",
+ "oidcRPMetaDataOptionsRequirePKCE":"Require PKCE",
+ "oidcRPMetaDataOptionsAuthnLevel":"Authentication level",
+ "oidcRPMetaDataOptionsRule":"Access rule",
+--- a/lemonldap-ng-manager/site/htdocs/static/languages/es.json
++++ b/lemonldap-ng-manager/site/htdocs/static/languages/es.json
+@@ -626,6 +626,7 @@
+ "oidcRPMetaDataOptionsLogoutUrl":"URL",
+ "oidcOPMetaDataOptionsProtocol":"Protocolo",
+ "oidcRPMetaDataOptionsPublic":"Cliente p?blico",
++"oidcRPMetaDataOptionsRequestUris":"Allowed URLs for fetching Request Object",
+ "oidcRPMetaDataOptionsRequirePKCE":"Se requiere PKCE",
+ "oidcRPMetaDataOptionsAuthnLevel":"Authentication level",
+ "oidcRPMetaDataOptionsRule":"Regla de acceso",
+@@ -1194,4 +1195,4 @@
+ "samlRelayStateTimeout":"RelayState session timeout",
+ "samlUseQueryStringSpecific":"Use specific query_string method",
+ "samlOverrideIDPEntityID":"Override Entity ID when acting as IDP"
+-}
+\ No newline at end of file
++}
+--- a/lemonldap-ng-manager/site/htdocs/static/languages/fr.json
++++ b/lemonldap-ng-manager/site/htdocs/static/languages/fr.json
+@@ -627,6 +627,7 @@
+ "oidcRPMetaDataOptionsLogoutUrl":"URL",
+ "oidcOPMetaDataOptionsProtocol":"Protocole",
+ "oidcRPMetaDataOptionsPublic":"Client public",
++"oidcRPMetaDataOptionsRequestUris":"URLs autoris?es pour r?cup?rer les param?tres de la requ?te",
+ "oidcRPMetaDataOptionsRequirePKCE":"PKCE requis",
+ "oidcRPMetaDataOptionsAuthnLevel":"Niveau d'authentification",
+ "oidcRPMetaDataOptionsRule":"R?gle d'acc?s",
+--- a/lemonldap-ng-manager/site/htdocs/static/languages/it.json
++++ b/lemonldap-ng-manager/site/htdocs/static/languages/it.json
+@@ -626,6 +626,7 @@
+ "oidcRPMetaDataOptionsLogoutUrl":"URL",
+ "oidcOPMetaDataOptionsProtocol":"Protocollo",
+ "oidcRPMetaDataOptionsPublic":"Cliente pubblico",
++"oidcRPMetaDataOptionsRequestUris":"Allowed URLs for fetching Request Object",
+ "oidcRPMetaDataOptionsRequirePKCE":"Richiedi PKCE",
+ "oidcRPMetaDataOptionsAuthnLevel":"Livello di autenticazione",
+ "oidcRPMetaDataOptionsRule":"Regola di accesso",
+@@ -1194,4 +1195,4 @@
+ "samlRelayStateTimeout":"Timeout di sessione di RelayState",
+ "samlUseQueryStringSpecific":"Utilizza il metodo specifico query_string",
+ "samlOverrideIDPEntityID":"Sostituisci l'ID entit? quando agisce come IDP"
+-}
+\ No newline at end of file
++}
+--- a/lemonldap-ng-manager/site/htdocs/static/languages/pl.json
++++ b/lemonldap-ng-manager/site/htdocs/static/languages/pl.json
+@@ -626,6 +626,7 @@
+ "oidcRPMetaDataOptionsLogoutUrl":"URL",
+ "oidcOPMetaDataOptionsProtocol":"Protok??",
+ "oidcRPMetaDataOptionsPublic":"Klient publiczny",
++"oidcRPMetaDataOptionsRequestUris":"Allowed URLs for fetching Request Object",
+ "oidcRPMetaDataOptionsRequirePKCE":"Wymagaj PKCE",
+ "oidcRPMetaDataOptionsAuthnLevel":"Poziom uwierzytelnienia",
+ "oidcRPMetaDataOptionsRule":"Regu?a dost?pu",
+@@ -1194,4 +1195,4 @@
+ "samlRelayStateTimeout":"Limit czasu sesji RelayState",
+ "samlUseQueryStringSpecific":"U?yj okre?lonej metody query_string",
+ "samlOverrideIDPEntityID":"Zast?p identyfikator jednostki podczas dzia?ania jako IDP"
+-}
+\ No newline at end of file
++}
+--- a/lemonldap-ng-manager/site/htdocs/static/languages/tr.json
++++ b/lemonldap-ng-manager/site/htdocs/static/languages/tr.json
+@@ -626,6 +626,7 @@
+ "oidcRPMetaDataOptionsLogoutUrl":"URL",
+ "oidcOPMetaDataOptionsProtocol":"Protokol",
+ "oidcRPMetaDataOptionsPublic":"A??k istemci",
++"oidcRPMetaDataOptionsRequestUris":"Allowed URLs for fetching Request Object",
+ "oidcRPMetaDataOptionsRequirePKCE":"PKCE gerektir",
+ "oidcRPMetaDataOptionsAuthnLevel":"Do?rulama seviyesi",
+ "oidcRPMetaDataOptionsRule":"Eri?im kural?",
+@@ -1194,4 +1195,4 @@
+ "samlRelayStateTimeout":"RelayState oturum zaman a??m?",
+ "samlUseQueryStringSpecific":"Spesifik query_string metodu kullan",
+ "samlOverrideIDPEntityID":"IDP olarak davrand???nda Varl?k ID'yi ge?ersiz k?l"
+-}
+\ No newline at end of file
++}
+--- a/lemonldap-ng-manager/site/htdocs/static/languages/vi.json
++++ b/lemonldap-ng-manager/site/htdocs/static/languages/vi.json
+@@ -626,6 +626,7 @@
+ "oidcRPMetaDataOptionsLogoutUrl":"URL",
+ "oidcOPMetaDataOptionsProtocol":"Giao th?c",
+ "oidcRPMetaDataOptionsPublic":"Public client",
++"oidcRPMetaDataOptionsRequestUris":"Allowed URLs for fetching Request Object",
+ "oidcRPMetaDataOptionsRequirePKCE":"Require PKCE",
+ "oidcRPMetaDataOptionsAuthnLevel":"M?c x?c th?c",
+ "oidcRPMetaDataOptionsRule":"Quy t?c truy c?p",
+@@ -1194,4 +1195,4 @@
+ "samlRelayStateTimeout":"Th?i gian h?t h?n phi?n RelayState ",
+ "samlUseQueryStringSpecific":"S? d?ng ph??ng ph?p query_string c? th?",
+ "samlOverrideIDPEntityID":"Override Entity ID when acting as IDP"
+-}
+\ No newline at end of file
++}
+--- a/lemonldap-ng-manager/site/htdocs/static/languages/zh.json
++++ b/lemonldap-ng-manager/site/htdocs/static/languages/zh.json
+@@ -626,6 +626,7 @@
+ "oidcRPMetaDataOptionsLogoutUrl":"URL",
+ "oidcOPMetaDataOptionsProtocol":"Protocol",
+ "oidcRPMetaDataOptionsPublic":"Public client",
++"oidcRPMetaDataOptionsRequestUris":"Allowed URLs for fetching Request Object",
+ "oidcRPMetaDataOptionsRequirePKCE":"Require PKCE",
+ "oidcRPMetaDataOptionsAuthnLevel":"????",
+ "oidcRPMetaDataOptionsRule":"Access rule",
+@@ -1194,4 +1195,4 @@
+ "samlRelayStateTimeout":"RelayState session timeout",
+ "samlUseQueryStringSpecific":"Use specific query_string method",
+ "samlOverrideIDPEntityID":"Override Entity ID when acting as IDP"
+-}
+\ No newline at end of file
++}
+--- a/lemonldap-ng-manager/site/htdocs/static/languages/zh_TW.json
++++ b/lemonldap-ng-manager/site/htdocs/static/languages/zh_TW.json
+@@ -626,6 +626,7 @@
+ "oidcRPMetaDataOptionsLogoutUrl":"URL",
+ "oidcOPMetaDataOptionsProtocol":"??",
+ "oidcRPMetaDataOptionsPublic":"?????",
++"oidcRPMetaDataOptionsRequestUris":"Allowed URLs for fetching Request Object",
+ "oidcRPMetaDataOptionsRequirePKCE":"?? PKCE",
+ "oidcRPMetaDataOptionsAuthnLevel":"????",
+ "oidcRPMetaDataOptionsRule":"????",
+@@ -1194,4 +1195,4 @@
+ "samlRelayStateTimeout":"RelayState ??????",
+ "samlUseQueryStringSpecific":"????? query_string ??",
+ "samlOverrideIDPEntityID":"?? IDP ???? ID"
+-}
+\ No newline at end of file
++}
+--- a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Issuer/OpenIDConnect.pm
++++ b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Issuer/OpenIDConnect.pm
+@@ -195,32 +195,97 @@
+             $self->logger->debug(
+                 "OIDC $flow flow requested (response type: $response_type)");
+ 
+-            # Extract request_uri/request parameter
+-            if ( $oidc_request->{'request_uri'} ) {
+-                my $request =
+-                  $self->getRequestJWT( $oidc_request->{'request_uri'} );
++            # Client ID must be provided and cannot come from
++            # request or request_uri
++            unless ( $oidc_request->{'client_id'} ) {
++                $self->logger->error("Client ID is required");
++                return PE_ERROR;
++            }
++
++            # Check client_id
++            my $client_id = $oidc_request->{'client_id'};
++            $self->logger->debug("Request from client id $client_id");
++
++            # Verify that client_id is registered in configuration
++            my $rp = $self->getRP($client_id);
++
++            unless ($rp) {
++                $self->logger->error(
++                        "No registered Relying Party found with"
++                      . " client_id $client_id" );
++                return PE_UNAUTHORIZEDPARTNER;
++            }
++            else {
++                $self->logger->debug("Client id $client_id matches RP $rp");
++            }
+ 
+-                if ($request) {
+-                    $oidc_request->{'request'} = $request;
++            # Scope must be provided and cannot come from request or request_uri
++            unless ( $oidc_request->{'scope'} ) {
++                $self->logger->error("Scope is required");
++                return PE_ERROR;
++            }
++
++            # Extract request_uri/request parameter
++            if ( my $request_uri = $oidc_request->{'request_uri'} ) {
++                if (
++                    $self->isUriAllowedForRP(
++                        $request_uri,                       $rp,
++                        "oidcRPMetaDataOptionsRequestUris", 1
++                    )
++                  )
++                {
++                    my $request = $self->getRequestJWT($request_uri);
++                    if ($request) {
++                        $oidc_request->{'request'} = $request;
++                    }
++                    else {
++                        $self->logger->error(
++                            "Error with Request URI resolution");
++                        return PE_ERROR;
++                    }
+                 }
+                 else {
+-                    $self->logger->error("Error with Request URI resolution");
++                    $self->logger->error(
++                        "Request URI $request_uri is not allowed for $rp");
+                     return PE_ERROR;
+                 }
+             }
+ 
+             if ( $oidc_request->{'request'} ) {
+-                my $request =
+-                  $self->getJWTJSONData( $oidc_request->{'request'} );
++                if (
++                    $self->verifyJWTSignature(
++                        $oidc_request->{'request'},
++                        undef, $rp
++                    )
++                  )
++                {
++                    $self->logger->debug("JWT signature request verified");
++                    my $request = getJWTPayload( $oidc_request->{'request'} );
+ 
+-                # Override OIDC parameters by request content
+-                foreach ( keys %$request ) {
+-                    $self->logger->debug(
+-"Override $_ OIDC param by value present in request parameter"
+-                    );
+-                    $oidc_request->{$_} = $request->{$_};
+-                    $self->p->setHiddenFormValue( $req, $_, $request->{$_}, '',
+-                        0 );
++                    # Override OIDC parameters by request content
++                    foreach ( keys %$request ) {
++                        $self->logger->debug( "Override $_ OIDC param"
++                              . " by value present in request parameter" );
++
++                        if ( $_ eq "client_id" or $_ eq "response_type" ) {
++                            if ( $oidc_request->{$_} ne $request->{$_} ) {
++                                $self->logger->error( "$_ from request JWT ("
++                                      . $oidc_request->{$_}
++                                      . ") does not match $_ from request URI ("
++                                      . $request->{$_}
++                                      . ")" );
++                                return PE_ERROR;
++                            }
++                        }
++                        $oidc_request->{$_} = $request->{$_};
++                        $self->p->setHiddenFormValue( $req, $_, $request->{$_},
++                            '', 0 );
++                    }
++                }
++                else {
++                    $self->logger->error(
++                        "JWT signature request can not be verified");
++                    return PE_ERROR;
+                 }
+             }
+ 
+@@ -229,37 +294,12 @@
+                 $self->logger->error("Redirect URI is required");
+                 return PE_ERROR;
+             }
+-            unless ( $oidc_request->{'scope'} ) {
+-                $self->logger->error("Scope is required");
+-                return PE_ERROR;
+-            }
+-            unless ( $oidc_request->{'client_id'} ) {
+-                $self->logger->error("Client ID is required");
+-                return PE_ERROR;
+-            }
+             if ( $flow eq "implicit" and not defined $oidc_request->{'nonce'} )
+             {
+                 $self->logger->error("Nonce is required for implicit flow");
+                 return PE_ERROR;
+             }
+ 
+-            # Check client_id
+-            my $client_id = $oidc_request->{'client_id'};
+-            $self->logger->debug("Request from client id $client_id");
+-
+-            # Verify that client_id is registered in configuration
+-            my $rp = $self->getRP($client_id);
+-
+-            unless ($rp) {
+-                $self->logger->error(
+-"No registered Relying Party found with client_id $client_id"
+-                );
+-                return PE_UNAUTHORIZEDPARTNER;
+-            }
+-            else {
+-                $self->logger->debug("Client id $client_id matches RP $rp");
+-            }
+-
+             # Check if this RP is authorized
+             if ( my $rule = $self->spRules->{$rp} ) {
+                 unless ( $rule->( $req, $req->sessionInfo ) ) {
+@@ -276,24 +316,14 @@
+ 
+             # Check redirect_uri
+             my $redirect_uri  = $oidc_request->{'redirect_uri'};
+-            my $redirect_uris = $self->conf->{oidcRPMetaDataOptions}->{$rp}
+-              ->{oidcRPMetaDataOptionsRedirectUris};
+-
+-            if ($redirect_uris) {
+-                my $redirect_uri_allowed = 0;
+-                foreach ( split( /\s+/, $redirect_uris ) ) {
+-                    $redirect_uri_allowed = 1 if $redirect_uri eq $_;
+-                }
+-                unless ($redirect_uri_allowed) {
+-                    $self->userLogger->error(
+-                        "Redirect URI $redirect_uri not allowed");
+-                    return PE_BADURL;
+-                }
+-            }
+-            elsif ($redirect_uri) {
+-                $self->logger->error(
+-"RP $rp has no RedirectUris, unable to handle accept redirect_uri=$redirect_uri"
+-                );
++            if (
++                !$self->isUriAllowedForRP(
++                    $redirect_uri, $rp, 'oidcRPMetaDataOptionsRedirectUris'
++                )
++              )
++            {
++                $self->userLogger->error(
++                    "Redirect URI $redirect_uri not allowed");
+                 return 37; # PE_BADURL value
+             }
+ 
+@@ -397,24 +427,6 @@
+                 return PE_OK;
+             }
+ 
+-            # Check Request JWT signature
+-            if ( $oidc_request->{'request'} ) {
+-                unless (
+-                    $self->verifyJWTSignature(
+-                        $oidc_request->{'request'},
+-                        undef, $rp
+-                    )
+-                  )
+-                {
+-                    $self->logger->error(
+-                        "JWT signature request can not be verified");
+-                    return PE_ERROR;
+-                }
+-                else {
+-                    $self->logger->debug("JWT signature request verified");
+-                }
+-            }
+-
+             # Check id_token_hint
+             my $id_token_hint = $oidc_request->{'id_token_hint'};
+             if ($id_token_hint) {
+@@ -957,27 +969,13 @@
+ 
+                 if ($post_logout_redirect_uri) {
+ 
+-                    # Check redirect URI is allowed
+-                    my $redirect_uri_allowed = 0;
+-                    foreach ( keys %{ $self->conf->{oidcRPMetaDataOptions} } ) {
+-                        my $logout_rp = $_;
+-                        if ( my $redirect_uris =
+-                            $self->conf->{oidcRPMetaDataOptions}->{$logout_rp}
+-                            ->{oidcRPMetaDataOptionsPostLogoutRedirectUris} )
+-                        {
+-
+-                            foreach ( split( /\s+/, $redirect_uris ) ) {
+-                                if ( $post_logout_redirect_uri eq $_ ) {
+-                                    $self->logger->debug(
+-"$post_logout_redirect_uri is an allowed logout redirect URI for RP $logout_rp"
+-                                    );
+-                                    $redirect_uri_allowed = 1;
+-                                }
+-                            }
+-                        }
+-                    }
+-
+-                    unless ($redirect_uri_allowed) {
++                    unless (
++                        $self->findRPFromUri(
++                            $post_logout_redirect_uri,
++                            'oidcRPMetaDataOptionsPostLogoutRedirectUris'
++                        )
++                      )
++                    {
+                         $self->logger->error(
+                             "$post_logout_redirect_uri is not allowed");
+                         return PE_BADURL;
+@@ -1009,6 +1007,43 @@
+     return PE_ERROR;
+ }
+ 
++sub findRPFromUri {
++    my ( $self, $uri, $option ) = @_;
++
++    my $found_rp;
++    foreach my $rp ( keys %{ $self->conf->{oidcRPMetaDataOptions} } ) {
++        $found_rp = $rp if $self->isUriAllowedForRP( $uri, $rp, $option );
++    }
++    return $found_rp;
++}
++
++sub isUriAllowedForRP {
++    my ( $self, $uri, $rp, $option, $wildcard_allowed ) = @_;
++    my $allowed_uris = $self->conf->{oidcRPMetaDataOptions}->{$rp}->{$option} // "";
++
++    my $is_uri_allowed;
++    if ($wildcard_allowed) {
++        $is_uri_allowed =
++          grep { _wildcard_match( $_, $uri ) } split( /\s+/, $allowed_uris );
++    }
++    else {
++        $is_uri_allowed = grep { $_ eq $uri } split( /\s+/, $allowed_uris );
++    }
++    return $is_uri_allowed;
++}
++
++sub _wildcard_match {
++    my ( $config_url, $candidate ) = @_;
++
++    # Quote everything
++    my $config_re = $config_url =~ s/(.)/\Q$1/gr;
++
++    # Replace \* by .*
++    $config_re =~ s/\\\*/.*/g;
++
++    return ( $candidate =~ qr/^$config_re$/ ? 1 : 0 );
++}
++
+ # Handle token endpoint
+ sub token {
+     my ( $self, $req ) = @_;
+@@ -1917,6 +1952,7 @@
+     my $userinfo_signed_response_alg =
+       $client_metadata->{userinfo_signed_response_alg};
+     my $redirect_uris = $client_metadata->{redirect_uris};
++    my $request_uris  = $client_metadata->{request_uris};
+ 
+     # Register RP in global configuration
+     my $conf = $self->confAcc->getConf( { raw => 1, noCache => 1 } );
+@@ -1938,6 +1974,9 @@
+       = $id_token_signed_response_alg;
+     $conf->{oidcRPMetaDataOptions}->{$rp}->{oidcRPMetaDataOptionsRedirectUris}
+       = join( ' ', @$redirect_uris );
++    $conf->{oidcRPMetaDataOptions}->{$rp}->{oidcRPMetaDataOptionsRequestUris} =
++      join( ' ', @$request_uris )
++      if $request_uris and @$request_uris;
+     $conf->{oidcRPMetaDataOptions}->{$rp}
+       ->{oidcRPMetaDataOptionsUserInfoSignAlg} = $userinfo_signed_response_alg
+       if defined $userinfo_signed_response_alg;
+@@ -1975,6 +2014,8 @@
+         $registration_response->{'id_token_signed_response_alg'} =
+           $id_token_signed_response_alg;
+         $registration_response->{'redirect_uris'} = $redirect_uris;
++        $registration_response->{'request_uris'}  = $request_uris
++          if $request_uris and @$request_uris;
+         $registration_response->{'userinfo_signed_response_alg'} =
+           $userinfo_signed_response_alg
+           if defined $userinfo_signed_response_alg;
+@@ -2001,25 +2042,13 @@
+ 
+     if ($post_logout_redirect_uri) {
+ 
+-        # Check redirect URI is allowed
+-        my $redirect_uri_allowed = 0;
+-        foreach ( keys %{ $self->conf->{oidcRPMetaDataOptions} } ) {
+-            my $logout_rp = $_;
+-            my $redirect_uris =
+-              $self->conf->{oidcRPMetaDataOptions}->{$logout_rp}
+-              ->{oidcRPMetaDataOptionsPostLogoutRedirectUris};
+-
+-            foreach ( split( /\s+/, $redirect_uris ) ) {
+-                if ( $post_logout_redirect_uri eq $_ ) {
+-                    $self->logger->debug(
+-"$post_logout_redirect_uri is an allowed logout redirect URI for RP $logout_rp"
+-                    );
+-                    $redirect_uri_allowed = 1;
+-                }
+-            }
+-        }
+-
+-        unless ($redirect_uri_allowed) {
++        unless (
++            $self->findRPFromUri(
++                $post_logout_redirect_uri,
++                'oidcRPMetaDataOptionsPostLogoutRedirectUris'
++            )
++          )
++        {
+             $self->logger->error("$post_logout_redirect_uri is not allowed");
+             return $self->p->login($req);
+         }
+@@ -2202,7 +2231,7 @@
+             claims_supported                 => [qw/sub iss auth_time acr/],
+             request_parameter_supported      => JSON::true,
+             request_uri_parameter_supported  => JSON::true,
+-            require_request_uri_registration => JSON::false,
++            require_request_uri_registration => JSON::true,
+ 
+             # Algorithms
+             id_token_signing_alg_values_supported =>
+@@ -2254,19 +2283,7 @@
+         }
+     }
+ 
+-    # Extract request_uri/request parameter
+-    my $request = $req->param('request');
+-    if ( $req->param('request_uri') ) {
+-        $request = $self->getRequestJWT( $req->param('request_uri') );
+-    }
+-
+-    if ($request) {
+-        my $request_data = $self->getJWTJSONData($request);
+-        foreach ( keys %$request_data ) {
+-            $req->env->{ "llng_oidc_" . $_ } = $request_data->{$_};
+-        }
+-    }
+-
++    my $rp;
+     if ( $req->param('client_id') ) {
+         my $rp = $self->getRP( $req->param('client_id') );
+         $req->env->{"llng_oidc_rp"} = $rp if $rp;
+@@ -2278,6 +2295,27 @@
+           if $targetAuthnLevel;
+     }
+ 
++    # Extract request_uri/request parameter
++    my $request = $req->param('request');
++    if ( my $request_uri = $req->param('request_uri') ) {
++        if (
++            $rp
++            and $self->isUriAllowedForRP(
++                $request_uri, $rp, 'oidcRPMetaDataOptionsRequestUris', 1
++            )
++          )
++        {
++            $request = $self->getRequestJWT($request_uri);
++        }
++    }
++
++    if ($request) {
++        my $request_data = getJWTPayload($request);
++        foreach ( keys %$request_data ) {
++            $req->env->{ "llng_oidc_" . $_ } = $request_data->{$_};
++        }
++    }
++
+     return PE_OK;
+ }
+ 
diff --git a/debian/patches/fix-open-redirection-without-OIDC-redirect-uris.patch b/debian/patches/fix-open-redirection-without-OIDC-redirect-uris.patch
new file mode 100644
index 000000000..d65366fd1
--- /dev/null
+++ b/debian/patches/fix-open-redirection-without-OIDC-redirect-uris.patch
@@ -0,0 +1,365 @@
+Description: Fix open redirection when OIDC RP has no oidcRPMetaDataOptionsRedirectUris
+ This issue concerns only people that modify config by hand. The manager
+ refuses already a relying party without redirect URIs.
+Author: Yadd <yadd at debian.org>
+Origin: upstream, commit:c1de35ad
+Bug: https://gitlab.ow2.org/lemonldap-ng/lemonldap-ng/-/issues/3003
+Forwarded: not-needed
+Applied-Upstream: v2.17.1, commit:c1de35ad
+Reviewed-By: <name and email of someone who approved/reviewed the patch>
+Last-Update: 2023-09-20
+
+--- a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Issuer/OpenIDConnect.pm
++++ b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Issuer/OpenIDConnect.pm
+@@ -290,6 +290,12 @@
+                     return PE_BADURL;
+                 }
+             }
++            elsif ($redirect_uri) {
++                $self->logger->error(
++"RP $rp has no RedirectUris, unable to handle accept redirect_uri=$redirect_uri"
++                );
++                return 37; # PE_BADURL value
++            }
+ 
+             # Check if flow is allowed
+             if ( $flow eq "authorizationcode"
+--- a/lemonldap-ng-portal/t/32-Auth-and-issuer-OIDC-authorization_code-OP-logout.t
++++ b/lemonldap-ng-portal/t/32-Auth-and-issuer-OIDC-authorization_code-OP-logout.t
+@@ -228,6 +228,8 @@
+                           'http://auth.rp.com/oidc/logout',
+                         oidcRPMetaDataOptionsLogoutType            => 'front',
+                         oidcRPMetaDataOptionsLogoutSessionRequired => 0,
++                        oidcRPMetaDataOptionsRedirectUris          =>
++                          'http://auth.rp.com?openidconnectcallback=1',
+                     }
+                 },
+                 oidcOPMetaDataOptions           => {},
+--- a/lemonldap-ng-portal/t/32-Auth-and-issuer-OIDC-authorization_code-public_client.t
++++ b/lemonldap-ng-portal/t/32-Auth-and-issuer-OIDC-authorization_code-public_client.t
+@@ -338,7 +338,9 @@
+                         oidcRPMetaDataOptionsUserIDAttr            => "",
+                         oidcRPMetaDataOptionsAccessTokenExpiration => 3600,
+                         oidcRPMetaDataOptionsPostLogoutRedirectUris =>
+-                          "http://auth.rp.com/?logout=1"
++                          "http://auth.rp.com/?logout=1",
++                        oidcRPMetaDataOptionsRedirectUris =>
++                          'http://auth.rp.com/?openidconnectcallback=1',
+                     }
+                 },
+                 oidcOPMetaDataOptions           => {},
+--- a/lemonldap-ng-portal/t/32-Auth-and-issuer-OIDC-authorization_code-with-authchoice.t
++++ b/lemonldap-ng-portal/t/32-Auth-and-issuer-OIDC-authorization_code-with-authchoice.t
+@@ -292,7 +292,9 @@
+                         oidcRPMetaDataOptionsUserIDAttr        => "",
+                         oidcRPMetaDataOptionsAccessTokenExpiration => 3600,
+                         oidcRPMetaDataOptionsPostLogoutRedirectUris =>
+-                          "http://auth.rp.com/?logout=1"
++                          "http://auth.rp.com/?logout=1",
++                        oidcRPMetaDataOptionsRedirectUris =>
++                          'http://auth.rp.com/?openidconnectcallback=1',
+                     }
+                 },
+                 oidcOPMetaDataOptions           => {},
+--- a/lemonldap-ng-portal/t/32-Auth-and-issuer-OIDC-authorization_code-with-info.t
++++ b/lemonldap-ng-portal/t/32-Auth-and-issuer-OIDC-authorization_code-with-info.t
+@@ -342,7 +342,9 @@
+                         oidcRPMetaDataOptionsUserIDAttr        => "",
+                         oidcRPMetaDataOptionsAccessTokenExpiration => 3600,
+                         oidcRPMetaDataOptionsPostLogoutRedirectUris =>
+-                          "http://auth.rp.com/?logout=1"
++                          "http://auth.rp.com/?logout=1",
++                        oidcRPMetaDataOptionsRedirectUris =>
++                          'http://auth.rp.com/?openidconnectcallback=1',
+                     }
+                 },
+                 oidcOPMetaDataOptions           => {},
+--- a/lemonldap-ng-portal/t/32-Auth-and-issuer-OIDC-authorization_code-with-none-alg.t
++++ b/lemonldap-ng-portal/t/32-Auth-and-issuer-OIDC-authorization_code-with-none-alg.t
+@@ -334,7 +334,9 @@
+                         oidcRPMetaDataOptionsUserIDAttr        => "",
+                         oidcRPMetaDataOptionsAccessTokenExpiration => 3600,
+                         oidcRPMetaDataOptionsPostLogoutRedirectUris =>
+-                          "http://auth.rp.com/?logout=1"
++                          "http://auth.rp.com/?logout=1",
++                        oidcRPMetaDataOptionsRedirectUris =>
++                          'http://auth.rp.com/?openidconnectcallback=1',
+                     }
+                 },
+                 oidcOPMetaDataOptions           => {},
+--- a/lemonldap-ng-portal/t/32-Auth-and-issuer-OIDC-authorization_code.t
++++ b/lemonldap-ng-portal/t/32-Auth-and-issuer-OIDC-authorization_code.t
+@@ -337,6 +337,8 @@
+                         oidcRPMetaDataOptionsClientSecret      => "rpsecret",
+                         oidcRPMetaDataOptionsUserIDAttr        => "",
+                         oidcRPMetaDataOptionsAccessTokenExpiration => 3600,
++                        oidcRPMetaDataOptionsRedirectUris =>
++                          'http://auth.rp.com/?openidconnectcallback=1',
+                         oidcRPMetaDataOptionsPostLogoutRedirectUris =>
+                           "http://auth.rp.com/?logout=1"
+                     }
+--- a/lemonldap-ng-portal/t/32-Auth-and-issuer-OIDC-hybrid.t
++++ b/lemonldap-ng-portal/t/32-Auth-and-issuer-OIDC-hybrid.t
+@@ -255,7 +255,9 @@
+                         oidcRPMetaDataOptionsBypassConsent     => 1,
+                         oidcRPMetaDataOptionsClientSecret      => "rpsecret",
+                         oidcRPMetaDataOptionsUserIDAttr        => "",
+-                        oidcRPMetaDataOptionsAccessTokenExpiration => 3600
++                        oidcRPMetaDataOptionsAccessTokenExpiration => 3600,
++                        oidcRPMetaDataOptionsRedirectUris          =>
++                          'http://auth.rp.com?openidconnectcallback=1',
+                     }
+                 },
+                 oidcOPMetaDataOptions           => {},
+--- a/lemonldap-ng-portal/t/32-Auth-and-issuer-OIDC-implicit-no-token.t
++++ b/lemonldap-ng-portal/t/32-Auth-and-issuer-OIDC-implicit-no-token.t
+@@ -237,7 +237,9 @@
+                         oidcRPMetaDataOptionsBypassConsent     => 0,
+                         oidcRPMetaDataOptionsClientSecret      => "rpsecret",
+                         oidcRPMetaDataOptionsUserIDAttr        => "",
+-                        oidcRPMetaDataOptionsAccessTokenExpiration => 3600
++                        oidcRPMetaDataOptionsAccessTokenExpiration => 3600,
++                        oidcRPMetaDataOptionsRedirectUris          =>
++                          'http://auth.rp.com?openidconnectcallback=1',
+                     }
+                 },
+                 oidcOPMetaDataOptions           => {},
+--- a/lemonldap-ng-portal/t/32-Auth-and-issuer-OIDC-implicit.t
++++ b/lemonldap-ng-portal/t/32-Auth-and-issuer-OIDC-implicit.t
+@@ -253,7 +253,9 @@
+                         oidcRPMetaDataOptionsBypassConsent     => 0,
+                         oidcRPMetaDataOptionsClientSecret      => "rpsecret",
+                         oidcRPMetaDataOptionsUserIDAttr        => "",
+-                        oidcRPMetaDataOptionsAccessTokenExpiration => 3600
++                        oidcRPMetaDataOptionsAccessTokenExpiration => 3600,
++                        oidcRPMetaDataOptionsRedirectUris          =>
++                          'http://auth.rp.com?openidconnectcallback=1',
+                     }
+                 },
+                 oidcOPMetaDataOptions           => {},
+--- a/lemonldap-ng-portal/t/32-OIDC-Code-Flow-with-2F-UpgradeOnly.t
++++ b/lemonldap-ng-portal/t/32-OIDC-Code-Flow-with-2F-UpgradeOnly.t
+@@ -362,7 +362,9 @@
+                         oidcRPMetaDataOptionsAccessTokenExpiration => 3600,
+                         oidcRPMetaDataOptionsAuthnLevel            => 5,
+                         oidcRPMetaDataOptionsPostLogoutRedirectUris =>
+-                          "http://auth.rp.com/?logout=1"
++                          "http://auth.rp.com/?logout=1",
++                        oidcRPMetaDataOptionsRedirectUris =>
++                          'http://auth.rp.com/?openidconnectcallback=1',
+                     }
+                 },
+                 oidcOPMetaDataOptions           => {},
+--- a/lemonldap-ng-portal/t/32-OIDC-Code-Flow-with-2F.t
++++ b/lemonldap-ng-portal/t/32-OIDC-Code-Flow-with-2F.t
+@@ -362,7 +362,9 @@
+                         oidcRPMetaDataOptionsUserIDAttr        => "",
+                         oidcRPMetaDataOptionsAccessTokenExpiration => 3600,
+                         oidcRPMetaDataOptionsPostLogoutRedirectUris =>
+-                          "http://auth.rp.com/?logout=1"
++                          "http://auth.rp.com/?logout=1",
++                        oidcRPMetaDataOptionsRedirectUris =>
++                          'http://auth.rp.com/?openidconnectcallback=1',
+                     }
+                 },
+                 oidcOPMetaDataOptions           => {},
+--- a/lemonldap-ng-portal/t/32-OIDC-Hooks.t
++++ b/lemonldap-ng-portal/t/32-OIDC-Hooks.t
+@@ -57,6 +57,7 @@
+                     oidcRPMetaDataOptionsUserIDAttr            => "",
+                     oidcRPMetaDataOptionsAccessTokenExpiration => 3600,
+                     oidcRPMetaDataOptionsBypassConsent         => 1,
++                    oidcRPMetaDataOptionsRedirectUris => 'http://rp2.com/',
+                 },
+                 oauth => {
+                     oidcRPMetaDataOptionsDisplayName  => "oauth",
+--- a/lemonldap-ng-portal/t/32-OIDC-Macro.t
++++ b/lemonldap-ng-portal/t/32-OIDC-Macro.t
+@@ -136,6 +136,7 @@
+                         oidcRPMetaDataOptionsClientSecret      => "rpsecret",
+                         oidcRPMetaDataOptionsUserIDAttr        => "custom_sub",
+                         oidcRPMetaDataOptionsAccessTokenExpiration => 3600,
++                        oidcRPMetaDataOptionsRedirectUris => 'http://rp.com/',
+                     }
+                 },
+                 oidcOPMetaDataOptions           => {},
+--- a/lemonldap-ng-portal/t/32-OIDC-Offline-Session.t
++++ b/lemonldap-ng-portal/t/32-OIDC-Offline-Session.t
+@@ -60,7 +60,7 @@
+                     oidcRPMetaDataOptionsIDTokenForceClaims    => 1,
+                     oidcRPMetaDataOptionsAdditionalAudiences =>
+                       "http://my.extra.audience/test urn:extra2",
+-
++                      oidcRPMetaDataOptionsRedirectUris => 'http://test/',
+                 }
+             },
+             oidcOPMetaDataOptions           => {},
+--- a/lemonldap-ng-portal/t/32-OIDC-Refresh-Token.t
++++ b/lemonldap-ng-portal/t/32-OIDC-Refresh-Token.t
+@@ -56,6 +56,7 @@
+                     oidcRPMetaDataOptionsIDTokenForceClaims    => 1,
+                     oidcRPMetaDataOptionsAdditionalAudiences =>
+                       "http://my.extra.audience/test urn:extra2",
++                    oidcRPMetaDataOptionsRedirectUris => 'http://test/',
+                 }
+             },
+             oidcOPMetaDataOptions           => {},
+--- a/lemonldap-ng-portal/t/32-OIDC-Token-Introspection.t
++++ b/lemonldap-ng-portal/t/32-OIDC-Token-Introspection.t
+@@ -57,6 +57,7 @@
+                     oidcRPMetaDataOptionsUserIDAttr            => "",
+                     oidcRPMetaDataOptionsAccessTokenExpiration => 3600,
+                     oidcRPMetaDataOptionsBypassConsent         => 1,
++                    oidcRPMetaDataOptionsRedirectUris => 'http://rp2.com/',
+                 },
+                 oauth => {
+                     oidcRPMetaDataOptionsDisplayName  => "oauth",
+--- a/lemonldap-ng-portal/t/32-OIDC-Token-Security.t
++++ b/lemonldap-ng-portal/t/32-OIDC-Token-Security.t
+@@ -57,6 +57,7 @@
+                     oidcRPMetaDataOptionsUserIDAttr            => "",
+                     oidcRPMetaDataOptionsAccessTokenExpiration => 3600,
+                     oidcRPMetaDataOptionsBypassConsent         => 1,
++                    oidcRPMetaDataOptionsRedirectUris => 'http://rp.com/',
+                 },
+                 rp2 => {
+                     oidcRPMetaDataOptionsDisplayName           => "RP2",
+@@ -67,7 +68,8 @@
+                     oidcRPMetaDataOptionsUserIDAttr            => "",
+                     oidcRPMetaDataOptionsAccessTokenExpiration => 3600,
+                     oidcRPMetaDataOptionsBypassConsent         => 1,
+-                    oidcRPMetaDataOptionsRule => '$uid eq "dwho"',
++                    oidcRPMetaDataOptionsRule         => '$uid eq "dwho"',
++                    oidcRPMetaDataOptionsRedirectUris => 'http://rp2.com/',
+                 }
+             },
+             oidcOPMetaDataOptions           => {},
+@@ -104,7 +106,7 @@
+ 
+ # Try to get code for RP1 with invalide scope name
+ $query =
+-"response_type=code&scope=openid%20profile%20email%22&client_id=rpid&state=af0ifjsldkj&redirect_uri=http%3A%2F%2Frp2.com%2F";
++"response_type=code&scope=openid%20profile%20email%22&client_id=rpid&state=af0ifjsldkj&redirect_uri=http%3A%2F%2Frp.com%2F";
+ ok(
+     $res = $op->_get(
+         "/oauth2/authorize",
+@@ -119,7 +121,7 @@
+ #
+ # Get code for RP1
+ $query =
+-"response_type=code&scope=openid%20profile%20email&client_id=rpid&state=af0ifjsldkj&redirect_uri=http%3A%2F%2Frp2.com%2F";
++"response_type=code&scope=openid%20profile%20email&client_id=rpid&state=af0ifjsldkj&redirect_uri=http%3A%2F%2Frp.com%2F";
+ ok(
+     $res = $op->_get(
+         "/oauth2/authorize",
+@@ -131,7 +133,7 @@
+ );
+ count(1);
+ 
+-my ($code) = expectRedirection( $res, qr#http://rp2\.com/.*code=([^\&]*)# );
++my ($code) = expectRedirection( $res, qr#http://rp\.com/.*code=([^\&]*)# );
+ 
+ # Play code on RP2
+ $query =
+--- a/lemonldap-ng-portal/t/37-Issuer-Timeout.t
++++ b/lemonldap-ng-portal/t/37-Issuer-Timeout.t
+@@ -182,7 +182,9 @@
+                         oidcRPMetaDataOptionsAccessTokenExpiration => 3600,
+                         oidcRPMetaDataOptionsBypassConsent         => 1,
+                         oidcRPMetaDataOptionsPostLogoutRedirectUris =>
+-                          "http://auth.rp.com/?logout=1"
++                          "http://auth.rp.com/?logout=1",
++                        oidcRPMetaDataOptionsRedirectUris =>
++                          'http://rp.example.com/',
+                     },
+                     rp2 => {
+                         oidcRPMetaDataOptionsDisplayName       => "RP",
+@@ -195,7 +197,9 @@
+                         oidcRPMetaDataOptionsAccessTokenExpiration => 3600,
+                         oidcRPMetaDataOptionsBypassConsent         => 1,
+                         oidcRPMetaDataOptionsPostLogoutRedirectUris =>
+-                          "http://auth.rp2.com/?logout=1"
++                          "http://auth.rp2.com/?logout=1",
++                        oidcRPMetaDataOptionsRedirectUris =>
++                          'http://rp2.example.com/',
+                     }
+                 },
+                 oidcOPMetaDataOptions           => {},
+--- a/lemonldap-ng-portal/t/37-Logout-from-OIDC-RP-to-SAML-SP.t
++++ b/lemonldap-ng-portal/t/37-Logout-from-OIDC-RP-to-SAML-SP.t
+@@ -356,7 +356,9 @@
+                         oidcRPMetaDataOptionsBypassConsent     => 0,
+                         oidcRPMetaDataOptionsClientSecret      => "rpsecret",
+                         oidcRPMetaDataOptionsUserIDAttr        => "",
+-                        oidcRPMetaDataOptionsAccessTokenExpiration => 3600
++                        oidcRPMetaDataOptionsAccessTokenExpiration => 3600,
++                        oidcRPMetaDataOptionsRedirectUris          =>
++                          'http://auth.rp.com?openidconnectcallback=1',
+                     }
+                 },
+                 oidcOPMetaDataOptions           => {},
+--- a/lemonldap-ng-portal/t/37-OIDC-RP-to-SAML-IdP-GET-with-WAYF.t
++++ b/lemonldap-ng-portal/t/37-OIDC-RP-to-SAML-IdP-GET-with-WAYF.t
+@@ -377,7 +377,9 @@
+                         oidcRPMetaDataOptionsBypassConsent     => 0,
+                         oidcRPMetaDataOptionsClientSecret      => "rpsecret",
+                         oidcRPMetaDataOptionsUserIDAttr        => "",
+-                        oidcRPMetaDataOptionsAccessTokenExpiration => 3600
++                        oidcRPMetaDataOptionsAccessTokenExpiration => 3600,
++                        oidcRPMetaDataOptionsRedirectUris          =>
++                          'http://auth.rp.com?openidconnectcallback=1',
+                     }
+                 },
+                 oidcOPMetaDataOptions           => {},
+--- a/lemonldap-ng-portal/t/37-OIDC-RP-to-SAML-IdP-GET.t
++++ b/lemonldap-ng-portal/t/37-OIDC-RP-to-SAML-IdP-GET.t
+@@ -357,7 +357,9 @@
+                         oidcRPMetaDataOptionsBypassConsent     => 0,
+                         oidcRPMetaDataOptionsClientSecret      => "rpsecret",
+                         oidcRPMetaDataOptionsUserIDAttr        => "",
+-                        oidcRPMetaDataOptionsAccessTokenExpiration => 3600
++                        oidcRPMetaDataOptionsAccessTokenExpiration => 3600,
++                        oidcRPMetaDataOptionsRedirectUris          =>
++                          'http://auth.rp.com?openidconnectcallback=1',
+                     }
+                 },
+                 oidcOPMetaDataOptions           => {},
+--- a/lemonldap-ng-portal/t/37-OIDC-RP-to-SAML-IdP-POST.t
++++ b/lemonldap-ng-portal/t/37-OIDC-RP-to-SAML-IdP-POST.t
+@@ -359,7 +359,9 @@
+                         oidcRPMetaDataOptionsBypassConsent     => 0,
+                         oidcRPMetaDataOptionsClientSecret      => "rpsecret",
+                         oidcRPMetaDataOptionsUserIDAttr        => "",
+-                        oidcRPMetaDataOptionsAccessTokenExpiration => 3600
++                        oidcRPMetaDataOptionsAccessTokenExpiration => 3600,
++                        oidcRPMetaDataOptionsRedirectUris          =>
++                          'http://auth.rp.com?openidconnectcallback=1',
+                     }
+                 },
+                 oidcOPMetaDataOptions           => {},
+--- a/lemonldap-ng-portal/t/37-SAML-SP-GET-to-OIDC-OP.t
++++ b/lemonldap-ng-portal/t/37-SAML-SP-GET-to-OIDC-OP.t
+@@ -295,7 +295,9 @@
+                         oidcRPMetaDataOptionsBypassConsent     => 0,
+                         oidcRPMetaDataOptionsClientSecret      => "rpsecret",
+                         oidcRPMetaDataOptionsUserIDAttr        => "",
+-                        oidcRPMetaDataOptionsAccessTokenExpiration => 3600
++                        oidcRPMetaDataOptionsAccessTokenExpiration => 3600,
++                        oidcRPMetaDataOptionsRedirectUris          =>
++                          'http://auth.proxy.com?openidconnectcallback=1',
+                     }
+                 },
+                 oidcOPMetaDataOptions           => {},
+--- a/lemonldap-ng-portal/t/37-SAML-SP-POST-to-OIDC-OP.t
++++ b/lemonldap-ng-portal/t/37-SAML-SP-POST-to-OIDC-OP.t
+@@ -294,7 +294,9 @@
+                         oidcRPMetaDataOptionsBypassConsent     => 0,
+                         oidcRPMetaDataOptionsClientSecret      => "rpsecret",
+                         oidcRPMetaDataOptionsUserIDAttr        => "",
+-                        oidcRPMetaDataOptionsAccessTokenExpiration => 3600
++                        oidcRPMetaDataOptionsAccessTokenExpiration => 3600,
++                        oidcRPMetaDataOptionsRedirectUris =>
++                          'http://auth.proxy.com?openidconnectcallback=1',
+                     }
+                 },
+                 oidcOPMetaDataOptions           => {},
diff --git a/debian/patches/fix-open-redirection.patch b/debian/patches/fix-open-redirection.patch
new file mode 100644
index 000000000..db9db737a
--- /dev/null
+++ b/debian/patches/fix-open-redirection.patch
@@ -0,0 +1,262 @@
+Description: fix open redirection
+Author: Yadd <yadd at debian.org>
+ Maxime Besson <maxime.besson at worteks.com>
+Origin: upstream, https://gitlab.ow2.org/lemonldap-ng/lemonldap-ng/-/merge_requests/342/diffs
+Bug: https://gitlab.ow2.org/lemonldap-ng/lemonldap-ng/-/issues/2931
+Forwarded: not-needed
+Applied-Upstream: 2.17.0, https://gitlab.ow2.org/lemonldap-ng/lemonldap-ng/-/merge_requests/342
+Last-Update: 2023-09-20
+
+--- a/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/ApacheMP2/Main.pm
++++ b/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/ApacheMP2/Main.pm
+@@ -16,6 +16,7 @@
+ use APR::Table;
+ use Apache2::Const -compile =>
+   qw(FORBIDDEN HTTP_UNAUTHORIZED REDIRECT OK DECLINED DONE SERVER_ERROR AUTH_REQUIRED HTTP_SERVICE_UNAVAILABLE);
++use URI;
+ use base 'Lemonldap::NG::Handler::Main';
+ 
+ use constant FORBIDDEN         => Apache2::Const::FORBIDDEN;
+@@ -166,7 +167,7 @@
+         $f->r->status( $class->REDIRECT );
+         $f->r->status_line("303 See Other");
+         $f->r->headers_out->unset('Location');
+-        $f->r->err_headers_out->set( 'Location' => $url );
++        $f->r->err_headers_out->set( 'Location' => URI->new($url)->as_string );
+         $f->ctx(1);
+     }
+     while ( $f->read( my $buffer, 1024 ) ) {
+--- a/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/Main/Run.pm
++++ b/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/Main/Run.pm
+@@ -9,6 +9,7 @@
+ 
+ #use AutoLoader 'AUTOLOAD';
+ use MIME::Base64;
++use URI;
+ use URI::Escape;
+ use Lemonldap::NG::Common::Session;
+ 
+@@ -697,7 +698,7 @@
+     ) ? '' : ":$portString";
+     my $url = "http" . ( $_https ? "s" : "" ) . "://$realvhost$portString$s";
+     $class->logger->debug("Build URL $url");
+-    return $url;
++    return URI->new($url)->as_string;
+ }
+ 
+ ## @rmethod protected int isUnprotected()
+--- a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/CDC.pm
++++ b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/CDC.pm
+@@ -9,6 +9,7 @@
+ use Mouse;
+ use MIME::Base64;
+ use Lemonldap::NG::Common::FormEncode;
++use URI;
+ 
+ our $VERSION = '2.0.6';
+ 
+@@ -163,7 +164,10 @@
+         );
+ 
+         # Redirect
+-        return [ 302, [ Location => $urldc, $req->spliceHdrs ], [] ];
++        return [
++            302, [ Location => URI->new($urldc)->as_string, $req->spliceHdrs ],
++            []
++        ];
+ 
+     }
+ 
+--- a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Issuer/CAS.pm
++++ b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Issuer/CAS.pm
+@@ -13,6 +13,7 @@
+   PE_BADURL
+   PE_SENDRESPONSE
+ );
++use URI;
+ 
+ our $VERSION = '2.0.9';
+ 
+@@ -84,7 +85,8 @@
+     if ( $gateway and $gateway eq "true" ) {
+         $self->logger->debug(
+             "Gateway mode requested, redirect without authentication");
+-        $req->response( [ 302, [ Location => $service ], [] ] );
++        $req->response(
++            [ 302, [ Location => URI->new($service)->as_string ], [] ] );
+         for my $s ( $self->ipath, $self->ipath . 'Path' ) {
+             $self->logger->debug("Removing $s from pdata")
+               if delete $req->pdata->{$s};
+--- a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/OpenIDConnect.pm
++++ b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/OpenIDConnect.pm
+@@ -16,6 +16,7 @@
+ use Lemonldap::NG::Common::UserAgent;
+ use MIME::Base64 qw/encode_base64 decode_base64/;
+ use Mouse;
++use URI;
+ 
+ use Lemonldap::NG::Portal::Main::Constants qw(PE_OK PE_REDIRECT);
+ 
+@@ -1684,7 +1685,7 @@
+         $response_url .= build_urlencoded( state => $state );
+     }
+ 
+-    return $response_url;
++    return URI->new($response_url)->as_string;
+ }
+ 
+ # Create session_state parameter
+--- a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/SAML.pm
++++ b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/SAML.pm
+@@ -2493,7 +2493,7 @@
+ 
+         # Redirect user to response URL
+         my $slo_url = $logout->msg_url;
+-        return [ 302, [ Location => $slo_url ], [] ];
++        return [ 302, [ Location => URI->new($slo_url)->as_string ], [] ];
+     }
+ 
+     # HTTP-POST
+--- a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Process.pm
++++ b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Process.pm
+@@ -132,6 +132,7 @@
+                 $req->{urldc} =~ s/[\r\n]//sg;
+             }
+         }
++        $req->{urldc} = URI->new( $req->{urldc} )->as_string;
+ 
+         # For logout request, test if Referer comes from an authorized site
+         my $tmp = (
+--- a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Run.pm
++++ b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Run.pm
+@@ -402,7 +402,13 @@
+                 $self->logger->info("Force cleaning pdata");
+                 delete $req->{pdata}->{_url};
+             }
+-            return [ 302, [ Location => $req->{urldc}, $req->spliceHdrs ], [] ];
++            return [
++                302,
++                [
++                    Location => URI->new( $req->{urldc} )->as_string,
++                ],
++                []
++            ];
+         }
+     }
+     my ( $tpl, $prms ) = $self->display($req);
+--- a/lemonldap-ng-portal/t/03-XSS-protection.t
++++ b/lemonldap-ng-portal/t/03-XSS-protection.t
+@@ -19,21 +19,25 @@
+     '' => 0, 'Empty',
+ 
+     # 2 http://test1.example.com/
+-    'aHR0cDovL3Rlc3QxLmV4YW1wbGUuY29tLw==' => 1, 'Protected virtual host',
++    'aHR0cDovL3Rlc3QxLmV4YW1wbGUuY29tLw==' => 'http://test1.example.com/',
++    'Protected virtual host',
+ 
+     # 3 http://test1.example.com
+-    'aHR0cDovL3Rlc3QxLmV4YW1wbGUuY29t' => 1, 'Missing / in URL',
++    'aHR0cDovL3Rlc3QxLmV4YW1wbGUuY29t' => 'http://test1.example.com',
++    'Missing / in URL',
+ 
+     # 4 http://test1.example.com:8000/test
+-    'aHR0cDovL3Rlc3QxLmV4YW1wbGUuY29tOjgwMDAvdGVzdA==' => 1,
++    'aHR0cDovL3Rlc3QxLmV4YW1wbGUuY29tOjgwMDAvdGVzdA==' =>
++      'http://test1.example.com:8000/test',
+     'Non default port',
+ 
+     # 5 http://test1.example.com:8000/
+-    'aHR0cDovL3Rlc3QxLmV4YW1wbGUuY29tOjgwMDAv' => 1,
++    'aHR0cDovL3Rlc3QxLmV4YW1wbGUuY29tOjgwMDAv' =>
++      'http://test1.example.com:8000/',
+     'Non default port with missing /',
+ 
+     # 6 http://t.example2.com/test
+-    'aHR0cDovL3QuZXhhbXBsZTIuY29tL3Rlc3Q=' => 1,
++    'aHR0cDovL3QuZXhhbXBsZTIuY29tL3Rlc3Q=' => 'http://t.example2.com/test',
+     'Undeclared virtual host in trusted domain',
+ 
+     # 7 http://testexample2.com/
+@@ -47,7 +51,7 @@
+       . ' "example3.com" is trusted, but domain "*.example3.com" not)',
+ 
+     # 9 http://example3.com/
+-    'aHR0cDovL2V4YW1wbGUzLmNvbS8K' => 1,
++    'aHR0cDovL2V4YW1wbGUzLmNvbS8K' => 'http://example3.com/',
+     'Undeclared virtual host with trusted domain name',
+ 
+     # 10 http://t.example.com/test
+@@ -85,6 +89,21 @@
+     'aHR0cHM6Ly90ZXN0MS5leGFtcGxlLmNvbTp0ZXN0QGhhY2tlci5jb20=' => 0,
+     'userinfo trick',
+ 
++    # 22 url=https://hacker.com\@@test1.example.com/
++    'aHR0cHM6Ly9oYWNrZXIuY29tXEBAdGVzdDEuZXhhbXBsZS5jb20v' =>
++      'https://hacker.com%5C@@test1.example.com/',
++    'Good reencoding (2931)',
++
++    # 23 url=https://hacker.com:\@@test1.example.com/
++    'aHR0cHM6Ly9oYWNrZXIuY29tOlxAQHRlc3QxLmV4YW1wbGUuY29tLw==' =>
++      'https://hacker.com:%5C@@test1.example.com/',
++    'Good reencoding (2931)',
++
++    # 24 url='https://hacker.com\anything@test1.example.com/'
++    'aHR0cHM6Ly9oYWNrZXIuY29tXGFueXRoaW5nQHRlc3QxLmV4YW1wbGUuY29tLw==' =>
++      'https://hacker.com%5Canything@test1.example.com/',
++    'Good reencoding (2931)',
++
+     # LOGOUT TESTS
+     'LOGOUT',
+ 
+@@ -95,7 +114,7 @@
+ 
+     # 19 url=http://www.toto.com/, good referer
+     'aHR0cDovL3d3dy50b3RvLmNvbS8=',
+-    'http://test1.example.com/' => 1,
++    'http://test1.example.com/' => 'http://www.toto.com/',
+     'Logout required by good site',
+ 
+     # 20 url=http://www?<script>, good referer
+@@ -130,10 +149,13 @@
+         ),
+         $detail
+     );
+-    ok( ( $res->[0] == ( $redir ? 302 : 200 ) ),
+-        ( $redir ? 'Get redirection' : 'Redirection dropped' ) )
+-      or explain( $res->[0], ( $redir ? 302 : 200 ) );
+-    count(2);
++    if ($redir) {
++        expectRedirection( $res, $redir );
++    }
++    else {
++        expectOK($res);
++    }
++    count(1);
+ }
+ 
+ while ( defined( my $url = shift(@tests) ) ) {
+@@ -151,9 +173,12 @@
+         ),
+         $detail
+     );
+-    ok( ( $res->[0] == ( $redir ? 302 : 200 ) ),
+-        ( $redir ? 'Get redirection' : 'Redirection dropped' ) )
+-      or explain( $res->[0], ( $redir ? 302 : 200 ) );
++    if ($redir) {
++        expectRedirection( $res, $redir );
++    }
++    else {
++        expectOK($res);
++    }
+     ok(
+         $res = $client->_post(
+             '/',
+@@ -164,7 +189,7 @@
+     );
+     expectOK($res);
+     $id = expectCookie($res);
+-    count(3);
++    count(2);
+ }
+ 
+ clean_sessions();
diff --git a/debian/patches/series b/debian/patches/series
index 8cd6b510b..a7803dae5 100644
--- a/debian/patches/series
+++ b/debian/patches/series
@@ -12,3 +12,6 @@ CVE-2021-40874.patch
 CVE-2022-37186.patch
 fix-url-validation-bypass.patch
 CVE-2023-28862.patch
+fix-open-redirection-without-OIDC-redirect-uris.patch
+fix-open-redirection.patch
+SSRF-issue.patch


More information about the pkg-perl-maintainers mailing list