Consent strings

A consent banner produces a record of what was agreed. Under the Transparency and Consent Framework, a cross-industry voluntary standard for European digital advertising, that record is a short encoded string passed between the site, the vendors on it, and the auction that follows. It is meant to be evidence that a choice was made. It is also a field that can travel with requests to the vendors taking part.

The banner writes it down

The string packs a version, timestamps, the identity of the consent platform, and two bit fields: one bit per purpose, one bit per vendor. It is base64 encoded, so anyone holding it can decode those fields. The encoding is not a confidentiality measure and was never meant to be one.

# Written when the banner is answered
string = encode(
    version,
    created, last_updated,      # to the decisecond
    cmp_id, cmp_version,        # which consent platform
    consent_language,
    vendor_list_version,
    purposes_consented,         # one bit per purpose
    vendors_consented,          # one bit per vendor, hundreds of them
)

# Passed onward with the bid request
send(string, page_url, ip_address, device_identifiers) to each vendor

# Any holder can decode it
decoded = base64_decode(string)
→ which purposes, which vendors, when the choice was made, by which platform

Two properties follow from the shape rather than from any misuse. The timestamps are stored in deciseconds, and the vendor bits are a pattern: the set of vendors a person allowed, together with the moment they allowed it, is not the same for everybody. In the traffic modelled here the string arrives beside an IP address and a device identifier, so whoever receives it has several fields to attach the choice to other activity by.

Two switches

greenhouse-attacks consent.py builds 50,000 answers to one banner over a day, from the published field layout and against the 1,214 vendors on the Global Vendor List of 17 September 2026, then counts how many of them stand alone.

The model is smaller than a whole TC String. The format is on v2.4, dated May 2026, and the vendor list used here carries the standardTexts field that version introduced. Neither v2.4 nor v2.3 before it altered the core fields, so the model keeps those: the two timestamps and the purpose and vendor bits, at the policy version the fetched list declares. It omits the disclosedVendors segment that TCF v2.3 made mandatory from 1 March 2026, which records which vendors a platform showed somebody rather than what they chose, and so is close to constant across one banner’s visitors. Every percentage below describes this generated population and measures nothing about how real visitors answer.

Alone in the population on

Share

purposes agreed

0.85%

vendors allowed

9.99%

the moment of answering

94.29%

both timestamps

97.93%

vendors and both timestamps

98.96%

A tenth of the population alone on the vendor bits sounds mild. Breaking that tenth out by what was done at the banner ends the impression.

At the banner

Alone on the vendor bits

accepted all

0.00%

refused all

0.00%

customised

100.00%

Accepting everything produces one pattern and refusing everything produces another, and here 35,116 people share the first and 9,891 the second. All 4,993 who opened the detail panel and chose came out with a pattern nobody else had.

Vendors switched off, the rest accepted

Alone on the vendor bits

0

0.00%

1

0.00%

2

93.41%

3

99.99%

Switching off one vendor leaves a person among about forty others who switched off the same one. Switching off two leaves almost everybody alone: 1,214 vendors make 736,291 possible pairs, and only 50,000 people answered the banner.

The timestamps do the rest, and they do it whatever was clicked. A decisecond is a narrow slot, so the figure turns on how many people answered the same banner that day:

Answers that day

Alone on the last answer

Alone on both timestamps

10,000

98.96%

99.66%

50,000

94.29%

97.93%

200,000

79.51%

92.10%

500,000

56.00%

82.31%

A returning visitor has two moments rather than one, and the pair holds up where a single moment falls away. Arrivals are spread evenly across the day here, whereas real traffic is uneven. People arriving inside a peak collide more often, so real traffic would produce fewer unique timestamps than this table does.

The receipt is personal data

The Court of Justice ruled on this in IAB Europe in March 2024. A consent string “constitutes personal data within the meaning of that provision in so far as, where those data may, by reasonable means, be associated with an identifier, such as, inter alia, the IP address of that user’s device, they allow the data subject to be identified”. The Court held IAB Europe to be a joint controller in relation to the processing of the string, and its lack of direct access to the data did not prevent that.

The Court drew a second line in the same judgment. Joint controllership of the body that defines the format “does not extend automatically to the subsequent processing of personal data carried out by third parties” for targeted advertising. Responsibility for the record and responsibility for what is done downstream are separate questions.

So the artefact recording a privacy choice can itself be personal data, and the record of the choice falls under the regime the choice was about.

Never being asked

Refusing at the banner produces a string as well, with the bits cleared. Clearing the consent signals does not make the signalling mechanism disappear. What refusal does do is put the refuser in the second-largest crowd on the record. The careful answer does worse.

Choosing vendor by vendor is the option that singles a person out in this population. Two switches are enough, and the detail panel exists to collect more than two. Nothing about the interface says that the considered answer is also the distinguishing one. The encoding can express a partial answer that others share, as the single-switch row shows; what it cannot do is stop the panel offering enough choices to land somebody alone.

Neither answer keeps the clock out of the record. The timestamps are written whatever was chosen, and at the stored resolution two answers have to fall in the same decisecond to collide on that field at all.

Blocking the scripts that construct and transmit the string stops that platform generating or sending one. Answering a question and not being asked one leave different traces, and a browser that never loads the consent platform produces no record for it.

Beyond that, the defences are the ones that apply to any identifier travelling with a request. Where the string arrives beside an address and a device identifier, those neighbours supply the joins to everything else, and breaking them is a different job from changing the string.

Last reviewed: 2026-09-19.