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.
This commit is contained in:
Felipe Martin 2026-03-17 12:07:07 +01:00 committed by GitHub
parent 82e5ff2ae8
commit 3319aec473
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 58 additions and 2 deletions

View file

@ -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};
}
}

View file

@ -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;

30
app/utils/headers.test.ts Normal file
View file

@ -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();
});
});

23
app/utils/headers.ts Normal file
View file

@ -0,0 +1,23 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
export function getResponseHeader(headers: Record<string, string> | 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;
}