Follow-up from PR #343 (writable selected_field bypasses CR conflict checks). Same root cause, different sink; deliberately out of #343's scope (which is conflict/duplicate detection).
Finding
selected_field_name on spp.change.request is only view/model-readonly (not server-side write protection), and group_cr_user has write on their own change requests. Besides conflict detection (fixed in #343), the field is also read by dynamic-approval routing:
_resolve_dynamic_approval (change_request.py:905) reads self.selected_field_name and feeds self to the CEL evaluator; candidate approval definitions' CEL conditions match on record.selected_field_name (see README/USAGE examples). Called from _get_approval_definition during the approval flow.
So in draft, a user can write({'selected_field_name': <field>}) to make a different candidate CEL condition match — steering the CR to a weaker or no-approval definition — then submit. #343 (conflict) does not touch routing; #264 (cr-dynamic-approval-field) freezes selected_field_name only after the CR leaves draft/revision, so neither closes this draft-time routing manipulation.
Severity: Medium
Requires an authenticated CR user, but lets them bypass the intended approval workflow for their own change request (approve via an easier/catch-all definition). Confidentiality/integrity impact depends on the CR type's field-conditioned approval config.
Suggested fix
Harden selected_field_name so it can only hold the server-derived value:
Best coordinated with #264 (which already adds a write() guard on these fields for the post-submit freeze) to avoid two overlapping write overrides. Needs a design decision on A vs B.
Source: observed at change_request.py:905 / :888 while working internal/plans/sec-cr-selected-field-conflict-bypass.md. Related: #343, #264.
Follow-up from PR #343 (writable selected_field bypasses CR conflict checks). Same root cause, different sink; deliberately out of #343's scope (which is conflict/duplicate detection).
Finding
selected_field_nameonspp.change.requestis only view/model-readonly(not server-side write protection), andgroup_cr_userhas write on their own change requests. Besides conflict detection (fixed in #343), the field is also read by dynamic-approval routing:_resolve_dynamic_approval(change_request.py:905) readsself.selected_field_nameand feedsselfto the CEL evaluator; candidate approval definitions' CEL conditions match onrecord.selected_field_name(see README/USAGE examples). Called from_get_approval_definitionduring the approval flow.So in draft, a user can
write({'selected_field_name': <field>})to make a different candidate CEL condition match — steering the CR to a weaker or no-approval definition — then submit. #343 (conflict) does not touch routing; #264 (cr-dynamic-approval-field) freezesselected_field_nameonly after the CR leaves draft/revision, so neither closes this draft-time routing manipulation.Severity: Medium
Requires an authenticated CR user, but lets them bypass the intended approval workflow for their own change request (approve via an easier/catch-all definition). Confidentiality/integrity impact depends on the CR type's field-conditioned approval config.
Suggested fix
Harden
selected_field_nameso it can only hold the server-derived value:selected_field_name/selected_field_old_value/selected_field_new_value(allow only the_sync_field_to_modifypath via a context flag orsudo), so it always mirrors the validateddetail.field_to_modify. This also makes security(cr): writable selected_field bypasses CR conflict checks #343's conflict fix redundant-safe and closes routing._resolve_dynamic_approval, derive the field fromdetail.field_to_modify(as security(cr): writable selected_field bypasses CR conflict checks #343 does for conflict detection) rather than readingselected_field_name.Best coordinated with #264 (which already adds a
write()guard on these fields for the post-submit freeze) to avoid two overlapping write overrides. Needs a design decision on A vs B.Source: observed at
change_request.py:905/:888while workinginternal/plans/sec-cr-selected-field-conflict-bypass.md. Related: #343, #264.