Skip to content
PubTrust — a ClearTrust product · cleartrust.ccAd quality and malvertising protection for publishers

Product

How PubTrust works

One tag, four detection layers, and a blocking mechanism designed around a single rule: the tag may fail, the page may not. This page describes what each layer can see, what it cannot, and why the difference matters.

What does the PubTrust tag do when the page loads?

It loads async — nothing synchronous touches your critical path — and starts working immediately, before it has finished downloading its full rule set. The tag is 11.4 KB gzipped and carries a critical inline ruleset: the highest-frequency, highest-confidence terms and domains for the packs and languages you have enabled. Blocking is live on line one. The full compiled corpus arrives asynchronously as a second file, and ads that rendered in the gap are re-scanned retroactively when it lands — ads persist in the DOM, so a late catch still blocks and still reports.

Why the second file is effectively free

The full corpus is immutable and content-addressed: the same bundle hash is served to every publisher on that policy version, with a one-year cache lifetime. One Cloudflare edge cache entry serves thousands of sites, so it typically resolves from edge or disk cache rather than from an origin. The tag and the bundle are both served from Cloudflare's global edge; your readers never make a request to our origin.

The kill switch

A small configuration file is polled every five minutes. It carries the current bundle hash and a kill switch, so a misbehaving tag can be disabled globally or for a single site within about five minutes — without you deploying anything. The tag does not wait for that poll to protect your page: its own watchdog stands it down the moment it throws too often or exceeds its CPU budget. You also have window.__ptDisable = true as your own escape hatch, which we will never take away.

Layer 1 — Pre-render: reading the bid before the browser paints

The highest-signal and lowest-risk layer. Before a creative is drawn, PubTrust reads what the ad stack already knows about it. From Prebid.js we take the bidResponse and bidWon events: the advertiser domains the bidder declared, the bidder name, the CPM, the creative ID, the deal ID, the media type and the size — and, most valuable of all, bid.adm, the winning creative's markup, readable as a plain string in page JavaScript. From Google Publisher Tag we take slotRequested, slotResponseReceived and slotRenderEnded, plus getResponseInformation() for the advertiser, campaign and creative identifiers. A generic window.__ptSlots bridge covers custom stacks. If any of it matches a policy we refuse the render and paint your fallback. No flash, no layout shift, no damage.

Layer 2 — DOM and creative inspection: everything the bid did not cover

Direct-sold campaigns, non-GPT stacks and creatives whose declared advertiser domain simply lies. A MutationObserver watches your configured slot selectors plus auto-discovered ones — [id^="div-gpt-ad"], ins.adsbygoogle, iframe[id^="google_ads_iframe"], [data-pt-slot]. Inside same-origin friendly iframes we read the creative document directly: visible text, link targets, image sources, meta tags and inline script hosts. Because a MutationObserver does not cross a document boundary and creative DOM arrives asynchronously, we run a second observer inside the creative document — a one-shot read at slotRenderEnded systematically under-reads, which is a real and common implementation bug elsewhere.

The cloaking signal

We read both innerText and textContent and treat the difference as evidence in itself. innerText is what a reader sees; textContent includes anything hidden with display:none. A creative whose hidden text diverges sharply from its visible text is cloaking, and that delta is a detection feature, not noise to be normalised away.

Click-URL unwrapping

Ad-server click macros prefix the real destination onto the tracker URL, so the landing page frequently sits unencoded at the tail of the href; the escaped variants nest it as a percent-encoded parameter. Both are recoverable in the browser with no network call, which means we can judge a landing domain before anyone clicks. Following the redirect chain is impossible from a browser — that happens server-side, see §2.6.

Cross-origin, stated plainly

When the creative is cross-origin — a SafeFrame, or any <iframe src> to another origin — its document is unreadable by design and no amount of engineering changes that. What is readable, and what we use: the iframe src, the name attribute (GPT and SafeFrame encode creative metadata there), the sandbox flags, the geometry, and — where header bidding supplied it — the bid markup, including every host it references. We do not attribute a cross-origin creative’s own network requests to its slot. A gambling creative almost always names a gambling host.

Layer 3 — Runtime Integrity: the malvertising layer, always on

