~/blog/perfect-consent-one-patch-behindPOSTED
by Shady Nathan Tawfik · October 8, 2026 · 9 min

Perfect Consent, One Patch Behind: The Law Firm With the Cleanest Privacy Layer I Have Measured

A German law firm's website had the cleanest consent architecture I have measured — zero cookies and zero third-party requests before opt-in. It was also one patch behind a public unauthenticated XSS fix, shipped zero security headers on every page except the login, leaked tokens anonymously from its REST API, and left its mail trust chain half-built. Privacy maturity and security maturity are owned by different people.

Interactive[blog-2.0]

TL;DR: A German law firm's website did consent better than anything I have measured: a fresh profile loaded zero cookies, zero third-party requests, zero iframes — only the consent manager itself, with fonts served locally. Seven days after the fix for an unauthenticated theme XSS was published, the site still ran the vulnerable version, with no CSP anywhere to contain it. Add zero of six security headers site-wide, an anonymous CSRF-token endpoint on the REST API, publicly readable readme/license files and a DMARC policy that forgives forged client mail. Two teams, two maturity levels, one origin — and the weaker one sets the risk.

The scenario

The client is a German law firm. Client money moves on written instruction, so mail authenticity is not a compliance topic there — it is an operational control. I was asked to review the public web surface: non-intrusive, no payloads against the live host, no login attempts. Headers, DNS, TLS, REST probes, and a headless browser with a fresh profile.

The first test is always consent, because it tells me how the site treats the visitors it does not have to. It came back green in a way I had not seen before: zero cookies, zero third-party requests, zero iframes before any interaction. Only the consent manager itself loaded. Google Fonts served locally. That is textbook — better than textbook, because most sites with a consent tool still fire something.

Then I ran the same fresh profile against the rest of the surface, and the picture split in half.

I removed all identifying information. The target is described only by sector and technology class so the pattern can be reused.

The pattern

Maturity in one dimension does not transfer

Whoever configured the consent stack cared, tested, and understood the topic. No third-party request before consent is not a default — it is a decision that someone made and verified. The same site shipped zero of six baseline security headers on every page. Those two facts coexisted for the same reason they coexist everywhere: privacy and security are different owners, different ticket queues, different definitions of done. A review that stops at the first green area misses the finding. You have to measure every dimension separately, because organizations do not leak maturity sideways.

A patch window is a public countdown, not an internal note

The site's theme was one patch behind an unauthenticated reflected XSS fix that had been public for seven days. Rendering the homepage tells you the version. Mapping that version against current advisories takes minutes. The gap between "fix published" and "installed" is measurable from the outside, and anyone with a scanner computes it automatically. The uncomfortable part is not the vulnerability — it is that a seven-day window cannot be distinguished from an unpatched site by any external observer. The only defense is to close the window before it becomes a statistic.

"The login is hardened" proves capability, not coverage

The login page had X-Frame-Options, a frame-ancestors policy and a Secure test cookie. Every other page had nothing. That asymmetry is diagnostic: someone in the organization knew the header baseline and applied it exactly once, at the page that historically gets attacked. But a reflected XSS does not care about the login form — it executes in the origin of whatever page the victim is on, and a victim who is logged in is exactly the target. Coverage is the metric, not capability.

An anonymous nonce endpoint is a preassembled attack tool

The REST index was 328 KB of route names — including security-plugin and editor namespaces — and one endpoint returned a CSRF nonce plus the admin path to any anonymous caller. User enumeration was properly blocked at the same API, which shows the lockdown was deliberate but incomplete. Token endpoints are the worst thing to leave open: enumeration gives an attacker a map, a nonce gives them a key blank they can cut later.

For some sectors, mail is the perimeter

SPF ended in -all, which is the part everyone gets right. DMARC sat at p=quarantine with no DKIM selector found — so mail that fails alignment is quarantined, not rejected, and a mailbox provider's spam decision becomes the last line of defense. For a law firm, the realistic fraud is not spam: it is a forged instruction to a client, with new bank details. That threat never touches the website. It is decided entirely by DMARC and DKIM, which makes them client-protection controls, not hygiene.

The control that worked — and shaped the engagement

The security plugin rate-limited us off the site after roughly sixty requests. That is a working control, and it is worth saying so: most sites I test do not react at all. It also disciplined the audit — the block was respected, affected probes were re-run after it lifted, and the timeline is documented in the evidence. A defense that changes an attacker's schedule is a defense doing its job.

TL;DR: The cleanest consent setup I have measured — zero cookies and zero third-party requests before opt-in — sat on a law firm site that was one patch behind a public unauth XSS fix, shipped zero of six security headers outside the login, handed out CSRF nonces to anonymous callers, and left DMARC at quarantine without DKIM.

Findings

SeverityFindingEvidence
HOCHUnauthenticated reflected XSS in the theme, seven days after the public fix — no CSP anywhere to contain itVersion on the wire vs. advisory timeline; lab mirror echoes the payload unescaped
HOCHZero of six baseline security headers on every page except the loginHeader capture of homepage, contact and login in direct comparison
MITTELREST index (328 KB) anonymous; an endpoint returns a CSRF nonce and admin path without loginREST capture; token endpoint response
MITTELwp-cron.php publicly reachable — unauthenticated cron triggerHTTP 200 on the live site
MITTELDMARC only p=quarantine, no DKIM selector — forged client mail is quarantined, not rejectedDNS TXT records
MITTELNo SRI on the consent vendor's external scriptsHomepage HTML
NIEDRIGreadme.html and license.txt open; pages countable (25 pages, 28 posts, 181 media); ftp/dev legacy hosts on foreign serversPath probes, REST counts, DNS
NIEDRIGCMP storage entry written before consent; 90-day certificate cycle without HSTSHeadless capture, TLS record
GOODConsent architecture: zero cookies, zero third-party requests, zero iframes before opt-in; hard paths 403; user enumeration blocked three ways; rate limiting workedFresh-profile capture; path probes; the block itself

