security(cr): writable selected_field bypasses CR conflict checks#343
Draft
gonzalesedwin1123 wants to merge 4 commits into
Draft
security(cr): writable selected_field bypasses CR conflict checks#343gonzalesedwin1123 wants to merge 4 commits into
gonzalesedwin1123 wants to merge 4 commits into
Conversation
…e selected_field_name Conflict and duplicate detection treated a non-empty selected_field_name as 'only that field is a proposed change'. selected_field_name is only view-readonly; group_cr_user has write on their own spp.change.request, so a user could create a CR blocked by a field-scoped conflict rule, then RPC-write selected_field_name to a field outside conflict_fields - _filter_by_field_conflicts returned empty, resetting conflict_status to none and allowing submission without the configured override path. The duplicate-detection path (_calculate_similarity) had the same flaw. Derive the effective changed field server-side from the detail's validated field_to_modify Selection via a new _effective_selected_field() helper, gated on use_dynamic_approval (non-dynamic types always use the full conflict_fields). The writable selected_field_name is no longer trusted for these decisions. Bump 19.0.3.0.0 -> 19.0.3.0.3 (above #261's .0.2 and #264's .0.1 on the same module) + HISTORY. No migration: behavior fix on existing fields. Scope: conflict/duplicate detection only. The same writable field is also read by dynamic-approval routing (_resolve_dynamic_approval); filed as a follow-up.
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## 19.0 #343 +/- ##
==========================================
- Coverage 74.28% 70.33% -3.95%
==========================================
Files 372 205 -167
Lines 25385 17911 -7474
==========================================
- Hits 18857 12598 -6259
+ Misses 6528 5313 -1215
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
Member
Author
|
Filed the related dynamic-approval routing bypass (same writable |
…hangelog Applies CI's pinned oca-gen output verbatim for the new HISTORY fragment.
…a writable label
Staff review found the first fix incomplete: it derived the effective field from
detail.field_to_modify, but that field is ALSO user-writable and does not
constrain apply — field_mapping.apply writes every mapped field that differs
from the registrant, not just field_to_modify. So the bypass survived with one
extra field: write({'field_to_modify': 'phone', 'given_name': 'X'}) cleared the
given_name conflict while still applying the given_name change.
Derive the proposed changes server-side from the detail-vs-registrant diff (the
exact set apply writes) via _proposed_changed_fields(), and scope both
_filter_by_field_conflicts and _calculate_similarity to it. Neither
selected_field_name nor field_to_modify is trusted for the decision.
Add field mappings to the dynamic test type (a field_mapping type needs them)
and two regression tests for the mislabel bypass (own-side and candidate-side).
339 tests pass.
….3.0.3 changelog Applies CI's pinned oca-gen output verbatim for the corrected HISTORY fragment.
Member
Author
This was referenced Jul 24, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Conflict and duplicate detection in
spp_change_request_v2scoped field-levelchecks using a user-writable label.
selected_field_nameis onlyview/model-
readonly(not server write-protection) andgroup_cr_userhaswrite on their own
spp.change.request. So a user could create a CR blocked bya field-scoped conflict rule, then RPC-write the label to a field NOT in the
rule's
conflict_fields; the check re-runs, returns empty,conflict_statusresets to
none, and the CR submits/approves without the configured overridepath.
_calculate_similarity(duplicate detection) had the identical flaw.Fix
Scope field-level conflict/duplicate detection to the fields that actually
differ from the registrant — the exact set the
field_mappingapply strategywill write — derived server-side, not from any declared label:
_proposed_changed_fields()on the conflict mixin: foruse_dynamic_approvalCR types, returns the mapped detail fields whose value differs from the
registrant (via
apply_mapping_ids);Nonefor non-dynamic types (fullconfigured field set always used — their details aren't registrant-prefilled).
_filter_by_field_conflictsand_calculate_similarityscope to that diff.selected_field_namenor the detail'sfield_to_modifyis trustedfor the security decision.
Why not just "derive from field_to_modify"
The first iteration derived the field from
detail.field_to_modifyinstead ofselected_field_name. A two-reviewer staff review caught that this was stillbypassable:
field_to_modifyis also user-writable, and — crucially — itdoes not constrain apply.
field_mapping.applywrites every mapped field thatdiffers from the registrant, so
write({'field_to_modify': 'phone', 'given_name': 'X'})cleared thegiven_nameconflict while still applying thegiven_namechange. Deriving from the actual diff (this version) makes both writable labels
irrelevant.
Testing
./spp t spp_change_request_v2→ 339 passed, 0 failed, 0 errors (5 new).selected_field_namecannot clear a field-scoped conflict(dynamic and static types) or drop duplicate similarity; mislabeling
field_to_modifywhile actually changing a conflict field cannot bypassdetection (own-side and candidate-side). Each fails against the pre-fix code.
apply_mapping_idsto the dynamic test CR type (afield_mappingtyperequires them; the fixture had omitted them).
Scope / follow-ups
selected_field_namealso steersdynamic-approval routing (
_resolve_dynamic_approval→ CEL). Out of scopefor this conflict-detection fix; best fixed by hardening the field's
writability, coordinated with security(cr): route and apply the same single field for dynamic approvals #264.
cr-dynamic-approval-field): it freezes these fieldspost-submit; this attack is in draft. Confined to
conflict_mixin.pytoavoid a
write()collision with security(cr): route and apply the same single field for dynamic approvals #264. Version19.0.3.0.3(above security(cr): add record rules to CR detail models (ownership + area) #261's.0.2and security(cr): route and apply the same single field for dynamic approvals #264's.0.1); trivial re-merge expected for whichever mergessecond. No migration.
Draft: awaiting human security review before undraft/merge.
Merge order (CR security cluster)
Per the cross-PR interaction analysis (2026-07-24,
internal/plans/security-prs-interaction-analysis.md), hard order: #264 → #261 → #338 → #343 (this PR, last).spp_change_request_v2manifest/HISTORY/README)._proposed_changed_fieldsclaims the derived set is "exactly the set thefield_mappingapply strategy will write" — once security(cr): route and apply the same single field for dynamic approvals #264 lands, apply writes only the selected field's mapping, so the derived set is a superset. One-line comment fix.