Require X-Signal-Timestamp header on all storage/group server 403 responses

Co-authored-by: trevor-signal <131492920+trevor-signal@users.noreply.github.com>
This commit is contained in:
automated-signal 2025-02-25 11:56:36 -06:00 committed by GitHub
parent ebd360c3a3
commit 0d9cb81130
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 70 additions and 5 deletions

View file

@ -796,6 +796,10 @@
"messageformat": "Failed to send message with endorsements", "messageformat": "Failed to send message with endorsements",
"description": "An error popup when we attempted and failed to send a message using endorsements, only for internal users." "description": "An error popup when we attempted and failed to send a message using endorsements, only for internal users."
}, },
"icu:Toast--InvalidStorageServiceHeaders": {
"messageformat": "Received invalid response from storage service. Please share your logs.",
"description": "[Only shown to internal/beta users] An error popup when we noticed an invalid response (i.e. a web request response) from one of our servers"
},
"icu:Toast--FailedToImportBackup": { "icu:Toast--FailedToImportBackup": {
"messageformat": "Failed to process some frames during backup import. Please share your logs.", "messageformat": "Failed to process some frames during backup import. Please share your logs.",
"description": "[Only shown to internal users] An error popup when we failed to process some parts of a backup import." "description": "[Only shown to internal users] An error popup when we failed to process some parts of a backup import."

View file

@ -121,6 +121,8 @@ function getToast(toastType: ToastType): AnyToast {
return { toastType: ToastType.GroupLinkCopied }; return { toastType: ToastType.GroupLinkCopied };
case ToastType.InvalidConversation: case ToastType.InvalidConversation:
return { toastType: ToastType.InvalidConversation }; return { toastType: ToastType.InvalidConversation };
case ToastType.InvalidStorageServiceHeaders:
return { toastType: ToastType.InvalidStorageServiceHeaders };
case ToastType.LeftGroup: case ToastType.LeftGroup:
return { toastType: ToastType.LeftGroup }; return { toastType: ToastType.LeftGroup };
case ToastType.LinkCopied: case ToastType.LinkCopied:

View file

@ -328,6 +328,20 @@ export function renderToast({
); );
} }
if (toastType === ToastType.InvalidStorageServiceHeaders) {
return (
<Toast
onClose={hideToast}
toastAction={{
label: i18n('icu:Toast__ActionLabel--SubmitLog'),
onClick: onShowDebugLog,
}}
>
{i18n('icu:Toast--InvalidStorageServiceHeaders')}
</Toast>
);
}
if (toastType === ToastType.FileSaved) { if (toastType === ToastType.FileSaved) {
return ( return (
<Toast <Toast

View file

@ -9,7 +9,7 @@
import type { RequestInit, Response } from 'node-fetch'; import type { RequestInit, Response } from 'node-fetch';
import fetch from 'node-fetch'; import fetch from 'node-fetch';
import type { Agent } from 'https'; import type { Agent } from 'https';
import { escapeRegExp, isNumber, isString, isObject } from 'lodash'; import { escapeRegExp, isNumber, isString, isObject, throttle } from 'lodash';
import PQueue from 'p-queue'; import PQueue from 'p-queue';
import { v4 as getGuid } from 'uuid'; import { v4 as getGuid } from 'uuid';
import { z } from 'zod'; import { z } from 'zod';
@ -81,6 +81,8 @@ import type {
import { isMockServer } from '../util/isMockServer'; import { isMockServer } from '../util/isMockServer';
import { getMockServerPort } from '../util/getMockServerPort'; import { getMockServerPort } from '../util/getMockServerPort';
import { pemToDer } from '../util/pemToDer'; import { pemToDer } from '../util/pemToDer';
import { ToastType } from '../types/Toast';
import { isProduction } from '../util/version';
// Note: this will break some code that expects to be able to use err.response when a // Note: this will break some code that expects to be able to use err.response when a
// web request fails, because it will force it to text. But it is very useful for // web request fails, because it will force it to text. But it is very useful for
@ -194,6 +196,7 @@ type PromiseAjaxOptionsType = {
socketManager?: SocketManager; socketManager?: SocketManager;
basicAuth?: string; basicAuth?: string;
certificateAuthority?: string; certificateAuthority?: string;
chatServiceUrl?: string;
contentType?: string; contentType?: string;
data?: Uint8Array | (() => Readable) | string; data?: Uint8Array | (() => Readable) | string;
disableRetries?: boolean; disableRetries?: boolean;
@ -212,8 +215,8 @@ type PromiseAjaxOptionsType = {
| 'byteswithdetails' | 'byteswithdetails'
| 'stream' | 'stream'
| 'streamwithdetails'; | 'streamwithdetails';
serverUrl?: string;
stack?: string; stack?: string;
storageUrl?: string;
timeout?: number; timeout?: number;
type: HTTPCodeType; type: HTTPCodeType;
user?: string; user?: string;
@ -401,9 +404,35 @@ async function _promiseAjax(
throw makeHTTPError('promiseAjax catch', 0, {}, e.toString(), stack); throw makeHTTPError('promiseAjax catch', 0, {}, e.toString(), stack);
} }
const urlHostname = getHostname(url);
if (options.storageUrl && url.startsWith(options.storageUrl)) {
// The cloud infrastructure that sits in front of the Storage Service / Groups server
// has in the past terminated requests with a 403 before they make it to a Signal
// server. That's a problem, since we might take destructive action locally in
// response to a 403. Responses from a Signal server should always contain the
// `x-signal-timestamp` headers.
if (response.headers.get('x-signal-timestamp') == null) {
log.error(
logId,
response.status,
'Invalid header: missing required x-signal-timestamp header'
);
onIncorrectHeadersFromStorageService();
// TODO: DESKTOP-8300
if (response.status === 403) {
throw new Error(
`${logId} ${response.status}: Dropping response, missing required x-signal-timestamp header`
);
}
}
}
if ( if (
options.serverUrl && options.chatServiceUrl &&
getHostname(options.serverUrl) === getHostname(url) getHostname(options.chatServiceUrl) === urlHostname
) { ) {
await handleStatusCode(response.status); await handleStatusCode(response.status);
@ -2004,6 +2033,7 @@ export function initialize({
socketManager: useWebSocketForEndpoint ? socketManager : undefined, socketManager: useWebSocketForEndpoint ? socketManager : undefined,
basicAuth: param.basicAuth, basicAuth: param.basicAuth,
certificateAuthority, certificateAuthority,
chatServiceUrl,
contentType: param.contentType || 'application/json; charset=utf-8', contentType: param.contentType || 'application/json; charset=utf-8',
data: data:
param.data || param.data ||
@ -2018,7 +2048,7 @@ export function initialize({
type: param.httpType, type: param.httpType,
user: param.username ?? username, user: param.username ?? username,
redactUrl: param.redactUrl, redactUrl: param.redactUrl,
serverUrl: chatServiceUrl, storageUrl,
validateResponse: param.validateResponse, validateResponse: param.validateResponse,
version, version,
unauthenticated: param.unauthenticated, unauthenticated: param.unauthenticated,
@ -4666,3 +4696,16 @@ export function initialize({
} }
} }
} }
// TODO: DESKTOP-8300
const onIncorrectHeadersFromStorageService = throttle(
() => {
if (!isProduction(window.getVersion())) {
window.reduxActions.toast.showToast({
toastType: ToastType.InvalidStorageServiceHeaders,
});
}
},
5 * MINUTE,
{ trailing: false }
);

View file

@ -40,6 +40,7 @@ export enum ToastType {
FileSize = 'FileSize', FileSize = 'FileSize',
GroupLinkCopied = 'GroupLinkCopied', GroupLinkCopied = 'GroupLinkCopied',
InvalidConversation = 'InvalidConversation', InvalidConversation = 'InvalidConversation',
InvalidStorageServiceHeaders = 'InvalidStorageServiceHeaders',
LeftGroup = 'LeftGroup', LeftGroup = 'LeftGroup',
LinkCopied = 'LinkCopied', LinkCopied = 'LinkCopied',
LoadingFullLogs = 'LoadingFullLogs', LoadingFullLogs = 'LoadingFullLogs',
@ -133,6 +134,7 @@ export type AnyToast =
}; };
} }
| { toastType: ToastType.InvalidConversation } | { toastType: ToastType.InvalidConversation }
| { toastType: ToastType.InvalidStorageServiceHeaders }
| { toastType: ToastType.LeftGroup } | { toastType: ToastType.LeftGroup }
| { toastType: ToastType.LinkCopied } | { toastType: ToastType.LinkCopied }
| { toastType: ToastType.LoadingFullLogs } | { toastType: ToastType.LoadingFullLogs }