Skip to content

[Direct Edit] Classic notification channel subscribes to Mercure without any authorization #351

Description

@fashxp

Affected Version

2026.2 (analysed on a 2026.1 installation running pimcore/pimcore: 2026.x-dev). The code is the
same on the other supported lines.

Affected capability

Other (Direct Edit / Portal Engine / Mercure)

Steps to reproduce

  1. Install Direct Edit and Portal Engine, with the Mercure hub as shipped in pimcore/skeleton and
    pimcore/demo-enterprise (MERCURE_EXTRA_DIRECTIVES: anonymous).
  2. Remove the anonymous directive from the hub, so it only accepts subscriptions that present a
    valid JWT.
  3. Open an asset in a Portal Engine data pool and start a Direct Edit session.

Actual Behavior

The Direct Edit status indicator in Portal Engine never updates: its Mercure subscription is
rejected with 401 and the failure is invisible, because eventSource.onerror is an empty
function while the browser retry-loops.

The reason is that the non-Studio Direct Edit channel has no authorization at all. Direct Edit
notifies the browser over two separate Mercure channels:

1. Studio channel. Service\Studio\DirectEditService publishes through StudioBackendBundle's
PublishServiceInterface to the Studio user topic. publish($topic, $payload) is called without a
third argument, so these are private updates, and the Studio frontend consumes them through
Studio's GlobalMessageBus. Authorized by Studio's mercureAuthorization cookie. Fine.

2. "Classic" channel. ClientCommunicationService::sendClientUploadEvent() and
sendStartLocalEditingEvent() publish through Direct Edit's own PublishService::sendUpdate():

$update = new Update($topic, json_encode($data), false);   // private = false

to http://www.pimcore.com/direct-edit/client-upload/user/<applicationPrefix><userId>, and the
browser subscribes to the hub directly. Nothing hands that browser a subscriber token.
PublishService::getClientJwtCookie() still exists but has zero callers anywhere in vendor: the
Admin Classic controller that used to set the cookie was removed, and Direct Edit's
doc/10_Integrate_into_Custom_Application.md documents issuing a token as the integrator's job.

The channel works today only because the updates are published public and the hub accepts
anonymous subscribers.

Portal Engine is a first-party consumer of this channel.
Controller/DataPool/AssetController.php passes MercureUrlService::getClientSideUrl() and the
prefixed portal user id into the frontend config, and
assets/scripts/features/asset/components/detail/DirectEditStatus.js opens:

const url = new URL(getConfig("directEdit.mercureUrl"))
url.searchParams.append("topic", 'http://www.pimcore.com/direct-edit/client-upload/user/' + getConfig("directEdit.userId"))
const eventSource = new EventSource(url)     // no credentials, no token

Portal users authenticate as PortalUser, a context entirely separate from Studio users, so they
never receive Studio's cookie and there is no other cookie to fall back on.

Two further defects in that component, independent of the authorization question:

  • the useEffect has no cleanup, so the EventSource is never closed on unmount and every mount
    of the asset detail view leaves another open hub subscription behind;
  • eventSource.onerror = event => {} swallows every failure, which is what makes the 401 above
    invisible.

This is the reason the hub hardening proposed in #346 had to be dropped
(pimcore/demo-enterprise#528 and pimcore/skeleton#287 were closed): anonymous is currently
load-bearing for a shipped bundle.

Not affected: the desktop client pimcore/direct-edit-client subscribes to no Mercure topic at all.
It uses pimcorefile:// plus HTTPS download/upload and a chokidar file watch; eventsource
appears only in its devDependencies and is required by no source file.

Expected Behavior

Every Mercure subscription Pimcore opens is authorized, so the hub can be configured to reject
unauthorized subscribers.

Suggested direction:

Direct Edit

Portal Engine

  • Fetch that token before opening the EventSource (or pass withCredentials: true if the
    mechanism ends up cookie-based), and renew it for as long as the page is open.
  • Close the EventSource in a useEffect cleanup.
  • Give onerror a real handler, so a refused or dropped subscription is visible instead of silent.

Skeleton / demo

Verification

Against a throwaway dunglas/mercure, publishing a public update on the Direct Edit topic:

Subscriber hub with anonymous hub without
no token (Portal Engine, custom app) 200, delivered 401
any valid subscriber JWT, topic not in its subscribe claim 200, delivered 200, delivered

The subscribe claim only gates private delivery; any valid token is enough to connect and receive
public updates on any topic. A token per user is therefore already enough to unblock the hub
change, and publishing private is the follow-on step.

Related: #346 (the Studio side of the same authorization model), fixed by
pimcore/studio-ui-bundle#4009 and pimcore/studio-backend-bundle#2012. Direct Edit in Studio was a
second victim of that bug - its "upload complete" notification is a private update on the Studio
user topic, so it was being dropped along with everything else after an hour.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Fields

    Affected capability

    None yet

    Platform Version

    None yet

    Galaxy

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions