Field mappings need consumed-source semantics to prevent plaintext passthrough and PII exposure
- Dominant language
- TypeScript
- Stars
- 80
- Forks
- 14
- PR merge metrics
- No merged PRs in 30d
Description
### Summary
WebCM currently passes an event payload to component listeners, but there is no public contract for fields that have been mapped from one source path into a component-specific destination path.
For tracking components, this architecture fails to reliably prevent PII exposure. A source field can be mapped into a destination-specific customer-information field, while the original plaintext source field remains in the payload and is later forwarded as generic event/custom data.
WebCM should support a per-component field-mapping consumption contract so mapped source fields containing PII can be removed before the component sends data to a tracking endpoint, or so mapping metadata is available to the component when it builds the outbound request.
### Problem
The core problem is that WebCM has no place to represent "this source field has already been used to populate a component-specific destination field and must not be forwarded again as generic payload data."
That is a privacy boundary, not just a convenience feature. For analytics and advertising components, mapped destination fields often have special handling such as normalization, hashing, customer-data placement, consent handling, or provider-specific validation. If the original source field remains in the generic payload after mapping, the same PII value can be sent twice:
- once through the intended destination-specific path,
- again as plaintext custom/event data.
The current WebCM architecture makes that hard to prevent because:
- `MCEvent.payload` is created directly from the request body: https://github.com/cloudflare/webcm/blob/v0.10.9/src/manager.ts#L26-L37
- `handleEvent` creates an `MCEvent` and dispatches it to registered listeners for each component: https://github.com/cloudflare/webcm/blob/v0.10.9/src/server.ts#L118-L147
- the public component config shape accepts `settings` and `permissions`, but does not express field mappings, consumed source fields, custom-data allowlists, or payload projection rules: https://github.com/cloudflare/webcm/blob/v0.10.9/src/compConfig.ts#L3-L22
- the component-facing `Manager` API supports event listeners, client listeners, fetch/proxy/route/serve, cache, embeds, and widgets, but does not expose mapping metadata or a pre-dispatch payload transform hook: https://github.com/cloudflare/webcm/blob/v0.10.9/src/manager.ts#L386-L461
As a result, a component can mutate the payload it receives, but it cannot know whether an arbitrary source field was already mapped into a destination field unless the manager removes that source field or passes mapping metadata. Without that contract, components that forward leftover payload data cannot distinguish intentional custom data from already-consumed PII source fields.
### Concrete example: Facebook Pixel
The Facebook Pixel integration shows the failure mode clearly.
Current component behavior:
- It recognizes Meta user-data keys such as `em`, `ph`, `fn`, `ln`, `ct`, `st`, `zp`, `country`, and `external_id`: https://github.com/managed-components/facebook-pixel/blob/v1.0.13/src/track.ts#L4-L18
- It hashes recognized keys when required, writes them to `body.user_data`, and deletes those exact destination keys from the payload: https://github.com/managed-components/facebook-pixel/blob/v1.0.13/src/track.ts#L124-L138
- It forwards the remaining payload as `custom_data`: https://github.com/managed-components/facebook-pixel/blob/v1.0.13/src/track.ts#L144-L144
That behavior is correct when the incoming payload uses the exact Meta destination keys. The gap appears when a manager maps a custom source field into one of those destination keys but preserves the original source field in the payload. The component can hash/delete the destination key, but it has no way to know that the original source field was consumed by that mapping.
Failure mode:
- mapped customer-information value is sent in the destination-specific field, for example Meta `user_data`
- original plaintext source field remains in the component payload
- original source field is eligible for generic event/custom data forwarding
- the tracking service can receive both the hashed destination value and the original plaintext source value
The issue is not a specific field name. Source field names are customer-defined. A hard-coded denylist of common aliases cannot solve this reliably.
### Proposed solution
Add a WebCM / Component Manager contract for component-specific payload projection and consumed source fields.
Preferred shape:
1. **Preserve the original payload.** Keep the request payload immutable.
2. **Create a component-specific payload.** Build a fresh payload for each component invocation.
3. **Apply mappings in the manager.** Resolve source paths into that component/action's destination fields.
4. **Record consumed source paths.** Mark mapped source paths as consumed when they populate destination-specific fields.
5. **Exclude consumed fields from passthrough data.** Remove consumed source paths before dispatch, or exclude them from any generic custom-data passthrough.
6. **Dispatch only the projected payload.** Invoke the component with the component-specific payload.
This keeps privacy-sensitive cleanup in the manager layer, where field mappings are known.
### Alternative acceptable design
If manager-side removal is not the right abstraction, expose mapping metadata to the component, for example:
```ts
event.mappings = [
{
source: "event.customer_email",
destination: "em",
consumeSource: true
}
]
event.consumedFields = ["event.customer_email"]
```
The component could then remove consumed source fields before building an outbound tracking-service request.
This is less robust than manager-side projection because every component must implement cleanup correctly, but it is still better than leaving components to guess source aliases.
### Alternatives considered
Hard-coded PII/source-field denylists in individual components are incomplete because source field names are customer-defined.
Expecting each component manager to invent its own cleanup convention works only locally and does not establish an interoperable Managed Components contract.
Leaving cleanup to the destination component is not possible with the current public event contract unless the component also receives mapping metadata.
### Impact
This affects any component manager or integration that:
- lets users map source event properties into component-specific destination fields,
- passes the original event payload through to the component, and
- relies on the component to forward remaining payload keys as generic event/custom data.
For analytics and advertising components, the failure mode can expose plaintext customer-information source fields alongside normalized or hashed destination fields.
### Prior-art search
Related but not duplicate:
- https://github.com/cloudflare/webcm/issues/58 discussed normalizing event payload shapes, but did not address mapped-field consumption or source-field cleanup.
- https://github.com/cloudflare/webcm/issues/68 discussed custom event payloads, but not component-specific mappings or consumed source fields.
### Reference
* https://community.cloudflare.com/t/zaraz-facebook-pixel-sends-mapped-pii-source-values-alongside-hashed-meta-user-data/939515
* https://github.com/managed-components/facebook-pixel/issues/17
Contributor guide
Research direction
Start by reading src/manager.ts at the MCEvent creation and Manager API sections, then inspect src/server.ts dispatch and src/compConfig.ts for the public component configuration shape. Compare these contracts with the referenced Facebook Pixel track.ts behavior; the work is done when mapped source fields cannot be forwarded as generic data or their consumption is explicitly available to the component.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- api, backend-api-design, security
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100