Технически
How We Built a System That Scans 100,000 Websites for Cookie Consent Violations
GDPR Privacy Monitor Engineering · 2026-04-13 · 7 min четене
Автоматизираната проверка на спазването на съгласието звучи просто, докато не се опитате да я изградите. Наивният подход -- заредете страница, проверете за бисквитки, потърсете банер -- пропуска по-голямата част от това, което има значение. Нарушенията на съгласието са поведенчески, а не структурни. Те се проявяват във времето на изпълнение на скриптовете, последователността на мрежовите заявки, реакцията на елементите на интерфейса при потребителско взаимодействие и устойчивостта на състоянието при повторно зареждане на страницата. Не можете да оцените нищо от това без да стартирате истински браузър, да взаимодействате със страницата, както би го направил човек, и да записвате какво всъщност се случва на мрежово ниво.
Тази публикация описва как изградихме скенерния двигател зад GDPR Monitor, инженерните предизвикателства, които погълнаха по-голямата част от времето ни, архитектурните решения, които взехме и защо, и ограниченията, за които сме честни. Ако работите в областта на уеб спазването, автоматизацията на браузъри или мащабните уеб измервания, тук трябва да намерите нещо полезно.
Преглед на конвейера
Всяко сканиране преминава през шест етапа. Разбирането на конвейера е необходим контекст за конкретните предизвикателства, които следват.
Етап 1: Стартиране на браузър и изолация. Стартира се нов екземпляр на Chromium с нулево състояние -- без бисквитки, без localStorage, без кеш, без service workers. Това е гаранцията за чиста стая, която прави измерването преди съгласие смислено. Конфигурираме стандартен viewport, реалистичен user-agent и Accept-Language хедъри, съответстващи на целевата държава, и стандартни флагове на браузъра. Всяко сканиране получава собствен браузърен процес; няма изтичане на състояние между сканиранията. Етап 2: Навигация и снимка преди съгласие. Скенерът навигира до целевия URL, изчаква страницата да достигне стабилно състояние (мрежата е в покой, DOM е установен) и улавя всичко, което се е случило: поставени бисквитки, направени мрежови заявки (с пълен URL, тайминг и метаданни на отговора), контактирани домейни на трети страни и пълноекранна снимка. Тази снимка отговаря на фундаменталния въпрос: какво направи този уебсайт, преди потребителят да е имал каквато и да е възможност да даде съгласие? Етап 3: Откриване на CMP и идентификация на банер. Скенерът се опитва да идентифицира платформата за управление на съгласието и да намери банера за съгласие, бутона за приемане и бутона за отхвърляне. Това използва многослойна система за откриване, описана подробно по-долу. Етап 4: Взаимодействие за съгласие. Скенерът взаимодейства с банера -- кликвайки приемане за стандартния поток, кликвайки отхвърляне за теста на потока за отхвърляне. Изчаква страницата да се установи след взаимодействието, отчитайки анимации, повторна оценка на скриптове и забавено задействане на тагове. Етап 5: Снимка след съгласие и диференциален анализ. Втора пълна снимка улавя състоянието след взаимодействието за съгласие. Сравняването на снимките преди и след съгласие разкрива какво се е променило: нови бисквитки, нови заявки за проследяване, състояние на съгласие в API на CMP. Етап 6: Анализ, класификация и генериране на доклад. Суровите данни захранват модули за анализ: класификация на бисквитки спрямо нашата база данни, съпоставяне на тракери с известни модели, оценка на живота на бисквитките, одит на достъпността на банера, валидация на Google Consent Mode, откриване на сигнали за дактилоскопиране и оценка на риска. Резултатът е структуриран доклад с констатации, артефакти с доказателства и комбиниран рисков резултат.Всеки етап произвежда доказателства с времеви печат, които се съхраняват трайно. Всяка констатация може да бъде проследена до конкретни мрежови заявки, записи за бисквитки или екранни снимки.
Предизвикателство 1: Откриване на CMP -- 45 платформи, безкрайни вариации
Управлението на съгласието не е стандартизирано. Няма универсален HTML атрибут, няма задължителен JavaScript API, няма последователна DOM структура, която да казва „това е банер за съгласие". Има 45 отделни CMP в нашата библиотека за откриване, всеки със своя собствена DOM структура, подписи на скриптове, JavaScript глобални променливи и модели на взаимодействие. Освен тях, 34,7% от банерите, които открихме в нашето проучване на 97 304 сайта, бяха общи или неидентифицирани -- персонализирани реализации, регионални доставчици или минимални решения, които не съответстват на нито един известен подпис на CMP.
Нашето откриване използва подход, базиран на ниво на увереност и слоеве:
Слой 1: Откриване по подпис на скрипт
Скенерът проверява за наличието на известни CMP скриптове по URL модел и JavaScript глобални променливи. Cookiebot, например, се зарежда от `consent.cookiebot.com` и разкрива `window.Cookiebot`. OneTrust се зарежда от `cdn.cookielaw.org` и разкрива `window.OneTrust`. Всеки CMP има характерни модели на зареждане, които могат да бъдат открити преди изследване на DOM.
Този слой е бърз и с висока увереност, когато съответства. Но има критично ограничение: казва ви кой CMP присъства на страницата, но не непременно кой CMP е отговорен за банера за съгласие. Сайт може да зарежда PiwikPRO за анализи (което включва CMP компонент), докато използва tarteaucitron за действителното управление на съгласието. Откриването на двата скрипта е лесно; знаенето кой от тях контролира банера е по-трудно.
Слой 2: Съпоставяне с верифицирани селектори
За всеки известен CMP поддържаме курирано множество от CSS селектори, които надеждно идентифицират контейнера на банера, бутона за приемане и бутона за отхвърляне. Това са селектори, които сме валидирали в множество версии и конфигурации на всеки CMP. Когато CMP е открит в Слой 1 и верифицираните му селектори съответстват на елементи в DOM, имаме висока увереност както в идентификацията на CMP, така и в целите за взаимодействие с банера.
Слой 3: Съпоставяне със съвместими селектори
По-широко множество от селектори, които работят в много версии на CMP, но са по-малко прецизни. Те обработват случаи, в които CMP е бил персонализиран, тематизиран или работи с версия, непокрита от верифицираните ни селектори. Те жертват прецизност за покритие.
Слой 4: Общи евристики
За 34,7% от банерите, които не са асоциирани с известен CMP, се връщаме към евристично откриване. Скенерът търси:
- Елементи с фиксирана или залепваща позиция в долната или горната част на viewport-а
- Елементи, съдържащи ключови думи, свързани със съгласието, на множество езици („cookies", „consent", „privacy", „akzeptieren", „accepter", „aceptar" и т.н.)
- Бутони с често срещани етикети за действие за съгласие („Accept All", „Reject All", „Manage Preferences" и еквиваленти)
- Структурни модели, типични за диалози за съгласие: фонове с наслагване, модални контейнери, бутони за затваряне
Този слой улавя много персонализирани реализации, но е по своята същност по-малко надежден. Промоционален банер с фиксирана позиция или запис за бюлетин може да изглежда структурно подобен на диалог за съгласие.
Слой 5: Проучване на CMP API
Някои CMP разкриват JavaScript API -- най-забележимо API на IAB Transparency and Consent Framework (TCF) чрез `__tcfapi`. Ние проучваме за тези API, за да верифицираме както откриването на CMP, така и да прочетем програмното състояние на съгласие, което по-късно сравняваме с наблюдаваното поведение на браузъра.
Моделът за увереност
Вместо да третираме откриването като двоично (намерено/ненамерено), присвояваме резултати за увереност въз основа на това кои слоеве са съответствали и колко силно. Сайт, на който откриваме CMP скрипт, съпоставяме верифицирани селектори и намираме TCF API, получава висока увереност. Сайт, на който само общите евристики са се задействали, получава по-ниска увереност. Този резултат за увереност захранва нашата класификация на риска -- по-ниска увереност в откриването означава, че констатациите е по-вероятно да бъдат класифицирани като неопределени, а не като окончателни.
Моделът за увереност е причината погрешната идентификация на CMP, макар и да се случва, да не въвежда системна грешка в резултатите ни. Когато откриването е двусмислено, ние го казваме, вместо да налагаме класификация.
Предизвикателство 2: Потокът за отхвърляне -- Защо „кликни и провери" е изненадващо трудно
Тестването на бутон за отхвърляне звучи просто: намерете го, кликнете го, проверете дали бисквитките са изчезнали. На практика всяка стъпка е изпълнена с проблеми с тайминга, асинхронно поведение и особености, специфични за платформата.
Намиране на бутона за отхвърляне. Не всички бутони за отхвърляне са надписани „Reject". Те могат да казват „Decline All", „Refuse", „Only necessary cookies", „Manage settings" (водещо към втори слой, където отхвърлянето е възможно) или еквиваленти на десетки езици. Някои CMP поставят опцията за отхвърляне на различно визуално място, с различен размер или различен цвят от опцията за приемане. Някои я скриват зад линк „Повече опции" или „Персонализиране". Нашият скенер поддържа многоезично множество от модели за действие за отхвърляне и също открива опции за отхвърляне на втори слой, когато първият слой предлага само приемане и персонализиране. Изчакване на правилния момент. След кликване на отхвърляне страницата може да претърпи значителни промени: банерът се затваря (често с анимация), CMP задейства обратни извиквания за състояние на съгласие, мениджърите на тагове преоценяват правилата си и скриптове могат да бъдат заредени или премахнати. Проверката на бисквитките твърде рано улавя състоянието в средата на прехода. Проверката твърде късно пропуска краткотрайно проследяване, което се задейства и завършва бързо. Ние използваме многосигнално изчакване: мрежа в покой, стабилност на DOM и минимален праг на забавяне, настроен от емпирично тестване в стотици конфигурации на CMP. Тестът с презареждане и consent respawn. Стъпката с презареждане е това, което разкри consent respawn като явление. Не сме тръгнали да го търсим -- нашият оригинален тест на потока за отхвърляне проверяваше само непосредственото състояние след отхвърляне. Но по време на разработката забелязахме сайтове, които изглеждаха чисти след отхвърляне, но имаха бисквитки за проследяване, когато проверихме отново след презареждане на страницата. Първоначалното отстраняване на грешки предположи проблем с тайминга на скенера. Допълнителното разследване потвърди, че е реално: скриптове от трети страни, поставящи отново бисквитки при зареждане на страницата независимо от състоянието на съгласие.Добавихме изрично откриване на respawn към конвейера: след потока за отхвърляне скенерът презарежда страницата, изчаква стабилност и сравнява описа на бисквитките със снимката след отхвърляне. Всяка бисквитка, която е била премахната при отхвърляне и се появява отново след презареждане, се маркира като respawn. Това хвана 1 642 сайта с 4 932 бисквитки с respawn -- констатация, която щеше да бъде невидима без стъпката с презареждане.
Полингът `waitForScriptIdentifiedCMP`. Някои CMP се зареждат асинхронно и не изобразяват банера си до няколко секунди след първоначалното зареждане на страницата. Ако скенерът пристъпи към стъпката за отхвърляне, преди CMP да се е инициализирал, той или пропуска банера изцяло, или взаимодейства с частично зареден интерфейс. Реализирахме механизъм за полинг, който изчаква JavaScript API на CMP да стане достъпен (напр. `__tcfapi` за CMP, базирани на TCF, глобалната `Cookiebot` за Cookiebot), преди да продължи. Това добавя забавяне на сканиране, но значително намалява фалшивите негативни резултати от асинхронно зареждане на CMP.Предизвикателство 3: Насищане на конвейера при мащаб
Сканирането на 97 304 уебсайта не е работа за една машина. Всяко сканиране стартира Chromium процес, навигира до уебсайт, прихваща и класифицира стотици мрежови заявки, прави множество екранни снимки и изпълнява модули за анализ. Едно сканиране отнема 30-90 секунди в зависимост от сложността на сайта. При 15 едновременни сканирания на работник управлението на ресурсите става основно инженерно безпокойство.
Архитектурата със семафори
Използваме модел на конкурентност, базиран на семафори, за ограничаване на броя на едновременните Chromium процеси на работник. Всеки работник има фиксиран семафор (15 слота в нашата производствена конфигурация). Сканирането придобива слот преди стартиране на браузъра и го освобождава при завършване. Това предотвратява изчерпването на паметта -- 15 екземпляра на Chromium с пълно прихващане на заявки вече консумират значително RAM -- и осигурява обратно налягане срещу Redis опашката.
Изключението за заявка за документ
В началото на разработката срещнахме проблем с пропускната способност: нашата логика за прихващане на заявки (която инспектира всяка заявка за SSRF безопасност -- блокирайки заявки към частни IP диапазони, вътрешни мрежи и други потенциално опасни цели) добавяше забавяне към всяко зареждане на ресурси, включително основната заявка за документ. Тъй като URL адресът на документа вече е бил валидиран преди началото на сканирането, добавихме бърз път за заобикаляне: заявки от тип документ към предварително валидирания целеви URL пропускат пълния конвейер за прихващане. Тази привидно малка оптимизация имаше значително въздействие върху цялостната пропускна способност, защото заявката за документ блокира всичко останало.
Предварително загряване на DNS
Първата заявка към нов домейн предизвиква DNS справка, която в нашата инфраструктура можеше да добави 50-200ms на домейн. При среден сайт, контактиращ 10,4 домейна на трети страни (а някои до 171), времето за DNS резолюция се натрупваше значително. Реализирахме предварително загряване на DNS, използвайки локален Unbound DNS резолвер кеш: преди всяко сканиране резолвираме целевия домейн и загряваме кеша. Екземплярът на Unbound обслужва кеширани отговори за последващи справки в рамките на сканирането, намалявайки натоварването за DNS на домейн до под милисекунда.
SSRF безопасност при мащаб
Всяка заявка, прихваната от скенера, се проверява срещу набор от правила за безопасност, преди да бъде разрешена. Заявки към частни IP диапазони (RFC 1918, RFC 4193, link-local, loopback) се блокират. Това предотвратява злонамерен целеви сайт да използва скенера като SSRF вектор за проучване на вътрешни мрежи.
Предизвикателството при мащаб беше разграничаването на истинските SSRF блокирания от насищане на семафора. Когато всичките 15 слота на семафора са заети и сканиране не може да придобие слот, полученото изтичане на времето изглежда повърхностно подобно на заявка, блокирана по причини за безопасност. Добавихме изрична категоризация на грешките, за да разграничим „блокирано, защото целта е резолвирана до частно IP" от „блокирано, защото скенерът е на капацитет". Това беше от съществено значение за оперативния мониторинг и за точната класификация на неуспехите при сканиране.
Предизвикателство 4: Откриване на укриване от ботове
По време на проучването идентифицирахме 137 уебсайта, които изглежда умишлено скриват банера си за съгласие от автоматизирани скенери. Банерът се сервира на човешки посетители, но се потиска, когато сайтът открие характеристики на автоматизирано сърфиране.
Най-често срещаният механизъм, който идентифицирахме, включва конфигурационната опция `isAcceptAllForBots` на WordPress плъгина RCB (Real Cookie Banner). Когато е активирана, тази настройка открива автоматизирани браузъри (чрез `navigator.webdriver`, евристики на user-agent или поведенчески сигнали) и или автоматично приема съгласие от тяхно име, или скрива банера изцяло. Намерението, както е документирано от плъгина, е да предотврати обслужването на диалог за съгласие на автоматизирани посетители, с който те не могат смислено да взаимодействат. Ефектът е, че скенери за спазване -- и регулаторни одитори, използващи автоматизирани инструменти -- виждат сайт, който изглежда няма механизъм за съгласие, когато човешките посетители виждат пълен банер за съгласие.
Това е проблем на прозрачността. Ако механизмът за съгласие на уебсайт е видим само за човешки посетители, той не може да бъде одитиран в мащаб. Маркираме тези сайтове отделно в нашите резултати, защото констатацията е качествено различна от „не е открит банер". Сайтът има банер; той избира да не ни го покаже.
Откриваме укриването от ботове чрез комбинация от сигнали: наличието на известна конфигурация за откриване на ботове в настройките на CMP (достъпна чрез JavaScript инспекция), несъответствия между това, което показва DOM и какво докладва API на CMP, и в някои случаи чрез сравняване на резултати от автоматизирано сканиране с ръчна проверка.
Числото 137 със сигурност е подценено. Можем да открием укриване от ботове само когато можем да идентифицираме механизма. Сайтове, използващи по-усъвършенствано или персонализирано откриване на ботове, може успешно да заобиколят както нашия скенер, така и откриването ни на укриване.
Предизвикателство 5: Погрешна идентификация на CMP
Сайт може да зарежда множество скриптове, които приличат на платформи за управление на съгласието. PiwikPRO включва CMP компонент, но е предимно аналитичен пакет. Някои WordPress сайтове зареждат Complianz наред с отделен аналитичен плъгин, който има CMP-подобни функции. Корпоративни сайтове може да имат останки от предишен CMP, който все още се зарежда наред с текущия.
Наивното откриване -- „ако виждаме скрипта, това е CMP" -- произвежда фалшиви идентификации, които се каскадират в неправилно взаимодействие с банера. Ако скенерът идентифицира PiwikPRO като CMP и се опита да използва селекторите на банера на PiwikPRO, може да пропусне действителния банер на tarteaucitron, който контролира съгласието на сайта.
Нашият подход, базиран на увереност, адресира това чрез класиране на CMP кандидатите. Когато са открити множество потенциални CMP:
1. Проверяваме кой има видим банер в DOM (скрипт присъства, но няма банер означава вероятно неактивен или CMP не се използва).
2. Проверяваме кой разкрива активен CMP API (напр. функциониращ `__tcfapi` или еквивалент).
3. Предпочитаме CMP, чиито верифицирани селектори съответстват на видими DOM елементи, пред този, който е открит само по URL на скрипта.
Тази евристика не е перфектна, но разреши най-честите случаи на погрешна идентификация, които срещнахме по време на разработката и тестването.
Ограничения
Никой автоматизиран скенер не може перфектно да възпроизведе всяко човешко преживяване при сърфиране. Това са известните ограничения:
Банери, зависещи от GeoIP. Някои CMP, по-специално CookieYes, обслужват различни преживявания за съгласие въз основа на IP геолокацията на посетителя. Нашите сканирания произхождат от конкретни мрежови местоположения в Европа. Сайт, който показва банер за съгласие на посетители от Франция, но не на посетители извън ЕС, ще покаже различни резултати в зависимост от произхода на сканирането. В момента не сканираме всеки сайт от всяка държава в ЕС. Затворен shadow DOM. Някои CMP изобразяват банера си вътре в затворен shadow DOM, който е недостъпен за стандартни DOM заявки чрез `document.querySelector`. CMP на Transcend използва този подход. Нашият скенер може да открие shadow host елемента, но не може да инспектира съдържанието му, за да намери бутони за приемане/отхвърляне. Тези сайтове обикновено завършват като неопределени в нашите резултати. Динамични имена на класове и обфускация. Някои CMP, по-специално Admiral, използват динамично генерирани имена на класове, които се променят при всяко зареждане на страницата. Откриването, базирано на селектори, се проваля за тях, защото селекторите не са стабилни между посещенията. Връщаме се към общи евристики, но увереността е по-ниска. Приложения с една страница. SPA, които управляват състоянието на съгласие изцяло в клиентски JavaScript и зареждат механизма за съгласие след първоначални промени на маршрута (а не при начално зареждане на страницата), са по-трудни за оценка. Нашият скенер навигира до URL и изчаква страницата да се стабилизира, но не симулира навигация в рамките на приложението. Банер за съгласие, който се появява само след като потребителят навигира в SPA, може да бъде пропуснат. Езиково покритие. Нашето откриване на бутон за отхвърляне използва съпоставяне на текст в множество поддържани езици, но не покриваме всеки език на ЕС еднакво. Банер на малтийски или естонски може да има етикети на бутон за отхвърляне, които нашето съпоставяне не разпознава, водейки до пропуск при тестване на потока за отхвърляне (макар че самият банер може да бъде открит от структурни евристики). Крайни случаи с тайминга. Скрипт, който се задейства 30 секунди след зареждане на страницата, ще бъде пропуснат от сканиране, което изчаква 15 секунди за мрежа в покой. Използваме щедри таймаути, но дългата опашка на асинхронно поведение е по своята същност трудна за улавяне изцяло.Тези ограничения допринасят за нашия 14,9% процент на неопределени резултати.
Инфраструктурата
Производствената инфраструктура за сканиране се състои от:
- Скенерен двигател: Go приложение, използващо chromedp като CDP клиент за автоматизация на Chromium. Go беше избран заради модела си на конкурентност (горутини и канали се съпоставят естествено с паралелна оркестрация на сканирания), характеристиките на производителност и простотата на деплойване (единичен статичен двоичен файл).
- Среда за изпълнение на браузъра: Headless Chromium, стартиран за всяко сканиране чрез CDP. Всяко сканиране получава нов браузърен процес без споделено състояние.
- Опашка: Работна опашка, подкрепена от Redis, разпределяща URL адреси към работници за сканиране. Redis се справя с разпределението на задачите, проследяването на напредъка и ограничаването на скоростта.
- База данни: PostgreSQL за трайни резултати от сканиране, констатации, метаданни за доказателства и всички структурирани данни. Сканирания, констатации, бисквитки, заявки и резултати от анализ се съхраняват релационно.
- DNS кеш: Локален Unbound резолвер, предоставящ кеширани DNS справки и SSRF-безопасна резолюция.
- Съхранение на доказателства: Екранни снимки, HAR файлове и PDF доклади се съхраняват като трайни артефакти, свързани със записи за сканиране.
За проучването на 97 304 сайта обработихме 114 748 кандидат-URL адреса (97 304 завършиха успешно) за приблизително 2,5 дни, използвайки 3 сървърни инстанции, изпълняващи работници за сканиране паралелно. Всеки сървър изпълняваше множество работни процеси с по 15 едновременни слота за сканиране. Пиковата пропускна способност беше приблизително 25-30 завършени сканирания на минута на сървър.
Основното тясно място не беше процесорът или паметта, а мрежата: всяко сканиране генерира стотици изходящи заявки (към целевия сайт и неговите ресурси от трети страни), и агрегираната честотна лента и броят на връзките при всички едновременни сканирания насищаха наличния мрежов капацитет, преди другите ресурси да бъдат изчерпани.
Отворени предизвикателства и бъдеща работа
Няколко проблема остават нерешени или частично решени:
Локализация на банери за съгласие. Нашето съпоставяне на текст покрива основните езици на ЕС, но е непълно за по-малки езикови общности. Разширяването на покритието изисква не само добавяне на преводи, но и валидиране, че селекторите и моделите на взаимодействие работят правилно с локализирани версии на CMP. Надлъжен мониторинг. Нашата текуща архитектура е оптимизирана за сканиране в момент от времето. Откриването на промени в поведението на съгласие с течение на времето -- подобрил ли се е сайт след прилагане? Поправило ли е обновление на CMP клас от неуспехи в потока за отхвърляне? -- изисква повтарящи се сканирания с диференциален анализ, което е архитектурно различно от еднократната оценка. Сравнителен анализ на спазването по CMP. Имаме данните да оценим нивата на спазване по CMP (асоцииран ли е Cookiebot с по-добро спазване от OneTrust?), но разделянето на качеството на CMP от качеството на конфигурацията от оператора е методологически сложно. CMP, който по-често се внедрява от големи предприятия с отделни екипи за поверителност, ще изглежда по-добре в агрегат, дори ако самият инструмент не е по-съвместим. Проверка на състоянието на съгласие в реално време. Текущият скенер работи в пакетен режим. Интегрирането на проверката на съгласието в CI/CD конвейери или мониторинг в реално време изисква по-бърз, по-лек режим на сканиране, който жертва известна дълбочина на доказателства за скорост. Проучваме това.API
Същият скенерен двигател, описан в тази публикация, е достъпен чрез публичния API на GDPR Monitor. Можете да изпращате заявки за сканиране програмно, да проверявате за резултати и да извличате структурирани констатации и артефакти с доказателства. API връща същите данни, които показва нашият потребителски интерфейс: снимки преди съгласие, описи на бисквитки, резултати от откриване на CMP, резултати от потока за отхвърляне, рискови резултати и пълни вериги от доказателства.
Ако изграждате инструменти за спазване, интегрирате проверки за поверителност в CI/CD конвейери, провеждате собствено изследване или изграждате мониторинг в програма за поверителност, API предоставя достъп до поведенчески анализ на съгласието без необходимостта от изграждане и поддръжка на собствена инфраструктура за автоматизация на Chromium.
Опитайте сами. Документация за API е достъпна на gdprprivacymonitor.eu/developers/api. Подайте един URL или интегрирайте автоматизиран мониторинг на поверителността в работния си процес.
Проверете вашия уебсайт
Стартирайте безплатно сканиране за съответствие с GDPR — без регистрация.
Сканирайте уебсайта си безплатно