TL;DR: On the homepage the consent setup looked textbook — tracking tags rewritten to
text/plain, waiting for opt-in. On/imprint,/privacy,/user/loginand an AJAX newsletter route of the same estate, the exact same tags rendered executable: analytics and a Meta pixel fired without consent, a device-storage cookie was set withoutSecure, and no banner appeared. Four domains, same defect. The lesson: consent software is an attachment problem, and you have to test every route class — not the banner.
The scenario
A municipal marketing organisation runs its city brand on a four-domain Drupal multisite. One platform, one consent manager, one analytics stack. I was asked to look at the web presence end to end, so I started where every visitor starts: the homepage, the legal pages, the login, the forms.
The homepage was reassuring. The consent manager rewrote every tracking tag to an inert template type, the analytics and pixel libraries were loaded only after opt-in, and a fresh browser profile showed zero third-party requests. That is how it should look.
Then I pointed the same fresh-profile test at the legal pages — the pages a visitor opens precisely to understand what happens with their data. On those pages the tracking fired immediately: no banner, no opt-in, device storage written anyway.
I removed all identifying information. The target is described only by sector and technology class so the pattern can be reused.
The pattern
A consent setup is only as good as its route coverage
The defect was not a broken banner and not a misconfigured service. The consent library that rewrites tracking tags into opt-in templates was attached to content pages — and not attached to the system and legal routes. On those routes the same tracking modules rendered as plain executable scripts. The visitor gets tracked on exactly the pages where the site explains that tracking would never happen without consent.
The evidence is a route matrix, not a single check
A single check on the homepage proves nothing. The test that mattered was the same probe run against every route class: content, legal pages, login, AJAX endpoints, and the error page. The result was a matrix with one green row (content) and four red ones (system and legal). A fresh browser profile, cache-busting and no interaction are essential — a warm cache or a previous consent decision hides the defect.
The root cause was visible in the HTML
On gated pages the tags carry type="text/plain" and a data-cookieconsent attribute; the consent library's config and preloader appear in the page source. On the affected routes those references were simply absent — zero consent library references in the HTML, while the tracking scripts were emitted as executable <script> elements. The 404 page attached the library again. That makes it a route-dependent attachment bug, not a global misconfiguration: the module behaviour is correct, the attachment scope is wrong.
Findings
| Severity | Finding | Evidence |
|---|---|---|
| HOCH | Consent gate missing on system and legal routes of all four domains | Fresh headless profile: analytics + Meta pixel load, _fbp set without Secure, no banner; content pages correctly gated |
| MITTEL | Two domains in the family without SPF at all, one without DMARC/DKIM | DNS TXT records; forged mails in the brand name are deliverable |
| MITTEL | Public module blueprint (README, 11 KB) on all four domains | Importers, external API endpoint, plaintext key hint, nightly cron windows |
| MITTEL | Module test script anonymously executable on all four domains | HTTP 200 JSON endpoint reachable in production |
| MITTEL | Analytics server three security releases behind | Version disclosed by a public changelog file; XSS-escaping fixes missing |
| NIEDRIG | Version disclosure, no security.txt, no CAA, installer redirect via plain HTTP | Headers and DNS |
The attack path
Reproducing it in the lab
Every probe below targets a lab multisite I control. None of it is directed at a live system.
# 1. control: does the content page gate its tags and attach the library?
curl -s http://lab.local/ | grep -o 'type="text/plain"[^>]*' | head -3
curl -s http://lab.local/ | grep -c 'consent-preloader'
# <- inert template tags, preloader present
# 2. same check on a system route
curl -s http://lab.local/user/login | grep -c 'consent-preloader'
curl -s http://lab.local/user/login | grep -c 'var _paq'
# <- 0 library references, executable analytics snippet
# 3. the real test: fresh profile, no interaction, per route class
for u in / /imprint /privacy /user/login /ajax/newsletter; do
TARGET="http://lab.local$u" node consent-audit.js
done
# <- fresh profile on the legal routes: third-party requests + cookie, no banner
# consent-audit.js asserts, per route:
# third-party requests before consent == 0
# cookies before consent == 0
# banner visible when tracking exists == true
# any route that violates the first two assertions is a finding.
Detecting it in your own estate
- Test route classes, not pages: content, legal pages, login, AJAX endpoints, search, error page, print views. Consent libraries attach per route; one excluded template is enough.
- Use a fresh browser profile with cache disabled for every probe. A previous consent decision or a warm page cache will show green where the visitor sees red.
- Check the HTML differential: gated tags carry
type="text/plain"plus a consent attribute, and the library config appears in the source. If tracking tags sit in executable<script>elements on a route, the gate is missing there — regardless of what the banner says. - Assert the outcome, not the tooling: zero third-party requests and zero cookies before opt-in. Make it a CI check over a list of routes and alert on regressions.
- Keep the privacy policy consistent with reality. A pixel described as "legitimate interest" while the consent config marks it as
needConsentis a contradiction a reviewer will find.
Remediation
# immediately
1. attach the consent library globally (Drupal: hook_page_attachments),
not via per-route blocks; fix the block visibility that excludes
system and legal routes
2. remove or gate the AJAX newsletter route; clear all caches
3. re-verify headless, per route class: 0 third-party requests pre-consent
4. block tests/, docs/ and README* under /modules/; rotate the plaintext key
# within one week
5. update the analytics server past the missing security releases
6. unify the email policy: SPF -all, DKIM and DMARC on every domain
7. ship the header baseline on every vhost: HSTS, CSP, X-Frame-Options,
Referrer-Policy, Permissions-Policy
# within one month
8. add the regression test to CI and to the rollout checklist of
every consent-tool or template change
Once an attacker gains access, they can perform the following actions:
Takeaways
- A consent banner is not a consent gate. What matters is whether the tracking tags are inert on every route — and that is an attachment question, not a UI question.
- The pages users visit to learn about tracking are the worst place for a bypass. Test legal pages first, not last.
- One green homepage row means nothing. A route matrix with fresh profiles is the only honest test.
- The same estate rarely has exactly one leftover. A route-scope bug sits next to public module documentation, an open test endpoint and a split email policy.
- Fix the attachment once, then verify the outcome per route in CI — otherwise the next template change reopens the gate silently.