Skip to content

Add privacy cover to prevent card data in app-switcher snapshots - #558

Open
robert-goheen-cko wants to merge 6 commits into
mainfrom
general-and-security-refactoring
Open

robert-goheen-cko wants to merge 6 commits into
mainfrom
general-and-security-refactoring

Conversation

@robert-goheen-cko

Copy link
Copy Markdown

Changes:

  • Add a lazily created opaque privacyCoverView in FramesPaymentViewController.
  • setupPrivacyCover() pins it full-screen over view in viewDidLoad; it starts hidden.
  • registerPrivacyCoverHandlers() / deregisterPrivacyCoverHandlers() subscribe to UIApplication.willResignActiveNotification / didBecomeActiveNotification (and their UIScene equivalents on iOS 13+) in viewWillAppear / viewWillDisappear.
  • addPrivacyCover() brings the cover to front and shows it; removePrivacyCover() hides it again.
  • Update testPaymentViewsHierarchy to reflect the new subview count (2).
  • Add four new unit tests covering cover visibility on resign/become- active and that observers are cleaned up on viewWillDisappear.

Issue

Issue ticket number and link.

Proposed changes

Describe the big picture of your changes here to communicate to the maintainers why we should accept this pull request. If it fixes a bug or resolves a feature request, be sure to link to that issue.

Test Steps

If there's any functionality change, please list a step by step guide of how to verify the changes, and/or upload a screen recording for any visible changes.

For instance:

  1. Go to the main screen
  2. Tap on primary button
  3. Verify the image is shown on the screen

Checklist

Put an x in the boxes that apply. You can also fill these out after creating the PR. If you're unsure about any of them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your code.

  • Reviewers assigned
  • I have performed a self-review of my code and manual testing
  • Lint and unit tests pass locally with my changes
  • I have added tests that prove my fix is effective or that my feature works
  • I have added necessary documentation (if applicable)

Further comments

If this is a relatively large or complex change, kick off the discussion by explaining why you choose the solution you did and what alternatives you considered, etc...

… snapshots

When the app resigns active, iOS captures the current window into a
snapshot used in the app-switcher, potentially exposing card number,
expiry, CVV and cardholder name in cleartext (CWE-524,
find_01GGqWLbh2boaS9unYDSQa8U).

Changes:
- Add a lazily created opaque privacyCoverView in
  FramesPaymentViewController.
- setupPrivacyCover() pins it full-screen over view in viewDidLoad; it
  starts hidden.
- registerPrivacyCoverHandlers() / deregisterPrivacyCoverHandlers()
  subscribe to UIApplication.willResignActiveNotification /
  didBecomeActiveNotification (and their UIScene equivalents on iOS 13+)
  in viewWillAppear / viewWillDisappear.
- addPrivacyCover() brings the cover to front and shows it;
  removePrivacyCover() hides it again.
- Update testPaymentViewsHierarchy to reflect the new subview count (2).
- Add four new unit tests covering cover visibility on resign/become-
  active and that observers are cleaned up on viewWillDisappear.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unfiltered scene notifications let unrelated windows change the payment cover’s visibility, while missed activation events can leave the opaque, touch-blocking cover stuck onscreen.

2 open issues
What changed in this PR

Adds a privacy cover to the payment form to prevent card details from appearing in app-switcher snapshots.

Changes:

  • Adds an opaque, full-view overlay controlled by application and scene activation notifications.
  • Registers and removes privacy observers as the payment controller appears and disappears.
  • Updates view-hierarchy assertions and adds four tests for cover visibility and observer cleanup.
File Description
Tests/​UI/​PaymentForm/​PaymentViewControllerTests.swift Updates hierarchy assertions and tests privacy-cover behavior.
Source/​UI/​PaymentForm/​ViewController/​FramesPaymentViewController.swift Adds the privacy overlay and lifecycle notification handlers.

Review effort: Balanced

…stration

- registerPrivacyCoverHandlers now binds UIScene notifications to the
  specific UIWindowScene that hosts the payment form's window (object:
  owningScene). This prevents deactivation events from an unrelated
  scene/window from toggling the privacy cover incorrectly.
- When no windowScene is available (pre-scene hosts, unit tests) the
  implementation falls back to UIApplication-level notifications,
  preserving backward compatibility.
- syncPrivacyCoverToCurrentState() is called after adding observers to
  align cover visibility with the actual foreground/background state at
  registration time. This eliminates a race where the cover could remain
  opaque if the app became active while the VC was off-screen and the
  activation notification was not received.
