Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: detect and mask out photo library and user photos #261

Merged
merged 6 commits into from
Nov 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ disabled_rules:
- trailing_comma
- opening_brace

line_length: 160
line_length:
warning: 160
ignores_comments: true
ioannisj marked this conversation as resolved.
Show resolved Hide resolved

file_length:
warning: 1000
error: 1200
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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))
Expand Down
51 changes: 44 additions & 7 deletions PostHog/Replay/PostHogReplayIntegration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
//
#if os(iOS)
import Foundation
import PhotosUI
import SwiftUI
import UIKit
import WebKit
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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)
ioannisj marked this conversation as resolved.
Show resolved Hide resolved
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 }
marandaneto marked this conversation as resolved.
Show resolved Hide resolved

return true
}

private func isAnyInputSensitive(_ view: UIView) -> Bool {
isTextInputSensitive(view) || config.sessionReplayConfig.maskAllImages
}
Expand Down Expand Up @@ -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() {
ioannisj marked this conversation as resolved.
Show resolved Hide resolved
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? {
Expand Down
11 changes: 11 additions & 0 deletions PostHog/Replay/PostHogSessionReplayConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
marandaneto marked this conversation as resolved.
Show resolved Hide resolved

/// Enable capturing network telemetry
/// Experimental support
/// Default: true
Expand Down
Loading