Summary
assessAndProtect() activates protection strategies (and their overlays) that no matched policy requested, because it only flips matched strategies on and leaves the rest at ContentProtector's defaults — several of which default to true.
This contradicts the documented contract ("Legitimate users see no protection overhead; automation and scrapers trigger it automatically" — i.e. activate only what the policies warrant).
Root cause
ContentProtector's constructor defaults several strategies ON (src/core/ContentProtector.ts):
preventSelection: true,
preventScreenshots: true,
// ...
preventDevTools: false,
assessAndProtect() builds protectionOptions by setting only the matched strategy keys to true (src/policy.ts), then new ContentProtector(protectionOptions). Unset keys fall through to the defaults above — so e.g. preventScreenshots activates even when every matched policy only asked for enableWatermark.
Reproduction
await assessAndProtect(el, {
policies: [{ when: { riskScore: { gte: 0 } }, enable: ['enableWatermark'] }],
});
// Expected: only the watermark is active.
// Actual: screenshot protection (and its red overlay) is also active,
// plus any other default-on strategy.
Proposed fix
In assessAndProtect(), initialize all StrategyKey booleans to false in protectionOptions before enabling the matched ones, so the protector reflects exactly the matched set:
const ALL_STRATEGY_KEYS: StrategyKey[] = [
'preventSelection', 'preventContextMenu', 'preventKeyboardShortcuts',
'preventPrinting', 'preventScreenshots', 'preventClipboard',
'enableWatermark', 'preventDevTools', 'preventExtensions',
];
const protectionOptions: ContentProtectionOptions = { targetElement };
for (const k of ALL_STRATEGY_KEYS) protectionOptions[k] = enabledStrategies.has(k);
A test asserting that a single-strategy policy leaves all other strategies inactive would lock this in.
Impact / workaround
Consumers get protections (and overlays) they didn't opt into. Found while wiring Shield into the tindalabs.dev demo; worked around there by calling protector.updateOptions({...}) post-creation to pin the matched set — but the fix belongs in assessAndProtect.
Summary
assessAndProtect()activates protection strategies (and their overlays) that no matched policy requested, because it only flips matched strategies on and leaves the rest atContentProtector's defaults — several of which default totrue.This contradicts the documented contract ("Legitimate users see no protection overhead; automation and scrapers trigger it automatically" — i.e. activate only what the policies warrant).
Root cause
ContentProtector's constructor defaults several strategies ON (src/core/ContentProtector.ts):assessAndProtect()buildsprotectionOptionsby setting only the matched strategy keys totrue(src/policy.ts), thennew ContentProtector(protectionOptions). Unset keys fall through to the defaults above — so e.g.preventScreenshotsactivates even when every matched policy only asked forenableWatermark.Reproduction
Proposed fix
In
assessAndProtect(), initialize allStrategyKeybooleans tofalseinprotectionOptionsbefore enabling the matched ones, so the protector reflects exactly the matched set:A test asserting that a single-strategy policy leaves all other strategies inactive would lock this in.
Impact / workaround
Consumers get protections (and overlays) they didn't opt into. Found while wiring Shield into the tindalabs.dev demo; worked around there by calling
protector.updateOptions({...})post-creation to pin the matched set — but the fix belongs inassessAndProtect.