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 9be8434138a4..53c7bc5a6f52 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'; @@ -61,6 +62,10 @@ addMiddleware(FraudMonitoring); // incrementing number would collide across tabs. let requestIndex = Date.now(); +function buildLogParams(command: string, params: Record): 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. */ @@ -168,7 +173,7 @@ function write( onyxData: OnyxData = {}, conflictResolver: RequestConflictResolver = {}, ): Promise> { - Log.info('[API] Called API write', false, {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); @@ -220,7 +225,7 @@ function makeRequestWithSideEffects = {}, ): Promise> { - Log.info('[API] Called API makeRequestWithSideEffects', false, {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 @@ -246,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, ...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); @@ -290,7 +295,7 @@ function paginate = {}, ): Promise | void> | void { - Log.info('[API] Called API.paginate', false, {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/Middleware/Logging.ts b/src/libs/Middleware/Logging.ts index 0918994895a0..739bacecfb61 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 34eed088e942..406f10c31297 100644 --- a/src/libs/Navigation/NavigationRoot.tsx +++ b/src/libs/Navigation/NavigationRoot.tsx @@ -13,6 +13,7 @@ import useThemePreference from '@hooks/useThemePreference'; import FS from '@libs/Fullstory'; import Log from '@libs/Log'; 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'; @@ -72,7 +73,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 e8d8d9c49ccf..41bd47ab08c0 100644 --- a/src/libs/actions/App.ts +++ b/src/libs/actions/App.ts @@ -16,6 +16,7 @@ import willRouteNavigateToRHP from '@libs/Navigation/helpers/willRouteNavigateTo import Navigation, {navigationRef} from '@libs/Navigation/Navigation'; import isTrackOnboardingChoice from '@libs/OnboardingUtils'; 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 6697bc219e63..240b33069857 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'; @@ -316,7 +317,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) { @@ -338,7 +339,7 @@ function shouldPersistOngoingRequest(request: AnyRequest | null): boolean { } 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 (shouldPersistOngoingRequest(ongoingRequest)) { 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..9c19155c12e0 --- /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 as unknown[]).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};