Problem/Motivation

Every place the module advertises which languages a resource has content in builds the list from getTranslationLanguages() with no access check:

  • the strict 406's error detail and meta.availableTranslations (#3612568),
  • the hreflang attribute on the self link of every individual read,
  • the per-item details of meta.omitted entries on the related endpoint (#3612537), on included resources (#3613728), and on the jsonapi_menu_items tree (#3612677).

A low-privilege or anonymous client can therefore learn that unpublished or otherwise view-restricted translations EXIST, and in which languages, without any right to see them. This is metadata disclosure rather than content disclosure, but "an fr translation of this node exists" can itself be sensitive: unannounced launches, embargoed content, work in progress.

Steps to reproduce

  1. Languages en (default) and fr. An article has a published en translation and an unpublished fr translation.
  2. As an anonymous user, GET /jsonapi/node/article/{uuid} with Accept-Language: de. Expected: the 406 advertises en only. Actual: detail and meta.availableTranslations name fr.
  3. As an anonymous user, GET /jsonapi/node/article/{uuid}. Expected: the hreflang attribute lists en. Actual: it lists en,fr.
  4. The same list leaks through the omission details of related, included and menu-items responses whose target has the unpublished translation.

Proposed resolution

Advertise only translations the requesting user may view, and filter at the advertisement sites, not in the negotiator:

  • A viewableTranslationLangcodes() helper on the controller keeps langcodes whose translation's access('view') is allowed (sorted), and feeds the 406 lists, targetAvailableLangcodes() and the hreflang attribute. Small, deliberately duplicated loops (commented) cover the menu-items miss recording in RequestLanguageEntityRepository, which must stay dependency-light, and the include-omission reason in EntityAccessChecker.
  • Language SELECTION is unchanged: negotiation still operates over all existing translations, and a selected translation the user cannot view keeps returning an access error, matching core JSON:API's entity-level posture where 403-vs-404 already distinguishes existence. Only what gets printed is filtered.
  • Cacheability follows the filter: every access result, allowed or denied, is attached where a filtered list lands on a cacheable response (the hreflang link's CacheableMetadata, the omission entries' access result), so users with different access are served correctly different lists. The 406s are exempt because they are max-age 0 by construction.
  • An empty viewable list advertises nothing: the "Available translations" clause is omitted from the detail rather than rendered empty, and the 406 omits meta.availableTranslations entirely.
  • Two surfaces stay unfiltered because they do not expose per-entity data: the menu 406's list of the site's configured languages (public configuration), and config-entity override availability (config overrides have no per-translation access concept).
  • Entity-type quirk, decided and recorded: core's MenuLinkContentAccessControlHandler gates view behind the administer menu permission for the whole entity, and menu_link_content's enabled field is not translatable, so there is no per-translation restricted state on menu links and the filter empties their advertised lists for every non-admin client. The filter applies as specified anyway: an anonymous menu omission report names the omitted link but no languages, while a user with administer menu sees the full list. The report still tells the client the link was omitted for language reasons.
  • The advertised lists become access-dependent rather than absolute, by design: clients cannot assume list stability across principals.
Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

lauriii created an issue. See original summary.

  • lauriii committed e74a9663 on 1.0.x
    task: #3613770 Advertised translation metadata discloses translations...
lauriii’s picture

Status: Active » Fixed

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.