Split out of the review of #459 (spp_change_request_v2 field-mapping transforms), where the same class of safe_eval sandbox escape was the headline finding. This one is in spp_programs and is out of that PR's scope, but it is the same defect.
The hole
spp_programs/models/managers/base_manager.py _get_eval_context() returns:
return {
"datetime": safe_eval.datetime,
"dateutil": safe_eval.dateutil,
"time": safe_eval.time,
"uid": self.env.uid,
"user": self.env.user, # <-- live recordset
}
user is a live res.users recordset. Odoo 19's safe_eval applies no attribute allowlist — only dunder-name blocking and the fixed _UNSAFE_ATTRIBUTES list (frames/code/mro/tracebacks). Recordsets pass check_values. So a mode="eval" expression evaluated in this context can reach the full ORM and the DB cursor via the recordset's own attributes:
user.env['res.users'].sudo().search([]).mapped('login')
user.sudo()
user._cr.execute("...") # raw SQL, ACL/record-rule/audit bypass
user.env['ir.config_parameter'].sudo().get_param('database.secret')
The nosemgrep justification on the safe_eval call claims "the evaluation context only exposes datetime/dateutil/time and the current user" — which is the same blind spot: handing in user hands in user.env.
This was proven at runtime for the identical pattern in #459 (two independent reviewers executed the escapes against Odoo 19's real safe_eval in-container). The mechanism is not module-specific.
What to check when fixing
- Who authors the expressions and who triggers evaluation.
_safe_eval here backs eligibility / entitlement / payment domain and condition expressions. Determine the trust boundary: which roles can write these expressions (config-time), and whether any evaluation runs under sudo/superuser or in a low-privileged user's session. That sets the severity (trusted-admin config vs privilege escalation).
- Enumerate every caller of
_safe_eval / _get_eval_context in spp_programs and downstream managers.
Fix direction (mirror #459's resolution)
- Don't put a live recordset in the context. If expressions need user attributes, pass a handle-free snapshot (e.g.
SimpleNamespace(id=..., login=..., ...)) or just the specific scalars (uid is already there). uid alone is safe; user is the problem.
- If author-side restriction is the intended boundary, enforce it with field-level
groups=/ACLs rather than help-text ("only admins should…" is not enforcement), and add a test that pins it.
- Correct the nosemgrep justification once the recordset is out.
Context: #459 (review — findings C1/C2 and suggestion S3), spp_change_request_v2/strategies/field_mapping.py for the resolved pattern.
Split out of the review of #459 (
spp_change_request_v2field-mapping transforms), where the same class ofsafe_evalsandbox escape was the headline finding. This one is inspp_programsand is out of that PR's scope, but it is the same defect.The hole
spp_programs/models/managers/base_manager.py_get_eval_context()returns:useris a liveres.usersrecordset. Odoo 19'ssafe_evalapplies no attribute allowlist — only dunder-name blocking and the fixed_UNSAFE_ATTRIBUTESlist (frames/code/mro/tracebacks). Recordsets passcheck_values. So amode="eval"expression evaluated in this context can reach the full ORM and the DB cursor via the recordset's own attributes:The nosemgrep justification on the
safe_evalcall claims "the evaluation context only exposes datetime/dateutil/time and the current user" — which is the same blind spot: handing inuserhands inuser.env.This was proven at runtime for the identical pattern in #459 (two independent reviewers executed the escapes against Odoo 19's real
safe_evalin-container). The mechanism is not module-specific.What to check when fixing
_safe_evalhere backs eligibility / entitlement / payment domain and condition expressions. Determine the trust boundary: which roles can write these expressions (config-time), and whether any evaluation runs undersudo/superuser or in a low-privileged user's session. That sets the severity (trusted-admin config vs privilege escalation)._safe_eval/_get_eval_contextinspp_programsand downstream managers.Fix direction (mirror #459's resolution)
SimpleNamespace(id=..., login=..., ...)) or just the specific scalars (uidis already there).uidalone is safe;useris the problem.groups=/ACLs rather than help-text ("only admins should…" is not enforcement), and add a test that pins it.Context: #459 (review — findings C1/C2 and suggestion S3),
spp_change_request_v2/strategies/field_mapping.pyfor the resolved pattern.