ENG-3418: Block approval of unverified duplicate privacy requests#7972
ENG-3418: Block approval of unverified duplicate privacy requests#7972
Conversation
A duplicate request whose identity was never verified could be approved and processed by an admin, bypassing identity verification. This adds a guard in approve_privacy_requests that rejects duplicates with identity_verified_at=None, and hides the approve button in the admin UI for unverified duplicates (deny is still allowed for cleanup). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. 2 Skipped Deployments
|
Codecov Report✅ All modified and coverable lines are covered by tests. ❌ Your project status has failed because the head coverage (84.94%) is below the target coverage (85.00%). You can increase the head coverage or adjust the target coverage. Additional details and impacted files@@ Coverage Diff @@
## main #7972 +/- ##
==========================================
- Coverage 85.04% 84.94% -0.11%
==========================================
Files 631 630 -1
Lines 41213 41086 -127
Branches 4807 4769 -38
==========================================
- Hits 35049 34899 -150
- Misses 5070 5103 +33
+ Partials 1094 1084 -10 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
|
/code-review |
There was a problem hiding this comment.
Code Review: Block approval of unverified duplicate privacy requests
This is a well-scoped security fix. The core guard is placed at exactly the right point in approve_privacy_requests — after the status allowlist check but before the DB write — so it cannot be bypassed by manipulating the request body. Backend and frontend defenses are independent of each other (defense in depth), which is the right pattern.
What's done well
- The
ifblock in the service is placed correctly: after the existingPrivacyRequestStatusallowlist check, before thetryblock that performs the state transition. A caller cannot bypass it. - The frontend guard in
useApproveDenyPrivacyRequestis independent of the backend — the Approve button is simply not rendered for unverified duplicates, not just disabled. - Two new tests cover both the single-request rejection and the mixed bulk-approval scenario with specific assertions on
succeeded/failedcounts and the error message. - The fix to
test_approve_privacy_request(settingidentity_verified_atfor the duplicate parametrize case) is the correct minimal change to keep the existing parametrized test passing under the new invariant. - Changelog entry is present.
Findings
Suggestion — Missing audit log for blocked approval attempt (see inline comment on privacy_request_service.py): Every successful approval and terminal failure (e.g. wrong status) records an AuditLog entry. The unverified-duplicate case silently continues with no audit trail. Since this is a security-relevant event (an admin attempted to approve a request that bypassed identity verification), it would be valuable to log it for compliance and anomaly-detection purposes.
Nit — identity_verified_at type mismatch in PrivacyRequestEntity (see inline comment on useApproveDenyPrivacyRequest.ts): The field is typed as string | undefined in the entity type, but API responses return string | null. Runtime behaviour is correct since !value handles both falsy values, but updating the type to string | null | undefined would prevent subtle bugs if stricter checks are added later.
Nit — Missing privacy_request.delete(db) at the end of test_approve_unverified_duplicate_rejected for consistency with sibling tests (see inline comment on the test file).
Overall this is a solid, well-tested fix. The audit log suggestion is the only thing worth considering before merge.
🔬 Codegraph: connected (47107 nodes)
💡 Write /code-review in a comment to re-run this review.
| data=PrivacyRequestResponse.model_validate( | ||
| privacy_request | ||
| ).model_dump(mode="json"), | ||
| ) |
There was a problem hiding this comment.
src/fides/service/privacy_request/privacy_request_service.py:665
Suggestion: Consider adding an audit log entry for blocked approval attempts
Every successful approval records an AuditLog entry. The unverified-duplicate case is a security-relevant event (an admin attempted to approve a request that skipped identity verification), but it silently continues with no audit trail. Compared to a normal validation failure (e.g. wrong status), this case is more useful to surface for compliance audits or to detect repeated attempts.
Consider adding an AuditLog.create(...) before the continue, using an appropriate existing AuditLogAction (e.g. rejected):
AuditLog.create(
db=db,
data={
"user_id": requester_id,
"privacy_request_id": privacy_request.id,
"action": AuditLogAction.rejected,
"message": "Cannot approve unverified duplicate request",
},
)This keeps it consistent with the rest of the approve flow where all terminal outcomes are logged.
There was a problem hiding this comment.
Done — added a logger.error() call since this path only fires if someone bypasses the UI and hits the API directly. AuditLog wasn't the right fit since denied means an admin denied the request, whereas this is an invalid API call. The error log captures the request ID and user for investigation.
- Add logger.error() for blocked approval of unverified duplicate requests - Fix identity_verified_at type to include null in PrivacyRequestEntity Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Adding do not merge label and converting to draft for now |
The guard added in the previous commit blocked approval of ALL unverified duplicates, but when subject_identity_verification_required is false, identity_verified_at is always None — breaking the admin override for false-positive duplicates in non-verification deployments. Now the guard only fires when identity verification is actually required by the system. Also applies the same logic to both UI paths (detail dropdown and table row actions) for consistency, and adds a test confirming unverified duplicates are approvable when verification is disabled. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Change type from Fixed to Security and link to GHSA-qx5f-ghc2-7g5c.
Ticket ENG-3418
Description Of Changes
A duplicate privacy request whose identity was never verified could be approved and processed by an admin, bypassing identity verification entirely. This happened because
duplicatewas an allowed status for approval (intentional admin override for false-positive duplicates), but there was no secondary check for whether the identity had actually been verified.The fix adds a guard: duplicate requests can only be approved if
identity_verified_atis set. The admin UI also hides the approve button for unverified duplicates. Deny is still allowed for cleanup.How this happens:
identity_unverifiedduplicatebefore user can verifyCode Changes
src/fides/service/privacy_request/privacy_request_service.py— Added guard inapprove_privacy_requests: rejectsduplicate+identity_verified_at is None; logs error for blocked attemptsclients/admin-ui/.../useApproveDenyPrivacyRequest.ts— Hides approve button for unverified duplicates (deny still shown)clients/admin-ui/.../types.ts— Fixedidentity_verified_attype tostring | nullto match API responsetests/.../test_privacy_request_endpoints.py— Addedtest_approve_unverified_duplicate_rejectedandtest_bulk_approve_mixed_verified_unverified_duplicates; updated existing duplicate approve tests to setidentity_verified_atSteps to Confirm
identity_unverifiedduplicateE2E results:
Pre-Merge Checklist
CHANGELOG.mdupdatedmaindowngrade()migration is correct and works