Skip to content
Detection teardowns

What a checkbox click records: one click, sixty properties and a count of everything before it

A challenge checkbox does not just receive a click. It writes down the event, the element, the window, the input that led up to it and the stack that handled it.

Pim
Pim· Clearcote Research
9 min read

Key takeaways

  • One click on a deployed challenge checkbox produces a record of at least 60 string values: properties of the pointer event, the checkbox's bounding box, the window, the active element, the element's sizes and computed style, plus derived values such as the element under the pointer.
  • Before the click, the widget counts seven input event types (mousemove, pointermove, pointerover, touchstart, click, keydown, wheel) and a shared total, so a click with no plausible lead-up is visible.
  • Playwright and Puppeteer both move the mouse to the exact centre of the element in a single step before clicking, while element.click() and dispatchEvent() produce events with isTrusted false.

The click record

Public reverse-engineering work on a widely deployed challenge widget shows what its checkbox writes when it is clicked. The handler converts every value to a string with String(...) and stores at least 60 of them. They come from seven places:

  • The event: isTrusted, type, clientX/Y, screenX/Y, pageX/Y, offsetX/Y, layerX/Y, movementX/Y, button and buttons, the modifier keys, detail, timeStamp, and the pointer fields pointerId, pointerType, isPrimary, width, height, pressure, tiltX/Y, twist, altitudeAngle and azimuthAngle.
  • The checkbox's bounding box: x, y, left, top, right, bottom, width and height.
  • The window: scroll position, inner size and devicePixelRatio.
  • The document: activeElement.
  • The element: isConnected, client and offset sizes.
  • Computed style: display, visibility, pointer-events and opacity.
  • Derived values: a touch flag, the element returned by elementFromPoint, and the click position relative to the box, rounded down.

Many of these values have to agree with each other. The event carries several coordinate systems: client, page, screen, layer, offset and the floored relative position. Offset coordinates are rounded relative to the box while the relative ones are floored, so they can differ by one, and screen coordinates must equal client coordinates plus the iframe's position on the screen. A click whose screen coordinates are relative to the iframe instead of the screen, a known quirk of protocol-generated clicks in cross-origin frames, contradicts the window geometry.

Engine details in a click

Even a click carries the engine's signature. In captures, a WebKit tap reports a primary pointer with a random negative 32-bit pointerId, while Chromium reports pointerId 1. The checkbox's own width depends on the device's fonts, because the input covers its label: 168.125 CSS pixels in iOS Safari 26.

The same handler reads the iframe's style sheets: at most 16 sheets and 256 rules, keeping the text of every @keyframes rule. The text is the browser's own serialisation, and the engines serialise differently: WebKit writes 0%,100% without a space and adds a unit to a zero stroke-width (0px), while Chromium writes 0%, 100% and keeps 0. A browser that claims Safari and serialises CSS like Chromium is exposed by the keyframes text alone. The scan also lists body divs that have an id or inline style, which an untouched widget frame does not contain.

The lead-up: an input tally

The widget keeps a running count of seven input event types, each with its own counter, and a total that every counted event also increments: mousemove, pointermove, pointerover, touchstart, click, keydown and wheel. The counts go into the payload with the click.

Real input has a characteristic shape. A mouse produces a pointerover when it enters the frame and then a pair of pointermove and mousemove for every movement on the way to the box. A phone tap produces pointerover and touchstart, and then, when the finger lifts, a single emulated mousemove before the click, with no pointermove at all.

Automation produces its own shapes. Playwright's and Puppeteer's click() both scroll the element into view, compute its centre and call mouse.click(x, y), which moves the mouse in one step (steps defaults to 1) and then presses and releases it. The tally sees one move and a click at the geometric centre. element.click() and dispatchEvent() fire only the click, with isTrusted false and no lead-up at all.

The handler's stack and a console marker

Right after the click record, the handler writes three more values. The first is new Error().stack, created inside the click callback, which has to match the engine's stack format and the real call path; see stack traces show what called the widget. The second is a marker string that a stack getter flips from one value to another if anything reads it, which is what happens when a console or an inspector serialises the object, the technique described in timing native functions. The third is a flag for a programmatic click.

Three Date.now() marks frame the interaction: when the checkbox was set up, when it was ready, and when the proof of work started. The proof itself, a SHA-256 search over a fixed time budget with one worker per three quarters of the claimed cores, records its nonce, the number of hashes, the workers that finished and the elapsed time; see proof of work as a core-count check.

What follows for automation

A click is only convincing as part of a sequence. The pointer has to arrive from somewhere, along a path with the event density of real input, at a point that is not exactly the centre, with trusted events whose coordinates agree across every system the record stores, and on an engine whose stack, pointer ids and CSS serialisation match the claimed browser. Engine-level input, rather than synthetic events, is what produces trusted events with consistent coordinates; the path and timing are a separate problem covered in behavioural bot detection.

The through-line

The checkbox is the one moment in a challenge where the visitor does something, and the record treats it like a fingerprint of its own: dozens of values that must agree with each other, with the lead-up and with the engine. See inside a challenge widget for everything collected before the click.

#bot-detection#automation#reference

Clearcote puts this into practice

An open-source Chromium with fingerprint control compiled into the engine. A drop-in for Playwright & Puppeteer.

  • 228engine patches
  • 34,679lines added
  • 336Chromium files changed
  • 354checks per build

Engine numbers are for the Pro build; the free open build is a smaller, public patch set.

Free for one browser with GitHub. No card.