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_key is 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

  1. Use a profile with allow_config_write and a config name that is not denied.
  2. Call mcp_sentinel_config_set with a value of the wrong type for a key that has a schema, or a value the owning module's import validator rejects.
  3. 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() then validate()). 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

jmcerda created an issue. See original summary.

jmcerda’s picture

Status: Active » Fixed

Committed 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.

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.