From b8a614274cb82dcad36575ef4c0fcbc9cea4da9b Mon Sep 17 00:00:00 2001 From: staszekscp Date: Wed, 20 May 2026 14:37:04 +0200 Subject: [PATCH 1/3] Hide sensitive data from logging --- android/app/proguard-rules.pro | 6 ++ .../CustomNotificationProvider.java | 9 +-- .../java/com/group_ib/react/FhpModule.java | 5 +- src/libs/API/index.ts | 9 +-- src/libs/Middleware/Logging.ts | 9 +-- src/libs/Navigation/NavigationRoot.tsx | 3 +- src/libs/actions/App.ts | 3 +- src/libs/actions/PersistedRequests.ts | 5 +- src/libs/asyncOpenURL/index.ts | 4 +- src/libs/sanitizeLogParams.ts | 59 +++++++++++++++++++ 10 files changed, 94 insertions(+), 18 deletions(-) create mode 100644 src/libs/sanitizeLogParams.ts diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro index 08365dce4c9e..942d033fbfef 100644 --- a/android/app/proguard-rules.pro +++ b/android/app/proguard-rules.pro @@ -51,3 +51,9 @@ # https://shopify.github.io/react-native-skia/docs/getting-started/installation/#proguard -keep class com.shopify.reactnative.skia.** { *; } + +# Strip verbose and debug log calls from release builds +-assumenosideeffects class android.util.Log { + public static int d(...); + public static int v(...); +} diff --git a/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationProvider.java b/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationProvider.java index 628cf90ce32a..3a0c3fb1ffcb 100644 --- a/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationProvider.java +++ b/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationProvider.java @@ -36,6 +36,7 @@ import androidx.core.graphics.drawable.IconCompat; import androidx.versionedparcelable.ParcelUtils; +import com.expensify.chat.BuildConfig; import com.expensify.chat.R; import com.expensify.chat.shortcutManagerModule.ShortcutManagerUtils; import com.expensify.chat.customairshipextender.PayloadHandler; @@ -103,7 +104,7 @@ public CustomNotificationProvider(@NonNull Context context, @NonNull AirshipConf protected NotificationCompat.Builder onExtendBuilder(@NonNull Context context, @NonNull NotificationCompat.Builder builder, @NonNull NotificationArguments arguments) { super.onExtendBuilder(context, builder, arguments); PushMessage message = arguments.getMessage(); - Log.d(TAG, "buildNotification: " + message.toString()); + if (BuildConfig.DEBUG) Log.d(TAG, "buildNotification: " + message.toString()); // Improve notification delivery by categorizing as a time-critical message builder.setCategory(CATEGORY_MESSAGE); @@ -127,7 +128,7 @@ protected NotificationCompat.Builder onExtendBuilder(@NonNull Context context, @ try { String rawPayload = message.getExtra(PAYLOAD_KEY); if (rawPayload == null) { - Log.d(TAG, "Failed to parse payload - payload is empty. SendID=" + message.getSendId()); + if (BuildConfig.DEBUG) Log.d(TAG, "Failed to parse payload - payload is empty. SendID=" + message.getSendId()); return builder; } @@ -135,12 +136,12 @@ protected NotificationCompat.Builder onExtendBuilder(@NonNull Context context, @ String processedPayload = handler.processPayload(rawPayload); JsonMap payload = JsonValue.parseString(processedPayload).optMap(); if (!payload.containsKey(ONYX_DATA_KEY)) { - Log.d(TAG, "Failed to process payload - no onyx data. SendID=" + message.getSendId()); + if (BuildConfig.DEBUG) Log.d(TAG, "Failed to process payload - no onyx data. SendID=" + message.getSendId()); return builder; } Objects.requireNonNull(payload.get(ONYX_DATA_KEY)).isNull(); - Log.d(TAG, "payload contains onxyData"); + if (BuildConfig.DEBUG) Log.d(TAG, "payload contains onxyData"); String alert = message.getExtra(PushMessage.EXTRA_ALERT); applyMessageStyle(context, builder, payload, arguments.getNotificationId(), alert); } catch (Exception e) { diff --git a/modules/group-ib-fp/android/src/main/java/com/group_ib/react/FhpModule.java b/modules/group-ib-fp/android/src/main/java/com/group_ib/react/FhpModule.java index 0ea1e564bae3..912ac77430b9 100644 --- a/modules/group-ib-fp/android/src/main/java/com/group_ib/react/FhpModule.java +++ b/modules/group-ib-fp/android/src/main/java/com/group_ib/react/FhpModule.java @@ -3,6 +3,7 @@ import android.app.Activity; import android.os.Handler; import android.util.Log; +import com.group_ib.react.BuildConfig; import androidx.annotation.NonNull; import main.java.com.group_ib.react.session.SessionEvents; import main.java.com.group_ib.react.session.SessionListenerImpl; @@ -66,7 +67,9 @@ public void initialize() { super.initialize(); final Activity activity = getCurrentActivity(); try { - MobileSdk.enableDebugLogs(); + if (BuildConfig.DEBUG) { + MobileSdk.enableDebugLogs(); + } PackageCollectionModule.init(); sdk = MobileSdk.init(activity != null ? activity : context); sdk.setSessionListener(sessionListener); diff --git a/src/libs/API/index.ts b/src/libs/API/index.ts index 0905cf1c4922..59309d712ed0 100644 --- a/src/libs/API/index.ts +++ b/src/libs/API/index.ts @@ -11,6 +11,7 @@ import {push as pushToSequentialQueue, waitForIdle as waitForSequentialQueueIdle import {getIsOffline} from '@libs/NetworkState'; import Pusher from '@libs/Pusher'; import {addMiddleware, processWithMiddleware} from '@libs/Request'; +import sanitizeLogParams from '@libs/sanitizeLogParams'; import {getAll, getLength as getPersistedRequestsLength} from '@userActions/PersistedRequests'; import CONST from '@src/CONST'; import type OnyxRequest from '@src/types/onyx/Request'; @@ -165,7 +166,7 @@ function write( onyxData: OnyxData = {}, conflictResolver: RequestConflictResolver = {}, ): Promise> { - Log.info('[API] Called API write', false, {command, ...apiCommandParameters}); + Log.info('[API] Called API write', false, {command, ...sanitizeLogParams(apiCommandParameters)}); const request = prepareRequest(command, CONST.API_REQUEST_TYPE.WRITE, apiCommandParameters, onyxData, conflictResolver); return processRequest(request, CONST.API_REQUEST_TYPE.WRITE); @@ -217,7 +218,7 @@ function makeRequestWithSideEffects = {}, ): Promise> { - Log.info('[API] Called API makeRequestWithSideEffects', false, {command, ...apiCommandParameters}); + Log.info('[API] Called API makeRequestWithSideEffects', false, {command, ...sanitizeLogParams(apiCommandParameters)}); const request = prepareRequest(command, CONST.API_REQUEST_TYPE.MAKE_REQUEST_WITH_SIDE_EFFECTS, apiCommandParameters, onyxData); // Return a promise containing the response from HTTPS @@ -243,7 +244,7 @@ function read(command: TCommand, apiCommandParamet function read(command: TCommand, apiCommandParameters: ApiRequestCommandParameters[TCommand], onyxData: OnyxData): void; function read(command: TCommand, apiCommandParameters: ApiRequestCommandParameters[TCommand], onyxData: OnyxData = {}): void { - Log.info('[API] Called API.read', false, {command, ...apiCommandParameters}); + Log.info('[API] Called API.read', false, {command, ...sanitizeLogParams(apiCommandParameters)}); // Apply optimistic updates of read requests immediately const request = prepareRequest(command, CONST.API_REQUEST_TYPE.READ, apiCommandParameters, onyxData); @@ -287,7 +288,7 @@ function paginate = {}, ): Promise | void> | void { - Log.info('[API] Called API.paginate', false, {command, ...apiCommandParameters}); + Log.info('[API] Called API.paginate', false, {command, ...sanitizeLogParams(apiCommandParameters)}); const request: PaginatedRequest = { ...prepareRequest(command, type, apiCommandParameters, onyxData, conflictResolver), ...config, diff --git a/src/libs/Middleware/Logging.ts b/src/libs/Middleware/Logging.ts index 35d32caa5a4a..a56331736f72 100644 --- a/src/libs/Middleware/Logging.ts +++ b/src/libs/Middleware/Logging.ts @@ -2,6 +2,7 @@ import type {OnyxKey} from 'react-native-onyx'; import {SIDE_EFFECT_REQUEST_COMMANDS} from '@libs/API/types'; import type HttpsError from '@libs/Errors/HttpsError'; import Log from '@libs/Log'; +import sanitizeLogParams from '@libs/sanitizeLogParams'; import CONST from '@src/CONST'; import type Request from '@src/types/onyx/Request'; import type Response from '@src/types/onyx/Response'; @@ -67,11 +68,11 @@ function logRequestDetails(message: string, request: Reque * requests because they contain sensitive information. */ if (request.command !== 'AuthenticatePusher') { - extraData.request = { + extraData.request = sanitizeLogParams({ ...request, data: serializeLoggingData(request.data), - }; - extraData.response = response; + }); + extraData.response = sanitizeLogParams(response); } Log.info(message, false, logParams, false, extraData); @@ -90,7 +91,7 @@ const Logging: Middleware = (response, request) => { message: error.message, status: error.status, title: error.title, - request, + request: sanitizeLogParams(request), }; // If the command that failed is Log it's possible that the next call to Log may also fail. diff --git a/src/libs/Navigation/NavigationRoot.tsx b/src/libs/Navigation/NavigationRoot.tsx index 388085047e85..f61a345b7c16 100644 --- a/src/libs/Navigation/NavigationRoot.tsx +++ b/src/libs/Navigation/NavigationRoot.tsx @@ -12,6 +12,7 @@ import useTheme from '@hooks/useTheme'; import useThemePreference from '@hooks/useThemePreference'; import FS from '@libs/Fullstory'; import Log from '@libs/Log'; +import {sanitizeUrlForLogging} from '@libs/sanitizeLogParams'; import shouldOpenLastVisitedPath from '@libs/shouldOpenLastVisitedPath'; import {getPathFromURL} from '@libs/Url'; import {getBaseTheme} from '@styles/theme/utils'; @@ -70,7 +71,7 @@ function parseAndLogRoute(state: NavigationState) { if (currentPath.includes('/transition')) { Log.info('Navigating from transition link from OldDot using short lived authToken'); } else { - Log.info('Navigating to route', false, {path: currentPath}); + Log.info('Navigating to route', false, {path: sanitizeUrlForLogging(currentPath)}); } Navigation.setIsNavigationReady(); diff --git a/src/libs/actions/App.ts b/src/libs/actions/App.ts index 82a90babd548..d93f8a4a8702 100644 --- a/src/libs/actions/App.ts +++ b/src/libs/actions/App.ts @@ -13,6 +13,7 @@ import Log from '@libs/Log'; import getCurrentUrl from '@libs/Navigation/currentUrl'; import Navigation, {navigationRef} from '@libs/Navigation/Navigation'; import {isPublicRoom, isValidReport} from '@libs/ReportUtils'; +import {sanitizeUrlForLogging} from '@libs/sanitizeLogParams'; import {isLoggingInAsNewUser as isLoggingInAsNewUserSessionUtils} from '@libs/SessionUtils'; import {clearSoundAssetsCache} from '@libs/Sound'; import {cancelAllSpans, endSpan, getSpan, startSpan} from '@libs/telemetry/activeSpans'; @@ -246,7 +247,7 @@ function saveCurrentPathBeforeBackground() { const currentPath = getPathFromState(currentState); if (currentPath) { - Log.info('Saving current path before background', false, {currentPath}); + Log.info('Saving current path before background', false, {currentPath: sanitizeUrlForLogging(currentPath)}); updateLastVisitedPath(currentPath); } } catch (error) { diff --git a/src/libs/actions/PersistedRequests.ts b/src/libs/actions/PersistedRequests.ts index 607eaf9f1f0c..3701f2ab8d3a 100644 --- a/src/libs/actions/PersistedRequests.ts +++ b/src/libs/actions/PersistedRequests.ts @@ -2,6 +2,7 @@ import {deepEqual} from 'fast-equals'; import type {OnyxKey} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import Log from '@libs/Log'; +import sanitizeLogParams from '@libs/sanitizeLogParams'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Request} from '@src/types/onyx'; import type {AnyRequest} from '@src/types/onyx/Request'; @@ -311,7 +312,7 @@ function deleteRequestsByIndices(indices: number[]): Promise { function update(oldRequestIndex: number, newRequest: Request): Promise { const requests = [...persistedRequests]; const oldRequest = requests.at(oldRequestIndex); - Log.info('[PersistedRequests] Updating a request', false, {oldRequest, newRequest, oldRequestIndex}); + Log.info('[PersistedRequests] Updating a request', false, {oldRequest: sanitizeLogParams(oldRequest), newRequest: sanitizeLogParams(newRequest), oldRequestIndex}); requests.splice(oldRequestIndex, 1, newRequest as AnyRequest); persistedRequests = requests; if (newRequest.requestID != null) { @@ -321,7 +322,7 @@ function update(oldRequestIndex: number, newRequest: Reque } function updateOngoingRequest(newRequest: Request) { - Log.info('[PersistedRequests] Updating the ongoing request', false, {ongoingRequest, newRequest}); + Log.info('[PersistedRequests] Updating the ongoing request', false, {ongoingRequest: sanitizeLogParams(ongoingRequest), newRequest: sanitizeLogParams(newRequest)}); ongoingRequest = newRequest as AnyRequest; if (newRequest.persistWhenOngoing) { diff --git a/src/libs/asyncOpenURL/index.ts b/src/libs/asyncOpenURL/index.ts index b52f7fbe99b7..c07663597df3 100644 --- a/src/libs/asyncOpenURL/index.ts +++ b/src/libs/asyncOpenURL/index.ts @@ -1,5 +1,6 @@ import {Linking} from 'react-native'; import Log from '@libs/Log'; +import {sanitizeUrlForLogging} from '@libs/sanitizeLogParams'; import type AsyncOpenURL from './types'; const asyncOpenURL: AsyncOpenURL = (promise, url) => { @@ -12,7 +13,8 @@ const asyncOpenURL: AsyncOpenURL = (promise, url) => { Linking.openURL(typeof url === 'string' ? url : url(params)); }) .catch(() => { - Log.warn('[asyncOpenURL] error occurred while opening URL', {url}); + const safeUrl = typeof url === 'string' ? sanitizeUrlForLogging(url) : '[function]'; + Log.warn('[asyncOpenURL] error occurred while opening URL', {url: safeUrl}); }); }; diff --git a/src/libs/sanitizeLogParams.ts b/src/libs/sanitizeLogParams.ts new file mode 100644 index 000000000000..963b48def798 --- /dev/null +++ b/src/libs/sanitizeLogParams.ts @@ -0,0 +1,59 @@ +const SENSITIVE_KEYS = new Set([ + 'authToken', + 'encryptedAuthToken', + 'password', + 'partnerUserSecret', + 'partnerPassword', + 'twoFactorAuthCode', + 'idToken', + 'token', + 'validateCode', + 'autoGeneratedPassword', + 'autoGeneratedLogin', + 'oldDotCurrentAuthToken', + 'oldDotCurrentEncryptedAuthToken', + 'oldDotAutoGeneratedPassword', +]); + +const REDACTED = ''; + +function sanitizeLogParams(params: T, depth = 0): T { + if (depth > 5 || params == null || typeof params !== 'object') { + return params; + } + + if (Array.isArray(params)) { + return params.map((item) => sanitizeLogParams(item, depth + 1)) as unknown as T; + } + + const sanitized: Record = {}; + for (const [key, value] of Object.entries(params)) { + if (SENSITIVE_KEYS.has(key)) { + sanitized[key] = REDACTED; + } else if (value != null && typeof value === 'object') { + sanitized[key] = sanitizeLogParams(value, depth + 1); + } else { + sanitized[key] = value; + } + } + + return sanitized as T; +} + +/** + * Redact sensitive segments from URL paths before logging. + * Handles /v/:accountID/:validateCode and /u/:accountID/:validateCode patterns, + * and strips query parameters that may contain tokens. + */ +function sanitizeUrlForLogging(url: string): string { + return ( + url + // Redact validateCode in /v/:accountID/:validateCode and /u/:accountID/:validateCode + .replace(/\/([vu])\/(\d+)\/[^/?#]+/, '/$1/$2/') + // Strip query string (may contain authToken, token, etc.) + .replace(/\?.*$/, '?') + ); +} + +export default sanitizeLogParams; +export {sanitizeUrlForLogging}; From 2fc830528bd30e1c44cea6dbff19b128d21d7bd2 Mon Sep 17 00:00:00 2001 From: staszekscp Date: Thu, 21 May 2026 13:08:58 +0200 Subject: [PATCH 2/3] Fixes for lint --- src/libs/API/index.ts | 12 ++++++++---- src/libs/Navigation/NavigationRoot.tsx | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/libs/API/index.ts b/src/libs/API/index.ts index f91c52151548..76f441ec98c9 100644 --- a/src/libs/API/index.ts +++ b/src/libs/API/index.ts @@ -62,6 +62,10 @@ addMiddleware(FraudMonitoring); // incrementing number would collide across tabs. let requestIndex = Date.now(); +function buildLogParams(command: string, params: {}): Record { + return {command, ...Object.fromEntries(Object.entries(sanitizeLogParams(params)))}; +} + /** * Prepare the request to be sent. Bind data together with request metadata and apply optimistic Onyx data. */ @@ -169,7 +173,7 @@ function write( onyxData: OnyxData = {}, conflictResolver: RequestConflictResolver = {}, ): Promise> { - Log.info('[API] Called API write', false, {command, ...sanitizeLogParams(apiCommandParameters)}); + Log.info('[API] Called API write', false, buildLogParams(command, apiCommandParameters)); const request = prepareRequest(command, CONST.API_REQUEST_TYPE.WRITE, apiCommandParameters, onyxData, conflictResolver); return processRequest(request, CONST.API_REQUEST_TYPE.WRITE); @@ -221,7 +225,7 @@ function makeRequestWithSideEffects = {}, ): Promise> { - Log.info('[API] Called API makeRequestWithSideEffects', false, {command, ...sanitizeLogParams(apiCommandParameters)}); + Log.info('[API] Called API makeRequestWithSideEffects', false, buildLogParams(command, apiCommandParameters)); const request = prepareRequest(command, CONST.API_REQUEST_TYPE.MAKE_REQUEST_WITH_SIDE_EFFECTS, apiCommandParameters, onyxData); // Return a promise containing the response from HTTPS @@ -247,7 +251,7 @@ function read(command: TCommand, apiCommandParamet function read(command: TCommand, apiCommandParameters: ApiRequestCommandParameters[TCommand], onyxData: OnyxData): void; function read(command: TCommand, apiCommandParameters: ApiRequestCommandParameters[TCommand], onyxData: OnyxData = {}): void { - Log.info('[API] Called API.read', false, {command, ...sanitizeLogParams(apiCommandParameters)}); + Log.info('[API] Called API.read', false, buildLogParams(command, apiCommandParameters)); // Apply optimistic updates of read requests immediately const request = prepareRequest(command, CONST.API_REQUEST_TYPE.READ, apiCommandParameters, onyxData); @@ -291,7 +295,7 @@ function paginate = {}, ): Promise | void> | void { - Log.info('[API] Called API.paginate', false, {command, ...sanitizeLogParams(apiCommandParameters)}); + Log.info('[API] Called API.paginate', false, buildLogParams(command, apiCommandParameters)); const request: PaginatedRequest = { ...prepareRequest(command, type, apiCommandParameters, onyxData, conflictResolver), ...config, diff --git a/src/libs/Navigation/NavigationRoot.tsx b/src/libs/Navigation/NavigationRoot.tsx index a3af4927d0d2..406f10c31297 100644 --- a/src/libs/Navigation/NavigationRoot.tsx +++ b/src/libs/Navigation/NavigationRoot.tsx @@ -12,8 +12,8 @@ import useTheme from '@hooks/useTheme'; import useThemePreference from '@hooks/useThemePreference'; import FS from '@libs/Fullstory'; import Log from '@libs/Log'; -import {sanitizeUrlForLogging} from '@libs/sanitizeLogParams'; import {setupNavigationFocusReturn, teardownNavigationFocusReturn} from '@libs/NavigationFocusReturn'; +import {sanitizeUrlForLogging} from '@libs/sanitizeLogParams'; import shouldOpenLastVisitedPath from '@libs/shouldOpenLastVisitedPath'; import {getPathFromURL} from '@libs/Url'; import {getBaseTheme} from '@styles/theme/utils'; From 0c68a940545ee30a4283c5e1864cd3d41bd01fab Mon Sep 17 00:00:00 2001 From: staszekscp Date: Thu, 21 May 2026 14:04:36 +0200 Subject: [PATCH 3/3] Fixes for lint --- src/libs/API/index.ts | 10 +++++----- src/libs/sanitizeLogParams.ts | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/libs/API/index.ts b/src/libs/API/index.ts index 76f441ec98c9..53c7bc5a6f52 100644 --- a/src/libs/API/index.ts +++ b/src/libs/API/index.ts @@ -62,7 +62,7 @@ addMiddleware(FraudMonitoring); // incrementing number would collide across tabs. let requestIndex = Date.now(); -function buildLogParams(command: string, params: {}): Record { +function buildLogParams(command: string, params: Record): Record { return {command, ...Object.fromEntries(Object.entries(sanitizeLogParams(params)))}; } @@ -173,7 +173,7 @@ function write( onyxData: OnyxData = {}, conflictResolver: RequestConflictResolver = {}, ): Promise> { - Log.info('[API] Called API write', false, buildLogParams(command, apiCommandParameters)); + Log.info('[API] Called API write', false, buildLogParams(command, apiCommandParameters ?? {})); const request = prepareRequest(command, CONST.API_REQUEST_TYPE.WRITE, apiCommandParameters, onyxData, conflictResolver); return processRequest(request, CONST.API_REQUEST_TYPE.WRITE); @@ -225,7 +225,7 @@ function makeRequestWithSideEffects = {}, ): Promise> { - Log.info('[API] Called API makeRequestWithSideEffects', false, buildLogParams(command, apiCommandParameters)); + Log.info('[API] Called API makeRequestWithSideEffects', false, buildLogParams(command, apiCommandParameters ?? {})); const request = prepareRequest(command, CONST.API_REQUEST_TYPE.MAKE_REQUEST_WITH_SIDE_EFFECTS, apiCommandParameters, onyxData); // Return a promise containing the response from HTTPS @@ -251,7 +251,7 @@ function read(command: TCommand, apiCommandParamet function read(command: TCommand, apiCommandParameters: ApiRequestCommandParameters[TCommand], onyxData: OnyxData): void; function read(command: TCommand, apiCommandParameters: ApiRequestCommandParameters[TCommand], onyxData: OnyxData = {}): void { - Log.info('[API] Called API.read', false, buildLogParams(command, apiCommandParameters)); + Log.info('[API] Called API.read', false, buildLogParams(command, apiCommandParameters ?? {})); // Apply optimistic updates of read requests immediately const request = prepareRequest(command, CONST.API_REQUEST_TYPE.READ, apiCommandParameters, onyxData); @@ -295,7 +295,7 @@ function paginate = {}, ): Promise | void> | void { - Log.info('[API] Called API.paginate', false, buildLogParams(command, apiCommandParameters)); + Log.info('[API] Called API.paginate', false, buildLogParams(command, apiCommandParameters ?? {})); const request: PaginatedRequest = { ...prepareRequest(command, type, apiCommandParameters, onyxData, conflictResolver), ...config, diff --git a/src/libs/sanitizeLogParams.ts b/src/libs/sanitizeLogParams.ts index 963b48def798..9c19155c12e0 100644 --- a/src/libs/sanitizeLogParams.ts +++ b/src/libs/sanitizeLogParams.ts @@ -23,7 +23,7 @@ function sanitizeLogParams(params: T, depth = 0): T { } if (Array.isArray(params)) { - return params.map((item) => sanitizeLogParams(item, depth + 1)) as unknown as T; + return (params as unknown[]).map((item) => sanitizeLogParams(item, depth + 1)) as unknown as T; } const sanitized: Record = {};