diff --git a/.changeset/metal-rice-retire.md b/.changeset/metal-rice-retire.md new file mode 100644 index 0000000000000..0ed9fb757076e --- /dev/null +++ b/.changeset/metal-rice-retire.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Adds SAML redirect validation by matching request parameters and configured IdP SLO diff --git a/apps/meteor/app/meteor-accounts-saml/server/lib/SAML.ts b/apps/meteor/app/meteor-accounts-saml/server/lib/SAML.ts index 51a6f4aa2faf3..cac4a5ae0376d 100644 --- a/apps/meteor/app/meteor-accounts-saml/server/lib/SAML.ts +++ b/apps/meteor/app/meteor-accounts-saml/server/lib/SAML.ts @@ -74,7 +74,7 @@ export class SAML { case 'logout': return this.processLogoutAction(req, res, service); case 'sloRedirect': - return this.processSLORedirectAction(req, res); + return this.processSLORedirectAction(req, res, service); case 'authorize': return this.processAuthorizeAction(res, service, samlObject); case 'validate': @@ -384,18 +384,58 @@ export class SAML { await logOutUser(inResponseTo); } finally { res.writeHead(302, { - Location: req.query.RelayState, + Location: Meteor.absoluteUrl(), }); res.end(); } }); } - private static processSLORedirectAction(req: IIncomingMessage, res: ServerResponse): void { + private static processSLORedirectAction(req: IIncomingMessage, res: ServerResponse, service: IServiceProviderOptions): void { + const { idpSLORedirectURL } = service; + const userRedirect = req.query.redirect as string; + + if (!idpSLORedirectURL) { + res.writeHead(500); + res.end('SLO redirect not configured'); + return; + } + + if (!userRedirect || typeof userRedirect !== 'string') { + res.writeHead(400); + res.end('Missing redirect parameter'); + return; + } + + let configuredURL: URL; + let requestURL: URL; + + try { + configuredURL = new URL(idpSLORedirectURL); + requestURL = new URL(userRedirect); + } catch { + res.writeHead(400); + res.end('Invalid URL format'); + return; + } + + if (configuredURL.origin !== requestURL.origin) { + res.writeHead(403); + res.end('Unauthorized redirect origin'); + return; + } + + const normalizePath = (p: string): string => p.replace(/\/+$/, '') || '/'; + if (normalizePath(configuredURL.pathname) !== normalizePath(requestURL.pathname)) { + res.writeHead(403); + res.end('Unauthorized redirect path'); + return; + } + res.writeHead(302, { - // credentialToken here is the SAML LogOut Request that we'll send back to IDP - Location: req.query.redirect, + Location: requestURL.toString(), }); + res.end(); }