From 17656c31d243ac5e1c6bbe2a5dfb5b345fef13f3 Mon Sep 17 00:00:00 2001 From: Ioannis J Date: Fri, 22 Nov 2024 10:42:33 +0200 Subject: [PATCH 1/6] fix: detect and mask out photo library and user photos --- .swiftlint.yml | 5 +- PostHog/Replay/PostHogReplayIntegration.swift | 59 ++++++++++++++++--- 2 files changed, 56 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/PostHog/Replay/PostHogReplayIntegration.swift b/PostHog/Replay/PostHogReplayIntegration.swift index ea22d2884..ca8399f45 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 systemRemoteView: AnyClass? = NSClassFromString("_UIRemoteView") static let dispatchQueue = DispatchQueue(label: "com.posthog.PostHogReplayIntegration", target: .global(qos: .utility)) @@ -283,6 +286,27 @@ } } + // if view in photo library picker controller, always mask the whole view + if let pickerViewController = view.nearestViewController as? UIImagePickerController { + maskableWidgets.append(pickerViewController.view.toAbsoluteRect(window)) + return + } + + if #available(iOS 14, *) { + if let pickerViewController = view.nearestViewController as? PHPickerViewController { + maskableWidgets.append(pickerViewController.view.toAbsoluteRect(window)) + return + } + } + + // detect any views that don't belong to the current process (likely system views) + if let systemRemoteView { + if view.isKind(of: systemRemoteView) { + 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 +404,20 @@ 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 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 +467,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 probable not sensitive + return !isAssetsImage(image) + } + + // try to detect user photo images + return isPhotoLibraryImage(image) } private func toWireframe(_ view: UIView) -> RRWireframe? { From b2761b077c74886c5267b94144397481eb29f9b7 Mon Sep 17 00:00:00 2001 From: Ioannis J Date: Fri, 22 Nov 2024 10:45:02 +0200 Subject: [PATCH 2/6] chore: update CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) 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)) From e3a37b375845e310fe4c1e27c7a65ecd7471116b Mon Sep 17 00:00:00 2001 From: Ioannis J Date: Fri, 22 Nov 2024 11:02:08 +0200 Subject: [PATCH 3/6] Update PostHog/Replay/PostHogReplayIntegration.swift Co-authored-by: Manoel Aranda Neto <5731772+marandaneto@users.noreply.github.com> --- PostHog/Replay/PostHogReplayIntegration.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PostHog/Replay/PostHogReplayIntegration.swift b/PostHog/Replay/PostHogReplayIntegration.swift index ca8399f45..179c938bf 100644 --- a/PostHog/Replay/PostHogReplayIntegration.swift +++ b/PostHog/Replay/PostHogReplayIntegration.swift @@ -476,7 +476,7 @@ } if config.sessionReplayConfig.maskAllImages { - // asset images are probable not sensitive + // asset images are probably not sensitive return !isAssetsImage(image) } From ff9291b3c40214ac4404f73599e553a648f66fa2 Mon Sep 17 00:00:00 2001 From: Ioannis J Date: Mon, 25 Nov 2024 09:32:42 +0200 Subject: [PATCH 4/6] feat: add maskPhotoLibraryImages and maskSandboxedViews config --- PostHog/Replay/PostHogReplayIntegration.swift | 30 +++++++------------ .../Replay/PostHogSessionReplayConfig.swift | 11 +++++++ 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/PostHog/Replay/PostHogReplayIntegration.swift b/PostHog/Replay/PostHogReplayIntegration.swift index 179c938bf..af594724f 100644 --- a/PostHog/Replay/PostHogReplayIntegration.swift +++ b/PostHog/Replay/PostHogReplayIntegration.swift @@ -84,7 +84,7 @@ 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 systemRemoteView: AnyClass? = NSClassFromString("_UIRemoteView") + private let systemSandboxedView: AnyClass? = NSClassFromString("_UIRemoteView") static let dispatchQueue = DispatchQueue(label: "com.posthog.PostHogReplayIntegration", target: .global(qos: .utility)) @@ -286,25 +286,13 @@ } } - // if view in photo library picker controller, always mask the whole view - if let pickerViewController = view.nearestViewController as? UIImagePickerController { - maskableWidgets.append(pickerViewController.view.toAbsoluteRect(window)) - return - } - - if #available(iOS 14, *) { - if let pickerViewController = view.nearestViewController as? PHPickerViewController { - maskableWidgets.append(pickerViewController.view.toAbsoluteRect(window)) - return - } - } - // detect any views that don't belong to the current process (likely system views) - if let systemRemoteView { - if view.isKind(of: systemRemoteView) { - maskableWidgets.append(view.toAbsoluteRect(window)) - return - } + if config.sessionReplayConfig.maskSandboxedViews, + 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 @@ -407,6 +395,10 @@ // 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 } diff --git a/PostHog/Replay/PostHogSessionReplayConfig.swift b/PostHog/Replay/PostHogSessionReplayConfig.swift index 360f30fbb..7c95c9c9a 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 maskSandboxedViews: 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 From 20d5e0f03100afef21994b2f73eb841d1ff4b5d2 Mon Sep 17 00:00:00 2001 From: Ioannis J Date: Mon, 25 Nov 2024 13:05:03 +0200 Subject: [PATCH 5/6] Update PostHog/Replay/PostHogSessionReplayConfig.swift Co-authored-by: Manoel Aranda Neto <5731772+marandaneto@users.noreply.github.com> --- PostHog/Replay/PostHogSessionReplayConfig.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PostHog/Replay/PostHogSessionReplayConfig.swift b/PostHog/Replay/PostHogSessionReplayConfig.swift index 7c95c9c9a..b9b29f92a 100644 --- a/PostHog/Replay/PostHogSessionReplayConfig.swift +++ b/PostHog/Replay/PostHogSessionReplayConfig.swift @@ -22,7 +22,7 @@ /// These may include UIImagePickerController, PHPickerViewController and CNContactPickerViewController /// Experimental support /// Default: true - @objc public var maskSandboxedViews: Bool = true + @objc public var maskAllSandboxedViews: Bool = true /// Enable masking of images that likely originated from user's photo library /// Experimental support (UIKit only) From 9e99aa4208786026345ebf4cd6649c102db694fd Mon Sep 17 00:00:00 2001 From: Ioannis J Date: Mon, 25 Nov 2024 13:07:02 +0200 Subject: [PATCH 6/6] fix: rename maskAllSandboxedViews config option --- PostHog/Replay/PostHogReplayIntegration.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PostHog/Replay/PostHogReplayIntegration.swift b/PostHog/Replay/PostHogReplayIntegration.swift index af594724f..0ce10b0b9 100644 --- a/PostHog/Replay/PostHogReplayIntegration.swift +++ b/PostHog/Replay/PostHogReplayIntegration.swift @@ -287,7 +287,7 @@ } // detect any views that don't belong to the current process (likely system views) - if config.sessionReplayConfig.maskSandboxedViews, + if config.sessionReplayConfig.maskAllSandboxedViews, let systemSandboxedView, view.isKind(of: systemSandboxedView) {