From d940967b8919f3dc28651d129556eed624b30be0 Mon Sep 17 00:00:00 2001 From: Ioannis J Date: Mon, 25 Nov 2024 15:22:34 +0200 Subject: [PATCH] fix: detect and mask out photo library and user photos (#261) * fix: detect and mask out photo library and user photos * chore: update CHANGELOG * Update PostHog/Replay/PostHogReplayIntegration.swift Co-authored-by: Manoel Aranda Neto <5731772+marandaneto@users.noreply.github.com> * feat: add maskPhotoLibraryImages and maskSandboxedViews config * Update PostHog/Replay/PostHogSessionReplayConfig.swift Co-authored-by: Manoel Aranda Neto <5731772+marandaneto@users.noreply.github.com> * fix: rename maskAllSandboxedViews config option --------- Co-authored-by: Manoel Aranda Neto <5731772+marandaneto@users.noreply.github.com> --- .swiftlint.yml | 5 +- CHANGELOG.md | 2 + PostHog/Replay/PostHogReplayIntegration.swift | 51 ++++++++++++++++--- .../Replay/PostHogSessionReplayConfig.swift | 11 ++++ 4 files changed, 61 insertions(+), 8 deletions(-) diff --git a/.swiftlint.yml b/.swiftlint.yml index f3bccd5f4..519c6e3ae 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -15,7 +15,10 @@ disabled_rules: - trailing_comma - opening_brace -line_length: 160 +line_length: + warning: 160 + ignores_comments: true + file_length: warning: 1000 error: 1200 diff --git a/CHANGELOG.md b/CHANGELOG.md index 68d9e4539..dcdf410aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ ## Next +- fix: detect and mask out system photo library and user photos ([#261](https://github.com/PostHog/posthog-ios/pull/261)) + ## 3.15.6 - 2024-11-20 - fix: read accessibilityLabel from parent's view to avoid performance hit on RN ([#259](https://github.com/PostHog/posthog-ios/pull/259)) diff --git a/PostHog/Replay/PostHogReplayIntegration.swift b/PostHog/Replay/PostHogReplayIntegration.swift index ea22d2884..0ce10b0b9 100644 --- a/PostHog/Replay/PostHogReplayIntegration.swift +++ b/PostHog/Replay/PostHogReplayIntegration.swift @@ -8,6 +8,7 @@ // #if os(iOS) import Foundation + import PhotosUI import SwiftUI import UIKit import WebKit @@ -82,6 +83,8 @@ private let reactNativeTextView: AnyClass? = NSClassFromString("RCTTextView") private let reactNativeImageView: AnyClass? = NSClassFromString("RCTImageView") + // These are usually views that don't belong to the current process and are most likely sensitive + private let systemSandboxedView: AnyClass? = NSClassFromString("_UIRemoteView") static let dispatchQueue = DispatchQueue(label: "com.posthog.PostHogReplayIntegration", target: .global(qos: .utility)) @@ -283,6 +286,15 @@ } } + // detect any views that don't belong to the current process (likely system views) + if config.sessionReplayConfig.maskAllSandboxedViews, + let systemSandboxedView, + view.isKind(of: systemSandboxedView) + { + maskableWidgets.append(view.toAbsoluteRect(window)) + return + } + // if its a generic type and has subviews, subviews have to be checked first let hasSubViews = !view.subviews.isEmpty @@ -380,6 +392,24 @@ image.imageAsset?.value(forKey: "_containingBundle") != nil } + // Photo library images have a UUID identifier as _assetName (e.g 64EF5A48-2E96-4AB2-A79B-AAB7E9116E3D) + // SF symbol and bundle images have the actual symbol name as _assetName (e.g chevron.backward) + private func isPhotoLibraryImage(_ image: UIImage) -> Bool { + guard config.sessionReplayConfig.maskPhotoLibraryImages else { + return false + } + + guard let assetName = image.imageAsset?.value(forKey: "_assetName") as? String else { + return false + } + + if assetName.isEmpty { return false } + if image.isSymbolImage { return false } + if isAssetsImage(image) { return false } + + return true + } + private func isAnyInputSensitive(_ view: UIView) -> Bool { isTextInputSensitive(view) || config.sessionReplayConfig.maskAllImages } @@ -429,14 +459,21 @@ } private func isImageViewSensitive(_ view: UIImageView) -> Bool { - var isAsset = false - if let image = view.image { - isAsset = isAssetsImage(image) - } else { - // if there's no image, there's nothing to mask - return false + // if there's no image, there's nothing to mask + guard let image = view.image else { return false } + + // sensitive, regardless + if view.isNoCapture() { + return true } - return (config.sessionReplayConfig.maskAllImages && !isAsset) || view.isNoCapture() + + if config.sessionReplayConfig.maskAllImages { + // asset images are probably not sensitive + return !isAssetsImage(image) + } + + // try to detect user photo images + return isPhotoLibraryImage(image) } private func toWireframe(_ view: UIView) -> RRWireframe? { diff --git a/PostHog/Replay/PostHogSessionReplayConfig.swift b/PostHog/Replay/PostHogSessionReplayConfig.swift index 360f30fbb..b9b29f92a 100644 --- a/PostHog/Replay/PostHogSessionReplayConfig.swift +++ b/PostHog/Replay/PostHogSessionReplayConfig.swift @@ -18,6 +18,17 @@ /// Default: true @objc public var maskAllImages: Bool = true + /// Enable masking of all sandboxed system views + /// These may include UIImagePickerController, PHPickerViewController and CNContactPickerViewController + /// Experimental support + /// Default: true + @objc public var maskAllSandboxedViews: Bool = true + + /// Enable masking of images that likely originated from user's photo library + /// Experimental support (UIKit only) + /// Default: true + @objc public var maskPhotoLibraryImages: Bool = true + /// Enable capturing network telemetry /// Experimental support /// Default: true