From 3319aec473a63e48f40743c6ea39e73f55dd9bc8 Mon Sep 17 00:00:00 2001 From: Felipe Martin <812088+fmartingr@users.noreply.github.com> Date: Tue, 17 Mar 2026 12:07:07 +0100 Subject: [PATCH] Fix case-insensitive pre-auth header check in doPing (#9577) * Fix case-insensitive pre-auth header check in doPing HTTP headers are case-insensitive per RFC 7230, but the pre-auth rejection check accessed x-reject-reason using an exact lowercase key. Servers/proxies may return varying cases (e.g. X-Reject-Reason). Add a getResponseHeader utility for case-insensitive lookup and use it in both pre-auth check paths. * Address PR feedback: optimize getResponseHeader and fix test description Try exact match and lowercase match before falling back to a full scan to avoid array allocation on every call. Fix test description to say "uppercase" instead of "mixed case" to match the test data. --- app/actions/remote/general.ts | 6 ++++-- app/client/rest/constants.ts | 1 + app/utils/headers.test.ts | 30 ++++++++++++++++++++++++++++++ app/utils/headers.ts | 23 +++++++++++++++++++++++ 4 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 app/utils/headers.test.ts create mode 100644 app/utils/headers.ts diff --git a/app/actions/remote/general.ts b/app/actions/remote/general.ts index dc15aa859..598a45e62 100644 --- a/app/actions/remote/general.ts +++ b/app/actions/remote/general.ts @@ -3,6 +3,7 @@ import {defineMessage} from 'react-intl'; +import * as ClientConstants from '@client/rest/constants'; import {SYSTEM_IDENTIFIERS} from '@constants/database'; import {PUSH_PROXY_RESPONSE_VERIFIED, PUSH_PROXY_STATUS_VERIFIED} from '@constants/push_proxy'; import DatabaseManager from '@database/manager'; @@ -10,6 +11,7 @@ import NetworkManager from '@managers/network_manager'; import {getDeviceToken} from '@queries/app/global'; import {getExpandedLinks, getPushVerificationStatus} from '@queries/servers/system'; import {getFullErrorMessage} from '@utils/errors'; +import {getResponseHeader} from '@utils/headers'; import {logDebug} from '@utils/log'; import {forceLogoutIfNecessary} from './session'; @@ -75,7 +77,7 @@ export const doPing = async (serverUrl: string, verifyPushProxy: boolean, timeou if (!client) { NetworkManager.invalidateClient(serverUrl); } - if (response.code === 403 && response.headers?.['x-reject-reason'] === 'pre-auth') { + if (response.code === 403 && getResponseHeader(response.headers, ClientConstants.HEADER_X_REJECT_REASON) === 'pre-auth') { return {error: {intl: pingError}, isPreauthError: true}; } return {error: {intl: pingError}}; @@ -84,7 +86,7 @@ export const doPing = async (serverUrl: string, verifyPushProxy: boolean, timeou // Check if this is a 403 with pre-auth header const errorObj = error as ClientError; if (errorObj.status_code === 403) { - if (errorObj.headers?.['x-reject-reason'] === 'pre-auth') { + if (getResponseHeader(errorObj.headers, ClientConstants.HEADER_X_REJECT_REASON) === 'pre-auth') { return {error: {intl: pingError}, isPreauthError: true}; } } diff --git a/app/client/rest/constants.ts b/app/client/rest/constants.ts index 5aeb125f0..6c3d426a5 100644 --- a/app/client/rest/constants.ts +++ b/app/client/rest/constants.ts @@ -11,6 +11,7 @@ export const HEADER_TOKEN = 'Token'; export const HEADER_USER_AGENT = 'User-Agent'; export const HEADER_X_CSRF_TOKEN = 'X-CSRF-Token'; export const HEADER_X_MATTERMOST_PREAUTH_SECRET = 'X-Mattermost-Preauth-Secret'; +export const HEADER_X_REJECT_REASON = 'X-Reject-Reason'; export const HEADER_X_VERSION_ID = 'X-Version-Id'; export const DEFAULT_LIMIT_BEFORE = 30; export const DEFAULT_LIMIT_AFTER = 30; diff --git a/app/utils/headers.test.ts b/app/utils/headers.test.ts new file mode 100644 index 000000000..6eb8635f2 --- /dev/null +++ b/app/utils/headers.test.ts @@ -0,0 +1,30 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {getResponseHeader} from './headers'; + +describe('getResponseHeader', () => { + it('should return the value for an exact-case match', () => { + const headers = {'X-Reject-Reason': 'pre-auth'}; + expect(getResponseHeader(headers, 'X-Reject-Reason')).toBe('pre-auth'); + }); + + it('should return the value when header name differs in case', () => { + const headers = {'x-reject-reason': 'pre-auth'}; + expect(getResponseHeader(headers, 'X-Reject-Reason')).toBe('pre-auth'); + }); + + it('should return the value when lookup name is lowercase but header is uppercase', () => { + const headers = {'X-REJECT-REASON': 'pre-auth'}; + expect(getResponseHeader(headers, 'x-reject-reason')).toBe('pre-auth'); + }); + + it('should return undefined when the header is not present', () => { + const headers = {'Content-Type': 'application/json'}; + expect(getResponseHeader(headers, 'X-Reject-Reason')).toBeUndefined(); + }); + + it('should return undefined when headers object is undefined', () => { + expect(getResponseHeader(undefined, 'X-Reject-Reason')).toBeUndefined(); + }); +}); diff --git a/app/utils/headers.ts b/app/utils/headers.ts new file mode 100644 index 000000000..c7fa0be84 --- /dev/null +++ b/app/utils/headers.ts @@ -0,0 +1,23 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +export function getResponseHeader(headers: Record | undefined, name: string): string | undefined { + if (!headers) { + return undefined; + } + const direct = headers[name]; + if (direct !== undefined) { + return direct; + } + const lowerName = name.toLowerCase(); + const lowerDirect = headers[lowerName]; + if (lowerDirect !== undefined) { + return lowerDirect; + } + for (const key in headers) { + if (key.toLowerCase() === lowerName) { + return headers[key]; + } + } + return undefined; +}