VYPR
Moderate severityNVD Advisory· Published Jun 19, 2020· Updated Aug 5, 2024

CVE-2017-18916

CVE-2017-18916

Description

Mattermost Server before 3.8.2, 3.7.5, and 3.6.7 fails to enforce permission checks on API endpoints, allowing integrations to bypass intended restrictions.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Mattermost Server before 3.8.2, 3.7.5, and 3.6.7 fails to enforce permission checks on API endpoints, allowing integrations to bypass intended restrictions.

Root

Cause

The vulnerability stems from missing authorization checks on certain API endpoints in Mattermost Server. The official description states that "API endpoint access control does not honor an integration permission restriction" [3]. This means that when an integration (e.g., a bot or webhook) attempts to access an API endpoint, the server does not validate whether that integration has the necessary permissions [3].

Exploitation

An attacker who can create or compromise an integration (or who already has a valid integration token) can call API endpoints that should be restricted to higher-privileged roles. No authentication bypass is required; the integration permission model is simply not enforced on those endpoints [1][2][3]. The attack surface includes any Mattermost instance with integrations enabled, and the issue affects versions before 3.8.2, 3.7.5, and 3.6.7 [3].

Impact

By exploiting this lack of permission enforcement, an attacker can perform actions that the integration should not be allowed to do, such as reading or modifying sensitive data, escalating privileges, or performing administrative actions. The exact capabilities depend on which endpoints are unprotected, but the vulnerability effectively allows an integration to act beyond its intended scope [3].

Mitigation

Mattermost released patched versions 3.8.2, 3.7.5, and 3.6.7 to address this issue. Administrators should upgrade to one of these versions or later. The fix ensures that API endpoint access control properly evaluates integration permissions [3]. There is no evidence that this CVE has been added to CISA's Known Exploited Vulnerabilities catalog as of publication.

AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
github.com/mattermost/mattermost-serverGo
< 3.6.7-0.20170420152529-0968e4079e0a3.6.7-0.20170420152529-0968e4079e0a
github.com/mattermost/mattermost-serverGo
>= 3.7.0, < 3.7.53.7.5
github.com/mattermost/mattermost-serverGo
>= 3.8.0, < 3.8.23.8.2

Affected products

3

Patches

3
fb325cc339eb

PLT-5900 Removed automatic configuration of Site URL (#6135)

https://github.com/mattermost/mattermostHarrison HealeyApr 20, 2017via ghsa
8 files changed · +76 11
  • api/context.go+1 6 modified
    @@ -142,12 +142,7 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
     		isTokenFromQueryString = true
     	}
     
    -	if utils.GetSiteURL() == "" {
    -		protocol := app.GetProtocol(r)
    -		c.SetSiteURL(protocol + "://" + r.Host)
    -	} else {
    -		c.SetSiteURL(utils.GetSiteURL())
    -	}
    +	c.SetSiteURL(utils.GetSiteURL())
     
     	w.Header().Set(model.HEADER_REQUEST_ID, c.RequestId)
     	w.Header().Set(model.HEADER_VERSION_ID, fmt.Sprintf("%v.%v.%v.%v", model.CurrentVersion, model.BuildNumber, utils.CfgHash, utils.IsLicensed))
    
  • config/config.json+1 1 modified
    @@ -1,6 +1,6 @@
     {
         "ServiceSettings": {
    -        "SiteURL": "",
    +        "SiteURL": "http://localhost:8065",
             "ListenAddress": ":8065",
             "ConnectionSecurity": "",
             "TLSCertFile": "",
    
  • Makefile+1 0 modified
    @@ -323,6 +323,7 @@ package: build build-client
     
     	@# Disable developer settings
     	sed -i'' -e 's|"ConsoleLevel": "DEBUG"|"ConsoleLevel": "INFO"|g' $(DIST_PATH)/config/config.json
    +	sed -i'' -e 's|"SiteURL": "http://localhost:8065"|"SiteURL": ""|g' $(DIST_PATH)/config/config.json
     
     	@# Reset email sending to original configuration
     	sed -i'' -e 's|"SendEmailNotifications": true,|"SendEmailNotifications": false,|g' $(DIST_PATH)/config/config.json
    
  • webapp/components/admin_console/admin_settings.jsx+8 0 modified
    @@ -69,6 +69,10 @@ export default class AdminSettings extends React.Component {
                     if (callback) {
                         callback();
                     }
    +
    +                if (this.handleSaved) {
    +                    this.handleSaved(config);
    +                }
                 },
                 (err) => {
                     this.setState({
    @@ -79,6 +83,10 @@ export default class AdminSettings extends React.Component {
                     if (callback) {
                         callback();
                     }
    +
    +                if (this.handleSaved) {
    +                    this.handleSaved(config);
    +                }
                 }
             );
         }
    
  • webapp/components/admin_console/configuration_settings.jsx+13 1 modified
    @@ -3,6 +3,8 @@
     
     import React from 'react';
     
    +import ErrorStore from 'stores/error_store.jsx';
    +
     import * as Utils from 'utils/utils.jsx';
     
     import AdminSettings from './admin_settings.jsx';
    @@ -21,6 +23,8 @@ export default class ConfigurationSettings extends AdminSettings {
     
             this.getConfigFromState = this.getConfigFromState.bind(this);
     
    +        this.handleSaved = this.handleSaved.bind(this);
    +
             this.renderSettings = this.renderSettings.bind(this);
         }
     
    @@ -62,6 +66,14 @@ export default class ConfigurationSettings extends AdminSettings {
             };
         }
     
    +    handleSaved(newConfig) {
    +        const lastError = ErrorStore.getLastError();
    +
    +        if (lastError && lastError.message === 'error_bar.site_url' && newConfig.ServiceSettings.SiteURL) {
    +            ErrorStore.clearLastError(true);
    +        }
    +    }
    +
         renderTitle() {
             return (
                 <h3>
    @@ -96,7 +108,7 @@ export default class ConfigurationSettings extends AdminSettings {
                         helpText={
                             <FormattedHTMLMessage
                                 id='admin.service.siteURLDescription'
    -                            defaultMessage='The URL, including port number and protocol, that users will use to access Mattermost. This field can be left blank unless you are configuring email batching in <b>Notifications > Email</b>. When blank, the URL is automatically configured based on incoming traffic.'
    +                            defaultMessage='The URL, including port number and protocol, that users will use to access Mattermost. This setting is required.'
                             />
                         }
                         value={this.state.siteURL}
    
  • webapp/components/error_bar.jsx+46 1 modified
    @@ -13,11 +13,13 @@ const StatTypes = Constants.StatTypes;
     
     import React from 'react';
     import {FormattedMessage, FormattedHTMLMessage} from 'react-intl';
    +import {Link} from 'react-router';
     
     const EXPIRING_ERROR = 'error_bar.expiring';
     const EXPIRED_ERROR = 'error_bar.expired';
     const PAST_GRACE_ERROR = 'error_bar.past_grace';
     const RENEWAL_LINK = 'https://licensing.mattermost.com/renew';
    +const SITE_URL_ERROR = 'error_bar.site_url';
     
     const BAR_DEVELOPER_TYPE = 'developer';
     const BAR_CRITICAL_TYPE = 'critical';
    @@ -38,7 +40,11 @@ export default class ErrorBar extends React.Component {
                 isSystemAdmin = Utils.isSystemAdmin(user.roles);
             }
     
    -        if (!ErrorStore.getIgnoreNotification() && global.window.mm_config.SendEmailNotifications === 'false') {
    +        const errorIgnored = ErrorStore.getIgnoreNotification();
    +
    +        if (!errorIgnored && isSystemAdmin && global.mm_config.SiteURL === '') {
    +            ErrorStore.storeLastError({notification: true, message: SITE_URL_ERROR});
    +        } else if (!errorIgnored && global.window.mm_config.SendEmailNotifications === 'false') {
                 ErrorStore.storeLastError({notification: true, message: Utils.localizeMessage('error_bar.preview_mode', 'Preview Mode: Email notifications have not been configured')});
             } else if (isLicensePastGracePeriod()) {
                 if (isSystemAdmin) {
    @@ -157,6 +163,45 @@ export default class ErrorBar extends React.Component {
                         defaultMessage='Enterprise license is expired and some features may be disabled. Please contact your System Administrator for details.'
                     />
                 );
    +        } else if (message === SITE_URL_ERROR) {
    +            let id;
    +            let defaultMessage;
    +            if (global.mm_config.EnableSignUpWithGitLab === 'true') {
    +                id = 'error_bar.site_url_gitlab';
    +                defaultMessage = '{docsLink} is now a required setting. Please configure it in the System Console or in gitlab.rb if you\'re using GitLab Mattermost.';
    +            } else {
    +                id = 'error_bar.site_url';
    +                defaultMessage = '{docsLink} is now a required setting. Please configure it in {link}.';
    +            }
    +
    +            message = (
    +                <FormattedMessage
    +                    id={id}
    +                    defaultMessage={defaultMessage}
    +                    values={{
    +                        docsLink: (
    +                            <a
    +                                href='https://docs.mattermost.com/administration/config-settings.html#site-url'
    +                                rel='noopener noreferrer'
    +                                target='_blank'
    +                            >
    +                                <FormattedMessage
    +                                    id='error_bar.site_url.docsLink'
    +                                    defaultMessage='Site URL'
    +                                />
    +                            </a>
    +                        ),
    +                        link: (
    +                            <Link to='/admin_console/general/configuration'>
    +                                <FormattedMessage
    +                                    id='error_bar.site_url.link'
    +                                    defaultMessage='the System Console'
    +                                />
    +                            </Link>
    +                        )
    +                    }}
    +                />
    +            );
             }
     
             return (
    
  • webapp/i18n/en.json+4 0 modified
    @@ -1291,6 +1291,10 @@
       "error_bar.expiring": "Enterprise license expires on {date}. <a href='{link}' target='_blank'>Please renew.</a>",
       "error_bar.past_grace": "Enterprise license is expired and some features may be disabled. Please contact your System Administrator for details.",
       "error_bar.preview_mode": "Preview Mode: Email notifications have not been configured",
    +  "error_bar.site_url": "{docsLink} is now a required setting. Please configure it in {link}.",
    +  "error_bar.site_url.docsLink": "Site URL",
    +  "error_bar.site_url.link": "the System Console",
    +  "error_bar.site_url_gitlab": "{docsLink} is now a required setting. Please configure it in the System Console or in gitlab.rb if you're using GitLab Mattermost.",
       "file_attachment.download": "Download",
       "file_info_preview.size": "Size ",
       "file_info_preview.type": "File type ",
    
  • webapp/stores/error_store.jsx+2 2 modified
    @@ -62,11 +62,11 @@ class ErrorStoreClass extends EventEmitter {
             BrowserStore.setGlobalItem('last_error_conn', count);
         }
     
    -    clearLastError() {
    +    clearLastError(force) {
             var lastError = this.getLastError();
     
             // preview message can only be cleared by clearNotificationError
    -        if (lastError && lastError.notification) {
    +        if (!force && lastError && lastError.notification) {
                 return;
             }
     
    
0968e4079e0a

PLT-5900 Removed automatic configuration of Site URL (3.6) (#6136)

https://github.com/mattermost/mattermostHarrison HealeyApr 20, 2017via ghsa
9 files changed · +79 11
  • api/context.go+1 6 modified
    @@ -153,12 +153,7 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
     		isTokenFromQueryString = true
     	}
     
    -	if *utils.Cfg.ServiceSettings.SiteURL != "" {
    -		c.SetSiteURL(*utils.Cfg.ServiceSettings.SiteURL)
    -	} else {
    -		protocol := GetProtocol(r)
    -		c.SetSiteURL(protocol + "://" + r.Host)
    -	}
    +	c.SetSiteURL(*utils.Cfg.ServiceSettings.SiteURL)
     
     	w.Header().Set(model.HEADER_REQUEST_ID, c.RequestId)
     	w.Header().Set(model.HEADER_VERSION_ID, fmt.Sprintf("%v.%v.%v.%v", model.CurrentVersion, model.BuildNumber, utils.ClientCfgHash, utils.IsLicensed))
    
  • config/config.json+1 1 modified
    @@ -1,6 +1,6 @@
     {
         "ServiceSettings": {
    -        "SiteURL": "",
    +        "SiteURL": "http://localhost:8065",
             "ListenAddress": ":8065",
             "ConnectionSecurity": "",
             "TLSCertFile": "",
    
  • Makefile+1 0 modified
    @@ -300,6 +300,7 @@ package: build build-client
     
     	@# Disable developer settings
     	sed -i'' -e 's|"ConsoleLevel": "DEBUG"|"ConsoleLevel": "INFO"|g' $(DIST_PATH)/config/config.json
    +	sed -i'' -e 's|"SiteURL": "http://localhost:8065"|"SiteURL": ""|g' $(DIST_PATH)/config/config.json
     
     	@# Package webapp
     	mkdir -p $(DIST_PATH)/webapp/dist
    
  • webapp/components/admin_console/admin_settings.jsx+8 0 modified
    @@ -68,6 +68,10 @@ export default class AdminSettings extends React.Component {
                     if (callback) {
                         callback();
                     }
    +
    +                if (this.handleSaved) {
    +                    this.handleSaved(config);
    +                }
                 },
                 (err) => {
                     this.setState({
    @@ -78,6 +82,10 @@ export default class AdminSettings extends React.Component {
                     if (callback) {
                         callback();
                     }
    +
    +                if (this.handleSaved) {
    +                    this.handleSaved(config);
    +                }
                 }
             );
         }
    
  • webapp/components/admin_console/configuration_settings.jsx+13 1 modified
    @@ -3,6 +3,8 @@
     
     import React from 'react';
     
    +import ErrorStore from 'stores/error_store.jsx';
    +
     import * as Utils from 'utils/utils.jsx';
     
     import AdminSettings from './admin_settings.jsx';
    @@ -21,6 +23,8 @@ export default class ConfigurationSettings extends AdminSettings {
     
             this.getConfigFromState = this.getConfigFromState.bind(this);
     
    +        this.handleSaved = this.handleSaved.bind(this);
    +
             this.renderSettings = this.renderSettings.bind(this);
         }
     
    @@ -62,6 +66,14 @@ export default class ConfigurationSettings extends AdminSettings {
             };
         }
     
    +    handleSaved(newConfig) {
    +        const lastError = ErrorStore.getLastError();
    +
    +        if (lastError && lastError.message === 'error_bar.site_url' && newConfig.ServiceSettings.SiteURL) {
    +            ErrorStore.clearLastError(true);
    +        }
    +    }
    +
         renderTitle() {
             return (
                 <h3>
    @@ -96,7 +108,7 @@ export default class ConfigurationSettings extends AdminSettings {
                         helpText={
                             <FormattedHTMLMessage
                                 id='admin.service.siteURLDescription'
    -                            defaultMessage='The URL, including port number and protocol, that users will use to access Mattermost. This field can be left blank unless you are configuring email batching in <b>Notifications > Email</b>. When blank, the URL is automatically configured based on incoming traffic.'
    +                            defaultMessage='The URL, including port number and protocol, that users will use to access Mattermost. This setting is required.'
                             />
                         }
                         value={this.state.siteURL}
    
  • webapp/components/error_bar.jsx+46 1 modified
    @@ -9,10 +9,12 @@ import {isLicenseExpiring, isLicenseExpired, isLicensePastGracePeriod, displayEx
     
     import React from 'react';
     import {FormattedMessage} from 'react-intl';
    +import {Link} from 'react-router';
     
     const EXPIRING_ERROR = 'error_bar.expiring';
     const EXPIRED_ERROR = 'error_bar.expired';
     const PAST_GRACE_ERROR = 'error_bar.past_grace';
    +const SITE_URL_ERROR = 'error_bar.site_url';
     
     export default class ErrorBar extends React.Component {
         constructor() {
    @@ -27,7 +29,11 @@ export default class ErrorBar extends React.Component {
                 isSystemAdmin = Utils.isSystemAdmin(user.roles);
             }
     
    -        if (!ErrorStore.getIgnoreNotification() && global.window.mm_config.SendEmailNotifications === 'false') {
    +        const errorIgnored = ErrorStore.getIgnoreNotification();
    +
    +        if (!errorIgnored && isSystemAdmin && global.mm_config.SiteURL === '') {
    +            ErrorStore.storeLastError({notification: true, message: SITE_URL_ERROR});
    +        } else if (!errorIgnored && global.window.mm_config.SendEmailNotifications === 'false') {
                 ErrorStore.storeLastError({notification: true, message: Utils.localizeMessage('error_bar.preview_mode', 'Preview Mode: Email notifications have not been configured')});
             } else if (isLicenseExpiring() && isSystemAdmin) {
                 ErrorStore.storeLastError({notification: true, message: EXPIRING_ERROR});
    @@ -120,6 +126,45 @@ export default class ErrorBar extends React.Component {
                         defaultMessage='Enterprise license has expired, please contact your System Administrator for details'
                     />
                 );
    +        } else if (message === SITE_URL_ERROR) {
    +            let id;
    +            let defaultMessage;
    +            if (global.mm_config.EnableSignUpWithGitLab === 'true') {
    +                id = 'error_bar.site_url_gitlab';
    +                defaultMessage = '{docsLink} is now a required setting. Please configure it in the System Console or in gitlab.rb if you\'re using GitLab Mattermost.';
    +            } else {
    +                id = 'error_bar.site_url';
    +                defaultMessage = '{docsLink} is now a required setting. Please configure it in {link}.';
    +            }
    +
    +            message = (
    +                <FormattedMessage
    +                    id={id}
    +                    defaultMessage={defaultMessage}
    +                    values={{
    +                        docsLink: (
    +                            <a
    +                                href='https://docs.mattermost.com/administration/config-settings.html#site-url'
    +                                rel='noopener noreferrer'
    +                                target='_blank'
    +                            >
    +                                <FormattedMessage
    +                                    id='error_bar.site_url.docsLink'
    +                                    defaultMessage='Site URL'
    +                                />
    +                            </a>
    +                        ),
    +                        link: (
    +                            <Link to='/admin_console/general/configuration'>
    +                                <FormattedMessage
    +                                    id='error_bar.site_url.link'
    +                                    defaultMessage='the System Console'
    +                                />
    +                            </Link>
    +                        )
    +                    }}
    +                />
    +            );
             }
     
             return (
    
  • webapp/i18n/en.json+4 0 modified
    @@ -1255,6 +1255,10 @@
       "error_bar.expiring": "The Enterprise license is expiring on {date}. To renew your license, please contact commercial@mattermost.com",
       "error_bar.past_grace": "Enterprise license has expired, please contact your System Administrator for details",
       "error_bar.preview_mode": "Preview Mode: Email notifications have not been configured",
    +  "error_bar.site_url": "{docsLink} is now a required setting. Please configure it in {link}.",
    +  "error_bar.site_url.docsLink": "Site URL",
    +  "error_bar.site_url.link": "the System Console",
    +  "error_bar.site_url_gitlab": "{docsLink} is now a required setting. Please configure it in the System Console or in gitlab.rb if you're using GitLab Mattermost.",
       "file_attachment.download": "Download",
       "file_info_preview.size": "Size ",
       "file_info_preview.type": "File type ",
    
  • webapp/sass/components/_error-bar.scss+3 0 modified
    @@ -11,6 +11,9 @@
         z-index: 9999;
     
         a {
    +        color: $white !important;
    +        text-decoration: underline;
    +
             &.error-bar__close {
                 color: $white;
                 font-family: 'Open Sans', sans-serif;
    
  • webapp/stores/error_store.jsx+2 2 modified
    @@ -62,11 +62,11 @@ class ErrorStoreClass extends EventEmitter {
             BrowserStore.setGlobalItem('last_error_conn', count);
         }
     
    -    clearLastError() {
    +    clearLastError(force) {
             var lastError = this.getLastError();
     
             // preview message can only be cleared by clearNotificationError
    -        if (lastError && lastError.notification) {
    +        if (!force && lastError && lastError.notification) {
                 return;
             }
     
    
b74e85653660

Invite salt fix for 3.8 (#6149)

https://github.com/mattermost/mattermostChristopher SpellerApr 18, 2017via ghsa
10 files changed · +43 34
  • api4/team_test.go+3 3 modified
    @@ -876,7 +876,7 @@ func TestAddTeamMember(t *testing.T) {
     	dataObject["id"] = team.Id
     
     	data := model.MapToJson(dataObject)
    -	hashed := model.HashPassword(fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt))
    +	hashed := model.HashSha256(fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt))
     
     	tm, resp = Client.AddTeamMember(team.Id, "", hashed, data, "")
     	CheckNoError(t, resp)
    @@ -906,15 +906,15 @@ func TestAddTeamMember(t *testing.T) {
     	// expired data of more than 50 hours
     	dataObject["time"] = fmt.Sprintf("%v", model.GetMillis()-1000*60*60*50)
     	data = model.MapToJson(dataObject)
    -	hashed = model.HashPassword(fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt))
    +	hashed = model.HashSha256(fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt))
     
     	tm, resp = Client.AddTeamMember(team.Id, "", hashed, data, "")
     	CheckNotFoundStatus(t, resp)
     
     	// invalid team id
     	dataObject["id"] = GenerateTestId()
     	data = model.MapToJson(dataObject)
    -	hashed = model.HashPassword(fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt))
    +	hashed = model.HashSha256(fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt))
     
     	tm, resp = Client.AddTeamMember(team.Id, "", hashed, data, "")
     	CheckNotFoundStatus(t, resp)
    
  • api4/user.go+2 2 modified
    @@ -941,8 +941,8 @@ func verifyUserEmail(c *Context, w http.ResponseWriter, r *http.Request) {
     		return
     	}
     
    -	hashed := model.HashPassword(hashedId)
    -	if model.ComparePassword(hashed, userId+utils.Cfg.EmailSettings.InviteSalt) {
    +	hashed := model.HashSha256(hashedId)
    +	if hashed == model.HashSha256(userId+utils.Cfg.EmailSettings.InviteSalt) {
     		if c.Err = app.VerifyUserEmail(userId); c.Err != nil {
     			return
     		} else {
    
  • api/oauth.go+6 5 modified
    @@ -200,7 +200,7 @@ func allowOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
     	}
     
     	authData := &model.AuthData{UserId: c.Session.UserId, ClientId: clientId, CreateAt: model.GetMillis(), RedirectUri: redirectUri, State: state, Scope: scope}
    -	authData.Code = model.HashPassword(fmt.Sprintf("%v:%v:%v:%v", clientId, redirectUri, authData.CreateAt, c.Session.UserId))
    +	authData.Code = model.HashSha256(fmt.Sprintf("%v:%v:%v:%v", clientId, redirectUri, authData.CreateAt, c.Session.UserId))
     
     	// this saves the OAuth2 app as authorized
     	authorizedApp := model.Preference{
    @@ -501,7 +501,7 @@ func getAccessToken(c *Context, w http.ResponseWriter, r *http.Request) {
     			return
     		}
     
    -		if !model.ComparePassword(code, fmt.Sprintf("%v:%v:%v:%v", clientId, redirectUri, authData.CreateAt, authData.UserId)) {
    +		if code != model.HashSha256(fmt.Sprintf("%v:%v:%v:%v", clientId, redirectUri, authData.CreateAt, authData.UserId)) {
     			c.LogAudit("fail - auth code is invalid")
     			c.Err = model.NewLocAppError("getAccessToken", "api.oauth.get_access_token.expired_code.app_error", nil, "")
     			return
    @@ -565,6 +565,7 @@ func getAccessToken(c *Context, w http.ResponseWriter, r *http.Request) {
     		<-app.Srv.Store.OAuth().RemoveAuthData(authData.Code)
     	} else {
     		// when grantType is refresh_token
    +		fmt.Printf(refreshToken)
     		if result := <-app.Srv.Store.OAuth().GetAccessDataByRefreshToken(refreshToken); result.Err != nil {
     			c.LogAudit("fail - refresh token is invalid")
     			c.Err = model.NewLocAppError("getAccessToken", "api.oauth.get_access_token.refresh_token.app_error", nil, "")
    @@ -636,7 +637,7 @@ func getTeamIdFromQuery(query url.Values) (string, *model.AppError) {
     		data := query.Get("d")
     		props := model.MapFromJson(strings.NewReader(data))
     
    -		if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) {
    +		if hash != model.HashSha256(fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) {
     			return "", model.NewLocAppError("getTeamIdFromQuery", "api.oauth.singup_with_oauth.invalid_link.app_error", nil, "")
     		}
     
    @@ -699,7 +700,7 @@ func GetAuthorizationCode(c *Context, service string, props map[string]string, l
     	endpoint := sso.AuthEndpoint
     	scope := sso.Scope
     
    -	props["hash"] = model.HashPassword(clientId)
    +	props["hash"] = model.HashSha256(clientId)
     	state := b64.StdEncoding.EncodeToString([]byte(model.MapToJson(props)))
     
     	redirectUri := c.GetSiteURLHeader() + "/signup/" + service + "/complete"
    @@ -732,7 +733,7 @@ func AuthorizeOAuthUser(service, code, state, redirectUri string) (io.ReadCloser
     
     	stateProps := model.MapFromJson(strings.NewReader(stateStr))
     
    -	if !model.ComparePassword(stateProps["hash"], sso.Id) {
    +	if stateProps["hash"] != model.HashSha256(sso.Id) {
     		return nil, "", nil, model.NewLocAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.invalid_state.app_error", nil, "")
     	}
     
    
  • api/oauth_test.go+9 8 modified
    @@ -5,16 +5,17 @@ package api
     
     import (
     	"encoding/base64"
    -	"github.com/mattermost/platform/app"
    -	"github.com/mattermost/platform/einterfaces"
    -	"github.com/mattermost/platform/model"
    -	"github.com/mattermost/platform/utils"
     	"io"
     	"io/ioutil"
     	"net/http"
     	"net/url"
     	"strings"
     	"testing"
    +
    +	"github.com/mattermost/platform/app"
    +	"github.com/mattermost/platform/einterfaces"
    +	"github.com/mattermost/platform/model"
    +	"github.com/mattermost/platform/utils"
     )
     
     func TestOAuthRegisterApp(t *testing.T) {
    @@ -491,10 +492,10 @@ func TestOAuthAuthorize(t *testing.T) {
     	}
     
     	authToken := Client.AuthType + " " + Client.AuthToken
    -	if r, err := HttpGet(Client.Url+"/oauth/authorize?client_id="+oauthApp.Id+"&&redirect_uri=http://example.com&response_type="+model.AUTHCODE_RESPONSE_TYPE, Client.HttpClient, authToken, true); err != nil {
    +	/*if r, err := HttpGet(Client.Url+"/oauth/authorize?client_id="+oauthApp.Id+"&&redirect_uri=http://example.com&response_type="+model.AUTHCODE_RESPONSE_TYPE, Client.HttpClient, authToken, true); err != nil {
     		t.Fatal(err)
     		closeBody(r)
    -	}
    +	}*/
     
     	// lets authorize the app
     	if _, err := Client.AllowOAuth(model.AUTHCODE_RESPONSE_TYPE, oauthApp.Id, oauthApp.CallbackUrls[0], "user", ""); err != nil {
    @@ -711,7 +712,7 @@ func TestOAuthComplete(t *testing.T) {
     		closeBody(r)
     	}
     
    -	stateProps["hash"] = model.HashPassword(utils.Cfg.GitLabSettings.Id)
    +	stateProps["hash"] = model.HashSha256(utils.Cfg.GitLabSettings.Id)
     	state = base64.StdEncoding.EncodeToString([]byte(model.MapToJson(stateProps)))
     	if r, err := HttpGet(Client.Url+"/login/gitlab/complete?code=123&state="+url.QueryEscape(state), Client.HttpClient, "", true); err == nil {
     		t.Fatal("should have failed - no connection")
    @@ -747,7 +748,7 @@ func TestOAuthComplete(t *testing.T) {
     	stateProps["action"] = model.OAUTH_ACTION_EMAIL_TO_SSO
     	delete(stateProps, "team_id")
     	stateProps["redirect_to"] = utils.Cfg.GitLabSettings.AuthEndpoint
    -	stateProps["hash"] = model.HashPassword(utils.Cfg.GitLabSettings.Id)
    +	stateProps["hash"] = model.HashSha256(utils.Cfg.GitLabSettings.Id)
     	stateProps["redirect_to"] = "/oauth/authorize"
     	state = base64.StdEncoding.EncodeToString([]byte(model.MapToJson(stateProps)))
     	if r, err := HttpGet(Client.Url+"/login/"+model.SERVICE_GITLAB+"/complete?code="+url.QueryEscape(code)+"&state="+url.QueryEscape(state), Client.HttpClient, "", false); err == nil {
    
  • api/user.go+1 1 modified
    @@ -1197,7 +1197,7 @@ func verifyEmail(c *Context, w http.ResponseWriter, r *http.Request) {
     		return
     	}
     
    -	if model.ComparePassword(hashedId, userId+utils.Cfg.EmailSettings.InviteSalt) {
    +	if hashedId == model.HashSha256(userId+utils.Cfg.EmailSettings.InviteSalt) {
     		if c.Err = app.VerifyUserEmail(userId); c.Err != nil {
     			return
     		} else {
    
  • api/user_test.go+1 1 modified
    @@ -184,7 +184,7 @@ func TestLogin(t *testing.T) {
     	props["display_name"] = rteam2.Data.(*model.Team).DisplayName
     	props["time"] = fmt.Sprintf("%v", model.GetMillis())
     	data := model.MapToJson(props)
    -	hash := model.HashPassword(fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt))
    +	hash := model.HashSha256(fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt))
     
     	ruser2, err := Client.CreateUserFromSignup(&user2, data, hash)
     	if err != nil {
    
  • app/email.go+11 12 modified
    @@ -18,7 +18,7 @@ func SendChangeUsernameEmail(oldUsername, newUsername, email, locale, siteURL st
     
     	subject := T("api.templates.username_change_subject",
     		map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"],
    -		"TeamDisplayName": utils.Cfg.TeamSettings.SiteName})
    +			"TeamDisplayName": utils.Cfg.TeamSettings.SiteName})
     
     	bodyPage := utils.NewHTMLTemplate("email_change_body", locale)
     	bodyPage.Props["SiteURL"] = siteURL
    @@ -36,12 +36,11 @@ func SendChangeUsernameEmail(oldUsername, newUsername, email, locale, siteURL st
     func SendEmailChangeVerifyEmail(userId, newUserEmail, locale, siteURL string) *model.AppError {
     	T := utils.GetUserTranslations(locale)
     
    -	link := fmt.Sprintf("%s/do_verify_email?uid=%s&hid=%s&email=%s", siteURL, userId, model.HashPassword(userId+utils.Cfg.EmailSettings.InviteSalt), url.QueryEscape(newUserEmail))
    +	link := fmt.Sprintf("%s/do_verify_email?uid=%s&hid=%s&email=%s", siteURL, userId, model.HashSha256(userId+utils.Cfg.EmailSettings.InviteSalt), url.QueryEscape(newUserEmail))
     
     	subject := T("api.templates.email_change_verify_subject",
     		map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"],
    -		"TeamDisplayName": utils.Cfg.TeamSettings.SiteName})
    -
    +			"TeamDisplayName": utils.Cfg.TeamSettings.SiteName})
     
     	bodyPage := utils.NewHTMLTemplate("email_change_verify_body", locale)
     	bodyPage.Props["SiteURL"] = siteURL
    @@ -63,7 +62,7 @@ func SendEmailChangeEmail(oldEmail, newEmail, locale, siteURL string) *model.App
     
     	subject := T("api.templates.email_change_subject",
     		map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"],
    -		"TeamDisplayName": utils.Cfg.TeamSettings.SiteName})
    +			"TeamDisplayName": utils.Cfg.TeamSettings.SiteName})
     
     	bodyPage := utils.NewHTMLTemplate("email_change_body", locale)
     	bodyPage.Props["SiteURL"] = siteURL
    @@ -81,7 +80,7 @@ func SendEmailChangeEmail(oldEmail, newEmail, locale, siteURL string) *model.App
     func SendVerifyEmail(userId, userEmail, locale, siteURL string) *model.AppError {
     	T := utils.GetUserTranslations(locale)
     
    -	link := fmt.Sprintf("%s/do_verify_email?uid=%s&hid=%s&email=%s", siteURL, userId, model.HashPassword(userId+utils.Cfg.EmailSettings.InviteSalt), url.QueryEscape(userEmail))
    +	link := fmt.Sprintf("%s/do_verify_email?uid=%s&hid=%s&email=%s", siteURL, userId, model.HashSha256(userId+utils.Cfg.EmailSettings.InviteSalt), url.QueryEscape(userEmail))
     
     	url, _ := url.Parse(siteURL)
     
    @@ -128,7 +127,7 @@ func SendWelcomeEmail(userId string, email string, verified bool, locale, siteUR
     
     	subject := T("api.templates.welcome_subject",
     		map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"],
    -		"ServerURL": rawUrl.Host})
    +			"ServerURL": rawUrl.Host})
     
     	bodyPage := utils.NewHTMLTemplate("welcome_body", locale)
     	bodyPage.Props["SiteURL"] = siteURL
    @@ -145,7 +144,7 @@ func SendWelcomeEmail(userId string, email string, verified bool, locale, siteUR
     	}
     
     	if !verified {
    -		link := fmt.Sprintf("%s/do_verify_email?uid=%s&hid=%s&email=%s", siteURL, userId, model.HashPassword(userId+utils.Cfg.EmailSettings.InviteSalt), url.QueryEscape(email))
    +		link := fmt.Sprintf("%s/do_verify_email?uid=%s&hid=%s&email=%s", siteURL, userId, model.HashSha256(userId+utils.Cfg.EmailSettings.InviteSalt), url.QueryEscape(email))
     		bodyPage.Props["VerifyUrl"] = link
     	}
     
    @@ -161,7 +160,7 @@ func SendPasswordChangeEmail(email, method, locale, siteURL string) *model.AppEr
     
     	subject := T("api.templates.password_change_subject",
     		map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"],
    -		"TeamDisplayName": utils.Cfg.TeamSettings.SiteName})
    +			"TeamDisplayName": utils.Cfg.TeamSettings.SiteName})
     
     	bodyPage := utils.NewHTMLTemplate("password_change_body", locale)
     	bodyPage.Props["SiteURL"] = siteURL
    @@ -234,8 +233,8 @@ func SendInviteEmails(team *model.Team, senderName string, invites []string, sit
     
     			subject := utils.T("api.templates.invite_subject",
     				map[string]interface{}{"SenderName": senderName,
    -				"TeamDisplayName": team.DisplayName,
    -				"SiteName": utils.ClientCfg["SiteName"]})
    +					"TeamDisplayName": team.DisplayName,
    +					"SiteName":        utils.ClientCfg["SiteName"]})
     
     			bodyPage := utils.NewHTMLTemplate("invite_body", model.DEFAULT_LOCALE)
     			bodyPage.Props["SiteURL"] = siteURL
    @@ -253,7 +252,7 @@ func SendInviteEmails(team *model.Team, senderName string, invites []string, sit
     			props["name"] = team.Name
     			props["time"] = fmt.Sprintf("%v", model.GetMillis())
     			data := model.MapToJson(props)
    -			hash := model.HashPassword(fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt))
    +			hash := model.HashSha256(fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt))
     			bodyPage.Props["Link"] = fmt.Sprintf("%s/signup_user_complete/?d=%s&h=%s", siteURL, url.QueryEscape(data), url.QueryEscape(hash))
     
     			if !utils.Cfg.EmailSettings.SendEmailNotifications {
    
  • app/team.go+1 1 modified
    @@ -197,7 +197,7 @@ func AddUserToTeamByTeamId(teamId string, user *model.User) *model.AppError {
     func AddUserToTeamByHash(userId string, hash string, data string) (*model.Team, *model.AppError) {
     	props := model.MapFromJson(strings.NewReader(data))
     
    -	if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) {
    +	if hash != model.HashSha256(fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) {
     		return nil, model.NewLocAppError("JoinUserToTeamByHash", "api.user.create_user.signup_link_invalid.app_error", nil, "")
     	}
     
    
  • app/user.go+1 1 modified
    @@ -37,7 +37,7 @@ func CreateUserWithHash(user *model.User, hash string, data string) (*model.User
     
     	props := model.MapFromJson(strings.NewReader(data))
     
    -	if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) {
    +	if hash != model.HashSha256(fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) {
     		return nil, model.NewLocAppError("CreateUserWithHash", "api.user.create_user.signup_link_invalid.app_error", nil, "")
     	}
     
    
  • model/user.go+8 0 modified
    @@ -4,6 +4,7 @@
     package model
     
     import (
    +	"crypto/sha256"
     	"encoding/json"
     	"fmt"
     	"io"
    @@ -535,6 +536,13 @@ func UserListFromJson(data io.Reader) []*User {
     	}
     }
     
    +func HashSha256(text string) string {
    +	hash := sha256.New()
    +	hash.Write([]byte(text))
    +
    +	return fmt.Sprintf("%x", hash.Sum(nil))
    +}
    +
     // HashPassword generates a hash using the bcrypt.GenerateFromPassword
     func HashPassword(password string) string {
     	hash, err := bcrypt.GenerateFromPassword([]byte(password), 10)
    

Vulnerability mechanics

Root cause

"The application incorrectly derived the `SiteURL` from the incoming request's `Host` header when the configuration was unset, allowing for potential manipulation."

Attack vector

An attacker could exploit this by manipulating the `SiteURL` context, which was previously derived from the incoming HTTP request host when the configuration was blank. By sending requests with a crafted `Host` header, an attacker could influence the application's perception of its own base URL. This could potentially lead to unauthorized access or bypasses related to integration permissions that rely on a correctly configured `SiteURL`. The advisory does not specify a CWE ID for this issue.

Affected code

The vulnerability exists in `api/context.go` where the application context handles the `SiteURL` configuration. Specifically, the logic that previously attempted to dynamically determine the `SiteURL` from the request host has been removed in favor of using the configured `SiteURL` [patch_id=21882, patch_id=21880].

What the fix does

The patch removes the logic that dynamically set the `SiteURL` based on the incoming request's host header when the server configuration was empty [patch_id=21882, patch_id=21880]. Instead, the application now strictly uses the `SiteURL` defined in the server configuration. This prevents attackers from influencing the `SiteURL` via the `Host` header, ensuring that integration permissions and other security-sensitive features operate against a trusted, static base URL. Additionally, the patch introduces a mandatory configuration requirement for `SiteURL` to ensure administrators explicitly set this value.

Preconditions

  • configThe server must have an empty 'SiteURL' configuration, allowing the application to fall back to dynamic derivation from the request.

Generated on May 11, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

7

News mentions

0

No linked articles in our index yet.