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.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/mattermost/mattermost-serverGo | < 3.6.7-0.20170420152529-0968e4079e0a | 3.6.7-0.20170420152529-0968e4079e0a |
github.com/mattermost/mattermost-serverGo | >= 3.7.0, < 3.7.5 | 3.7.5 |
github.com/mattermost/mattermost-serverGo | >= 3.8.0, < 3.8.2 | 3.8.2 |
Affected products
3- Mattermost/Serverdescription
- ghsa-coords2 versionspkg:golang/github.com/mattermost/mattermost-serverpkg:rpm/opensuse/govulncheck-vulndb&distro=openSUSE%20Leap%2015.6
< 3.6.7-0.20170420152529-0968e4079e0a+ 1 more
- (no CPE)range: < 3.6.7-0.20170420152529-0968e4079e0a
- (no CPE)range: < 0.0.20260226T182644-150000.1.149.1
Patches
3fb325cc339ebPLT-5900 Removed automatic configuration of Site URL (#6135)
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; }
0968e4079e0aPLT-5900 Removed automatic configuration of Site URL (3.6) (#6136)
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; }
b74e85653660Invite salt fix for 3.8 (#6149)
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- github.com/advisories/GHSA-x33g-375j-jhf7ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2017-18916ghsaADVISORY
- github.com/mattermost/mattermost/commit/0968e4079e0aa670254f3fe3a7248d126e3cf877ghsaWEB
- github.com/mattermost/mattermost/commit/b74e85653660525d351d090a1e1874ae933bcbc8ghsaWEB
- github.com/mattermost/mattermost/commit/fb325cc339eb8d8efb60dbadc48fd38897201c6fghsaWEB
- mattermost.com/security-updatesghsaWEB
- mattermost.com/security-updates/mitrex_refsource_CONFIRM
News mentions
0No linked articles in our index yet.