Package: release.debian.org Severity: normal Tags: bullseye User: release.debian....@packages.debian.org Usertags: pu X-Debbugs-Cc: lemonldap...@packages.debian.org, y...@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
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 <y...@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 <y...@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 <y...@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.bes...@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 <y...@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 <y...@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 <y...@debian.org> + Maxime Besson <maxime.bes...@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\anyth...@test1.example.com/' ++ 'aHR0cHM6Ly9oYWNrZXIuY29tXGFueXRoaW5nQHRlc3QxLmV4YW1wbGUuY29tLw==' => ++ 'https://hacker.com%5canyth...@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