The attack path

Version fingerprint shows theme one patch behindAdvisory maps fix published seven days earlierNo CSP anywhere to contain the scriptCrafted link reaches a logged-in editorScript runs in the firm originAnonymous nonce endpoint supplies tokensAdmin actions and client data within reach

Reproducing it in the lab

Every probe below targets a lab mirror I control. None of it is directed at a live system.

~/code/bash bash
# 1. the control: consent in a fresh profile
TARGET_URL=http://lab.local/ node consent-audit.js
# <- cookies: 0  third-party hosts: only the CMP  banner: present

# 2. the same origin, the six headers
curl -sI http://lab.local/ | grep -ciE \
  'strict-transport|content-security|x-frame|x-content-type|referrer-policy|permissions-policy'
# <- 0
curl -sI http://lab.local/wp-login.php | grep -iE 'x-frame|content-security'
# <- the login is hardened — capability, not coverage

# 3. version vs. advisory
curl -s http://lab.local/ | grep -oE 'ver=[0-9.]+' | sort -u
# <- theme version one release behind the fix

# 4. the payload, lab only
curl -s 'http://lab.local/?lang=%22%3E%3Cimg%20src%3Dx%20onerror%3Dalert(1)%3E' \
  | grep -o 'img src=x'
# <- vulnerable build echoes it raw; hardened build returns &lt;img

# 5. the token endpoint
curl -s http://lab.local/wp-json/security-plugin/v1/authenticate
# <- vulnerable: {success, nonce, adminUrl}; hardened: 401
~/code/text text
# assertions the lab mirror checks, vulnerable vs. hardened:
#   security headers count       7 checks: 0        -> 6
#   lang reflection              raw                 -> escaped
#   readme.html / license.txt    200                 -> 404
#   REST user list               200 with names       -> 401
#   token endpoint               200 + nonce          -> 401
#   wp-cron.php                  200                  -> 403

Detecting it in your own estate

  • Run the consent test and the security test as separate drills with separate owners. A green privacy result can hide an unpatched theme for months, because nobody re-opens the security backlog after a compliance win.
  • Track patch windows as a number: for every component, days between the fix publication and your deployment. Alert on anything above your policy threshold instead of waiting for the next release train.
  • Assert the header baseline on every vhost and every route class, not on the login. If the login is the only hardened page, the hardening was done by hand — automate it or it will never spread.
  • Inventory REST endpoints by what they return to anonymous callers. Any endpoint returning a nonce, token or URL is a finding, even if user enumeration is blocked elsewhere.
  • Treat DMARC and DKIM as client-protection controls: DKIM first, then move to p=reject once the reports stay clean. Quarantine is a suggestion; reject is a decision.
  • Measure your own defenses from the outside. The rate limit on this site fired and changed the engagement — know the threshold and verify it works before an adversary does.

Remediation

~/code/text text
# immediately
1. update the theme to the fixed release; verify the new version on the wire
2. ship the header baseline everywhere (HSTS, CSP, X-Frame-Options, nosniff,
   Referrer-Policy, Permissions-Policy) — .htaccess or vhost, not per page

# within one week
3. restrict the REST API to authenticated users; disable vendor and token
   endpoints for anonymous callers
4. block readme.html, license.txt and wp-cron.php; switch to DISABLE_WP_CRON
   with a system cron
5. enable DKIM at the mail gateway and publish the selector; plan the move
   to DMARC p=reject after 30 clean report days

# within one month
6. SRI or CSP hashes for the consent vendor; publish security.txt (200);
   add CAA, inventory the legacy hosts, add certificate renewal alerting

Once an attacker gains access, they can perform the following actions:

Unauthenticated reflected XSS in the theme with no CSP: crafted link to a logged-in editor, script runs in the firm origin
Zero security headers outside the login: no HSTS, no CSP, no frame protection — every browser-side mitigation absent
DMARC quarantine without DKIM: forged client instructions are evaluated, not rejected — money-transfer fraud risk
Anonymous REST endpoint returns CSRF nonce and admin path; 328 KB index and media counts publicly enumerable
Public wp-cron trigger, open readme/license, legacy ftp/dev hosts — version and config intelligence for free

Takeaways

  • Privacy maturity does not transfer to security maturity. They have different owners, different queues, and the weaker one sets the risk.
  • A patch window is external, public and countable: version on the wire plus fix publication date. Seven days unpatched and never patched look identical from the outside.
  • Hardening one page proves someone cared. Hardening every route proves the control exists. Reflected XSS targets the pages you did not harden.
  • Token endpoints are worse than enumeration. A map is reconnaissance; a nonce is an attack tool.
  • For professional services, mail authenticity is client protection. SPF with -all is a start; DKIM and p=reject are the finish.
  • Praise what works. The consent layer and the rate limiter genuinely worked — the report says so, because security advice that only counts failures is not advice, it is a scoreboard.
Shady Nathan Tawfik

Founder & Cyber IT Security Engineer in Bielefeld. Offensive security, audits and full-stack builds. More at

For educational purposes only. Every reproducible command targets my own lab environment (localhost), never a live system. © 2026