Skip to content

Commit d940967

Browse files
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 <[email protected]> * feat: add maskPhotoLibraryImages and maskSandboxedViews config * Update PostHog/Replay/PostHogSessionReplayConfig.swift Co-authored-by: Manoel Aranda Neto <[email protected]> * fix: rename maskAllSandboxedViews config option --------- Co-authored-by: Manoel Aranda Neto <[email protected]>
1 parent f8d5524 commit d940967

File tree

4 files changed

+61
-8
lines changed

4 files changed

+61
-8
lines changed

.swiftlint.yml

+4-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@ disabled_rules:
1515
- trailing_comma
1616
- opening_brace
1717

18-
line_length: 160
18+
line_length:
19+
warning: 160
20+
ignores_comments: true
21+
1922
file_length:
2023
warning: 1000
2124
error: 1200

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
## Next
22

3+
- fix: detect and mask out system photo library and user photos ([#261](https://github.com/PostHog/posthog-ios/pull/261))
4+
35
## 3.15.6 - 2024-11-20
46

57
- fix: read accessibilityLabel from parent's view to avoid performance hit on RN ([#259](https://github.com/PostHog/posthog-ios/pull/259))

PostHog/Replay/PostHogReplayIntegration.swift

+44-7
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
//
99
#if os(iOS)
1010
import Foundation
11+
import PhotosUI
1112
import SwiftUI
1213
import UIKit
1314
import WebKit
@@ -82,6 +83,8 @@
8283

8384
private let reactNativeTextView: AnyClass? = NSClassFromString("RCTTextView")
8485
private let reactNativeImageView: AnyClass? = NSClassFromString("RCTImageView")
86+
// These are usually views that don't belong to the current process and are most likely sensitive
87+
private let systemSandboxedView: AnyClass? = NSClassFromString("_UIRemoteView")
8588

8689
static let dispatchQueue = DispatchQueue(label: "com.posthog.PostHogReplayIntegration",
8790
target: .global(qos: .utility))
@@ -283,6 +286,15 @@
283286
}
284287
}
285288

289+
// detect any views that don't belong to the current process (likely system views)
290+
if config.sessionReplayConfig.maskAllSandboxedViews,
291+
let systemSandboxedView,
292+
view.isKind(of: systemSandboxedView)
293+
{
294+
maskableWidgets.append(view.toAbsoluteRect(window))
295+
return
296+
}
297+
286298
// if its a generic type and has subviews, subviews have to be checked first
287299
let hasSubViews = !view.subviews.isEmpty
288300

@@ -380,6 +392,24 @@
380392
image.imageAsset?.value(forKey: "_containingBundle") != nil
381393
}
382394

395+
// Photo library images have a UUID identifier as _assetName (e.g 64EF5A48-2E96-4AB2-A79B-AAB7E9116E3D)
396+
// SF symbol and bundle images have the actual symbol name as _assetName (e.g chevron.backward)
397+
private func isPhotoLibraryImage(_ image: UIImage) -> Bool {
398+
guard config.sessionReplayConfig.maskPhotoLibraryImages else {
399+
return false
400+
}
401+
402+
guard let assetName = image.imageAsset?.value(forKey: "_assetName") as? String else {
403+
return false
404+
}
405+
406+
if assetName.isEmpty { return false }
407+
if image.isSymbolImage { return false }
408+
if isAssetsImage(image) { return false }
409+
410+
return true
411+
}
412+
383413
private func isAnyInputSensitive(_ view: UIView) -> Bool {
384414
isTextInputSensitive(view) || config.sessionReplayConfig.maskAllImages
385415
}
@@ -429,14 +459,21 @@
429459
}
430460

431461
private func isImageViewSensitive(_ view: UIImageView) -> Bool {
432-
var isAsset = false
433-
if let image = view.image {
434-
isAsset = isAssetsImage(image)
435-
} else {
436-
// if there's no image, there's nothing to mask
437-
return false
462+
// if there's no image, there's nothing to mask
463+
guard let image = view.image else { return false }
464+
465+
// sensitive, regardless
466+
if view.isNoCapture() {
467+
return true
438468
}
439-
return (config.sessionReplayConfig.maskAllImages && !isAsset) || view.isNoCapture()
469+
470+
if config.sessionReplayConfig.maskAllImages {
471+
// asset images are probably not sensitive
472+
return !isAssetsImage(image)
473+
}
474+
475+
// try to detect user photo images
476+
return isPhotoLibraryImage(image)
440477
}
441478

442479
private func toWireframe(_ view: UIView) -> RRWireframe? {

PostHog/Replay/PostHogSessionReplayConfig.swift

+11
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,17 @@
1818
/// Default: true
1919
@objc public var maskAllImages: Bool = true
2020

21+
/// Enable masking of all sandboxed system views
22+
/// These may include UIImagePickerController, PHPickerViewController and CNContactPickerViewController
23+
/// Experimental support
24+
/// Default: true
25+
@objc public var maskAllSandboxedViews: Bool = true
26+
27+
/// Enable masking of images that likely originated from user's photo library
28+
/// Experimental support (UIKit only)
29+
/// Default: true
30+
@objc public var maskPhotoLibraryImages: Bool = true
31+
2132
/// Enable capturing network telemetry
2233
/// Experimental support
2334
/// Default: true

0 commit comments

Comments
 (0)