- Added regression test testPrivacyCoverSyncedOnReappearAfterMissedActivation
  covering the stale-cover scenario described above.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

A critical first-presentation lifecycle issue can leave the privacy cover hidden when the payment scene deactivates, exposing card data in snapshots.

1 open issue
2 resolved since last review

Review effort: Balanced

viewWillAppear can run before the view has a window, so
view.window?.windowScene resolves to nil and registerPrivacyCoverHandlers
falls back to application-level notifications. If the payment scene then
deactivates independently (while another scene keeps the app active),
the app-level notification never fires and the cover stays hidden.

Add viewDidAppear which, once the view is guaranteed to be in the window
hierarchy, tears down the previously registered observers and re-registers
using the now-available UIWindowScene. The sync performed inside
registerPrivacyCoverHandlers then aligns cover visibility with the current
scene state. If view.window?.windowScene is still nil (non-scene hosts,
unit tests), the method is a no-op and app-level observers remain active.

Add regression test testPrivacyCoverObserversUpgradedAfterWindowAttachment
to confirm observers are live and functional after the viewDidAppear path.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Privacy observers are removed before the payment form is fully hidden, leaving card fields exposed during navigation transitions, and the scene-observer upgrade path lacks effective test coverage.

1 open issue
1 previously missed issue

In code that hasn't changed since last review

High severity Keep privacy observers active until the navigation transition completes

Source/​UI/​PaymentForm/​ViewController/​FramesPaymentViewController.swift:128

Removing the observers in viewWillDisappear leaves the payment form unprotected while it is still visible during an animated or interactive navigation transition. For example, backgrounding during an unfinished back swipe delivers the deactivation notification after deregistration, so the cover stays hidden over the visible card fields. Keep privacy observation active until viewDidDisappear, and update the observer-cleanup test to verify protection during the transition as well as cleanup afterward.

1 resolved since last review

Review effort: Balanced

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Privacy observers are removed while card data may still be visible during transitions, creating a capture risk, and the scene-observer handoff lacks effective regression coverage.

3 open issues

Review effort: Balanced

…ne provider

Add an internal windowSceneProvider closure to FramesPaymentViewController
so tests can supply a scene token without requiring a real scene-backed window.
In production the closure is nil and the implementation falls back to
view.window?.windowScene unchanged.

Extract resolvedWindowScene() private helper (iOS 13+) that consults the
provider first, then view.window?.windowScene. Both registerPrivacyCoverHandlers
and viewDidAppear now call this helper, keeping the resolution logic in one place.

Replace the previous testPrivacyCoverObserversUpgradedAfterWindowAttachment
with a test that actually exercises the upgrade branch:
- Phase 1: windowSceneProvider returns nil → app-level observers registered
- Phase 2: provider returns a scene token → viewDidAppear upgrades to scene-scoped
- Asserts app-level notifications no longer affect the cover after the upgrade
- Asserts owning-scene deactivation shows the cover
- Asserts an unrelated-scene notification does not affect the cover

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@robert-goheen-cko
robert-goheen-cko requested a balanced review from Copilot November 11, 2026 09:40
…tions

Move deregisterPrivacyCoverHandlers() from viewWillDisappear to viewDidDisappear.
During an animated or interactive dismissal the payment view is still visible and
in the view hierarchy, so observers must remain active until the transition
completes and the view has fully left the screen.

Update testPrivacyCoverObserversRemovedOnDisappear to assert both sides of this
boundary: cover still responds between viewWillDisappear and viewDidDisappear,
and stops responding after viewDidDisappear deregisters the observers.

Update testPrivacyCoverSyncedOnReappearAfterMissedActivation to call
viewDidDisappear (the new deregistration point) instead of viewWillDisappear.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Privacy observers are removed in viewWillDisappear while payment fields may still be visible, leaving sensitive card data unprotected if the scene deactivates before the transition completes.

No open issues 🎉

3 resolved since last review

Review effort: Balanced

@sonarqubecloud

Copy link
Copy Markdown

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Security-sensitive redaction still needs device-level validation of real iOS app-switcher snapshots, since notification-driven tests cannot verify UIKit capture timing with keyboards visible, scene-based hosts, or navigation transitions.

No open issues 🎉

Review effort: Balanced

@robert-goheen-cko
robert-goheen-cko marked this pull request as ready for review November 11, 2026 10:21
@robert-goheen-cko
robert-goheen-cko requested review from a team November 11, 2026 10:28

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants