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