Problem/Motivation
The mcp_sentinel_config_set tool writes configuration without validating it against the config schema. McpConfigSetTool::doExecute() checks the profile, the rate limit, the denied config types and the approval gate, then calls set() for each key and save(). Nothing validates the resulting data.
Many modules validate their settings only in a settings form or in a config import validator. Neither runs on this path. A governed agent that is allowed to write a config object can save values the owning module would refuse, and the tool reports success.
Examples from modules that ship their own import validation:
- A mail-suppression module treats a malformed policy map as "refuse every recipient". One write blocks all site mail.
- A file-gating module checks that a gated field uses the private scheme on import only. A write can mark a public field as gated.
- Audit Chain keeps writing when
hash_keyis blank, so a write can turn a keyed chain into an unkeyed one.
The write is audited and the denylist still applies, so this is not an access bypass. It is a missing check: the tool accepts data that a config import of the same values would reject.
Steps to reproduce
- Use a profile with
allow_config_writeand a config name that is not denied. - Call
mcp_sentinel_config_setwith a value of the wrong type for a key that has a schema, or a value the owning module's import validator rejects. - The tool returns success and the invalid value is active.
Found by reading the code in 2.22.1. No typed-config validation call exists in src/ or the submodules' write paths.
Proposed resolution
- Before saving, build the merged data and validate it with typed config (
TypedConfigManager::createFromNameAndData()thenvalidate()). Refuse on any violation and return the property paths, not the values. - Refuse a config name that has no schema unless the profile opts in. Keep the default strict and put the relaxation in the profile.
- Log a refused write through the existing denied-access path so it appears in the audit log.
- Consider dispatching the same validation event a config import uses, so module import validators run for this path too.
Remaining tasks
- Kernel tests: wrong type refused, constraint violation refused, valid write saved, schema-less name refused by default, refusal audited.
- Confirm behaviour on the Drupal 10.6 lane, where fewer core config objects carry constraints.
API changes
None. A write that saved invalid data before is now refused.
Comments
Comment #2
jmcerdaCommitted to 1.x. A governed config write is now validated before it is saved or queued. The merged data is built on a clone and validated with typed config, then run through the same import validation event a single-object import uses, against a read-only overlay of the active storage, so a module's import validator sees the change. A violation refuses the write and returns property paths only, never values. A config name with no schema is refused unless the profile sets the new
allow_schemaless_config_write, which ships off; update 10024 adds it to existing profiles. Validation runs before the approval event, and the approval executor validates again at replay. Refusals are audited. One thing to know: an object that already violates its own schema refuses every governed write until it is repaired. While testing, the clone turned out to matter: the config factory caches mutable objects, so without it a refused value could be saved by the next writer in the same request.