Problem
SendEvent carries the full Setono\MetaConversionsApi\Event\Event object. At dispatch time (DispatchOnCommandBusSubscriber) that object contains:
$event->pixels: Pixel objects including accessToken, populated by PopulatePixelsSubscriber from the pixels configuration.
$event->userData: raw email, phone, names, date of birth, external ids, exactly as the application set them. Normalisation and SHA-256 hashing only happen in Parameters::getPayload(), which is called inside Client::sendEvent() in the handler.
When the application routes SendEvent to an async transport (the recommended setup), the PHP-serialized message with the access token and the unhashed personal data is written to the transport storage (Doctrine table, Redis, AMQP, SQS), to the failure transport on failure, and to anything that dumps messages (messenger:failed:show, monitoring).
Impact
- The Conversions API access token ends up in queue storage and failure transports, which are usually less protected than
.env or a vault, and are rarely rotated.
- Personal data is stored unhashed in one more system. Failure transports are often kept indefinitely, which is a retention problem under GDPR.
Suggested fix
- Access tokens. Keep them out of the message. Strip
accessToken from the pixels before dispatch and resolve tokens in the handler through a small interface (for example AccessTokenResolverInterface::resolve(string $pixelId): ?string, default implementation backed by the pixels configuration). Equivalent: split PixelProviderInterface into "which pixels apply to this request" (request time) and "token for pixel id" (send time).
- PII. Hash on the request side: call
getPayload() before dispatching and send the ready-to-post payload array plus the pixel ids and the test event code in the message, instead of the mutable Event object. This also makes the message serializable with the Symfony Serializer (the current message holds objects with untyped properties and DateTimeInterface values, which only the PHP serializer handles). If the Event object must stay in the message, document that the transport stores raw PII and the access token.
Tests
Serialize a SendEvent (or its replacement) with serialize() and assert the string contains neither the access token nor the raw email address.
Problem
SendEventcarries the fullSetono\MetaConversionsApi\Event\Eventobject. At dispatch time (DispatchOnCommandBusSubscriber) that object contains:$event->pixels:Pixelobjects includingaccessToken, populated byPopulatePixelsSubscriberfrom thepixelsconfiguration.$event->userData: raw email, phone, names, date of birth, external ids, exactly as the application set them. Normalisation and SHA-256 hashing only happen inParameters::getPayload(), which is called insideClient::sendEvent()in the handler.When the application routes
SendEventto an async transport (the recommended setup), the PHP-serialized message with the access token and the unhashed personal data is written to the transport storage (Doctrine table, Redis, AMQP, SQS), to the failure transport on failure, and to anything that dumps messages (messenger:failed:show, monitoring).Impact
.envor a vault, and are rarely rotated.Suggested fix
accessTokenfrom the pixels before dispatch and resolve tokens in the handler through a small interface (for exampleAccessTokenResolverInterface::resolve(string $pixelId): ?string, default implementation backed by thepixelsconfiguration). Equivalent: splitPixelProviderInterfaceinto "which pixels apply to this request" (request time) and "token for pixel id" (send time).getPayload()before dispatching and send the ready-to-post payload array plus the pixel ids and the test event code in the message, instead of the mutableEventobject. This also makes the message serializable with the Symfony Serializer (the current message holds objects with untyped properties andDateTimeInterfacevalues, which only the PHP serializer handles). If theEventobject must stay in the message, document that the transport stores raw PII and the access token.Tests
Serialize a
SendEvent(or its replacement) withserialize()and assert the string contains neither the access token nor the raw email address.