Problem/Motivation

The module stores its state in two base fields on menu links, menu_autopilot (a map) and menu_autopilot_dynamic, both marked internal. Internal keeps them out of API responses. It does not keep API clients from writing them.

JSON:API filters internal fields when it normalizes an entity, but its resource type still lists them as enabled, and the denormalizer only skips fields that are disabled. The module defines no field access for the two fields, so a client that may update a menu link may also send menu_autopilot in a PATCH. The client can never read the field back, so the write leaves no visible trace.

What a write can do:

  • Mark any link as a managed child of a node, so the next sync renames, moves or prunes it.
  • Turn a link into a dynamic parent with a source of its choosing. With the replace policy that deletes the parent's other children on the next sync.
  • Set or clear the flag that lets a later sync re-enable a link, including on a link an editor disabled on purpose.

The account needs update access to menu links, which is already a strong permission. The difference is that these writes skip the validation the form applies to a source descriptor, and they cause deletes on sites where the same account is not allowed to delete menu links.

Steps to reproduce

  1. As an account with update access to a menu link, PATCH the link over JSON:API with a menu_autopilot attribute.
  2. The request succeeds and the map is stored. A GET does not show it.

Found by reading 1.x and core's JSON:API denormalizer. Not yet reproduced on a site.

Proposed resolution

  • Implement field access for both fields and forbid edit for every account. The module writes the fields from its own code, which field access does not affect. API layers and form widgets then refuse the write.
  • Keep view forbidden as well, so the rule does not depend on the internal flag alone.
  • Add a functional test that PATCHes the field over JSON:API and expects a refusal, and one that proves the module's own form and sync still write it.

Remaining tasks

  • Check REST and GraphQL the same way.
  • Note in the README that the fields are module-owned and not writable over any API.

API changes

An API write to either field, which used to succeed without effect on the response, is refused.

Comments

jmcerda created an issue. See original summary.

jmcerda’s picture

Status: Active » Fixed

Committed to 1.x; ships in 1.4.0. Reproduced before the fix on Drupal 11.4 with an account that holds only administer menu: a JSON:API PATCH of menu_autopilot returned 200 and stored the map, including a source descriptor with the replace policy. Core REST accepted the same write, and POST accepted it on create. A PATCH of menu_autopilot_dynamic returned 200 and was not kept, because presave recomputes it. The module now forbids every field operation on both fields for every account. Its own form, sync and Drush paths write the fields from code and are unaffected. Forbidding view also closed a second gap found while testing: a JSON:API filter or sort on menu_autopilot_dynamic listed the dynamic parents; it now returns 403. A map that an API client wrote before this release is not detected or reverted.

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.

  • jmcerda committed 8fcb6564 on 1.x
    #3624463: Cover the collection filter and sort, narrow two claims...

  • jmcerda committed 3f4d135a on 1.x
    #3624463: Forbid field access on the two internal menu link fields
    
    JSON...

  • jmcerda committed 8fcb6564 on cursor/empty-managed-menus-b880
    #3624463: Cover the collection filter and sort, narrow two claims...

  • jmcerda committed 3f4d135a on cursor/empty-managed-menus-b880
    #3624463: Forbid field access on the two internal menu link fields
    
    JSON...