Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 37 additions & 17 deletions src/analytics/posthogModel.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,29 @@ export function resolveAnalyticsHost(configured, origin) {
// SDK-assembled properties derived from URLs, query strings, referrers and
// click/campaign ids — none of these may leave the browser. Applied by
// property_denylist at capture assembly AND re-checked in before_send.
// Query-string/campaign parameter names the SDK lifts into campaign_params
// and $session_entry_/$initial_ variants. The SDK builds those derived names
// dynamically (`$session_entry_${param}`), so they are enumerated here.
const CAMPAIGN_PARAM_NAMES = [
'utm_source',
'utm_medium',
'utm_campaign',
'utm_term',
'utm_content',
'campaign_params',
'gclid',
'fbclid',
'msclkid',
'ttclid',
'twclid',
'igshid',
'li_fat_id',
'mc_cid',
'dclid',
'wbraid',
'gbraid',
];

export const DENIED_EVENT_PROPERTIES = [
'$current_url',
'$pathname',
Expand All @@ -148,25 +171,21 @@ export const DENIED_EVENT_PROPERTIES = [
'$initial_referring_domain',
'$initial_campaign_params',
'$initial_referrer_info',
'utm_source',
'utm_medium',
'utm_campaign',
'utm_term',
'utm_content',
'campaign_params',
'gclid',
'fbclid',
'msclkid',
'ttclid',
'twclid',
'igshid',
'li_fat_id',
'mc_cid',
'dclid',
'wbraid',
'gbraid',
'$initial_person_info',
...CAMPAIGN_PARAM_NAMES,
// Derived session-entry/initial variants — e.g. $session_entry_utm_source.
...CAMPAIGN_PARAM_NAMES.flatMap((p) => [
`$session_entry_${p}`,
`$initial_${p}`,
]),
];

// Prefixes the SDK uses for session-entry and persisted "initial" URL,
// referrer and campaign properties. Denying at prefix level in
// before_send covers custom_campaign_params and any future variant the
// static denylist cannot enumerate.
const DENIED_PROPERTY_PREFIXES = ['$session_entry_', '$initial_'];

// posthog-js places these two transport-critical fields inside the event's
// properties object before before_send runs. They are not caller-controlled:
// token is the public project token already present in the browser bundle,
Expand All @@ -184,6 +203,7 @@ export function scrubEventProperties(eventName, properties, pageviewProps) {
const requiredSdk = new Set(REQUIRED_SDK_EVENT_PROPERTIES);
const clean = {};
for (const [key, value] of Object.entries(properties || {})) {
if (DENIED_PROPERTY_PREFIXES.some((p) => key.startsWith(p))) continue;
if (DENIED_EVENT_PROPERTIES.includes(key)) continue;
if (!key.startsWith('$') && !allowedCustom.has(key) && !requiredSdk.has(key)) continue;
clean[key] = value;
Expand Down
14 changes: 14 additions & 0 deletions test/posthog-analytics.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -237,13 +237,27 @@ test('before_send strips private extras but preserves SDK ingestion fields', ()
$referrer: 'https://ref.example/?q=2',
utm_source: 'newsletter',
gclid: 'abc',
// SDK-derived session-entry/initial campaign variants — generated as
// $session_entry_${param}, including for query params we never listed.
$session_entry_utm_source: 'DO_NOT_SEND',
$session_entry_utm_campaign: 'SECRET',
$session_entry_gclid: 'click-id',
$initial_utm_medium: 'cpc',
$initial_fbclid: 'fb-click',
$session_entry_custom_param: 'unlisted variant',
$browser: 'Chrome',
stray_key: 'must be dropped',
};
const clean = scrubEventProperties('sync_started', dirty);
for (const key of DENIED_EVENT_PROPERTIES) {
assert.ok(!(key in clean), `${key} survived scrubbing`);
}
assert.equal(clean.$session_entry_utm_source, undefined, 'session-entry UTM leaked');
assert.equal(clean.$session_entry_utm_campaign, undefined, 'session-entry campaign leaked');
assert.equal(clean.$session_entry_gclid, undefined, 'session-entry click id leaked');
assert.equal(clean.$initial_utm_medium, undefined, 'initial UTM leaked');
assert.equal(clean.$initial_fbclid, undefined, 'initial click id leaked');
assert.equal(clean.$session_entry_custom_param, undefined, 'unlisted session-entry variant leaked');
assert.equal(clean.stray_key, undefined, 'non-schema custom key survived');
assert.equal(clean.trigger, 'manual');
assert.equal(clean.token, 'phc_public_project_token', 'SDK project token was stripped');
Expand Down
Loading