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:
parent
82e5ff2ae8
commit
3319aec473
4 changed files with 58 additions and 2 deletions
|
|
@ -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};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
30
app/utils/headers.test.ts
Normal 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
23
app/utils/headers.ts
Normal 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;
|
||||
}
|
||||
Loading…
Reference in a new issue