This layer uses no keywords and is enabled on every site by default, because the attacks it stops do not care what the ad claims to advertise. Forced navigation is the signature malvertising attack, and it is also the place where a widespread industry claim is simply untrue: window.location, window.top and location.href are non-configurable, and redefining any of them throws — including from a parent page reaching into its own friendly iframe. Every "we intercept the redirect in JavaScript" design is impossible as stated. What JavaScript can reach are the laundering paths, and those we hook: window.open, programmatic a.click() on links that target the top frame, history.pushState and history.replaceState, with beforeunload as a detector.

The measure that actually works

Iframe sandboxing. Injecting sandbox="allow-scripts allow-same-origin allow-popups allow-forms" — without allow-top-navigation — onto ad iframes makes it structurally impossible for the creative to navigate your page. Not "difficult". Impossible. It is opt-in per site, and applied only when the site is in Block, because a minority of legitimate rich media breaks under it, and we would rather you switched it on deliberately than discovered it in a support ticket.

Also in this layer

Pop-under and pop-up abuse — window.open is proxied, and on a site in Block a call is refused unless it was started by a genuine user gesture — one gesture, one window. document.write hijack during the render window. Autoplay with sound — we patch HTMLMediaElement.prototype.play and flag unmuted autoplay. Heavy creatives — a PerformanceObserver reports long tasks while ads are on the page, so you can see when advertising is eating your Interaction to Next Paint. The browser does not say which cross-origin iframe caused a long task, so it is reported as a page-level signal rather than pinned on an advertiser we cannot prove caused it.

Layer 4 — Server-side deep scan: the part that cannot happen in a browser

On a violation, or on a first sighting of an unfamiliar advertiser, our platform fetches the landing URL server-side, follows the redirect chain to its destination, checks domain reputation and classifies the page — in a headless browser we control, where the cross-origin boundary does not apply and a real screenshot is possible. Newly confirmed bad domains are proposed back into the Protection Packs, where a human assigns a weight and writes a note before anything is published. This is also the only place any language model is used in PubTrust: server-side, out of band, never in the tag.

Does blocking an ad shift my layout or hurt Core Web Vitals?

No, and the mechanism is deliberately boring. At tag init we inject one stylesheet rule: [data-pt-blocked]{visibility:hidden!important}. When a violation is found, the observer callback sets that attribute in the same task, before the browser paints, so the creative never becomes visible. We use visibility:hidden and never display:none, which means the slot keeps its box and Cumulative Layout Shift from a PubTrust block is zero — surrounding content does not jump. Your Fallback Creative is then overlaid at the exact slot dimensions, and the offending iframe is torn down so it stops executing. Fallbacks are configurable per site, per size and per policy, and can be an image, safe HTML or a plain colour block.

What happens if the PubTrust tag itself breaks?

It goes dormant and your page carries on. Every hook and callback is wrapped in try/catch — the tag can fail but the page cannot. A self-disabling watchdog unhooks everything immediately and reports why if the tag throws more than a threshold number of times or exceeds its CPU budget. There is a remote kill switch, global or per-site, that reaches every running tag within about five minutes. There is window.__ptDisable for you. And every monkey-patch we apply is reversible and preserves both the original behaviour and Function.prototype.toString fidelity, so we do not trip other vendors' integrity checks and start a fight on your page between two scripts that are both trying to help you.

We are a third-party script on somebody else's page. Every one of those guarantees is load-bearing, and we would rather lose a block than lose your page.

Can I see the source of the tag running on my site?

Yes — the full, readable, commented source of your site's exact tag build is viewable and downloadable from your dashboard, with the build's git SHA and bundle hash recorded beside it. The delivered tag is minified and obfuscated, because a published rule set is a published evasion guide, but obfuscation should never mean you cannot audit what you are serving to your readers. Your security team can read every line of it and diff it against what is being served.

The whole path, end to end

A reader loads your page → the tag initialises from Cloudflare's edge and starts judging with its inline rules → the full corpus arrives and everything is re-scanned → a violation is caught at whichever layer sees it first → the creative is hidden before paint and your fallback is painted → a violation beacon leaves within about two seconds carrying the full forensic record → it appears in your live feed → and one summary beacon at page exit carries exact counts, so your usage figures are counted, not estimated.