~/blog/org-chart-is-the-payloadPOSTED
by Shady Nathan Tawfik · October 22, 2026 · 15 min

The Org Chart Is the Payload: What a Clean Front End Still Publishes

A pattern essay about the half of a web estate that a website review usually misses: a static front end with perfect cookie hygiene, and behind it a CMS on its own subdomain — a frameable admin login, an unprotected staging twin, an API that ignores visibility flags, forms that validate only in the browser, and a mail domain with no policy at all. The chain is not an exploit. It is a spear-phishing kit built from public data.

Interactive[blog-2.0]

TL;DR: The front end was genuinely clean: no analytics, no pixels, no third-party fonts, zero cookies before consent, blocked dotfiles, HTTPS only. The CMS behind it lived on its own subdomain — and that host had no clickjacking protection on the admin login, a staging twin without the basic auth that protected the staging front end, an API that returned records the interface deliberately hides, forms whose only validation ran in the browser, and a mail domain with SPF hardfail but no DMARC and no DKIM. Nothing here is a remote code execution. Together it is a targeted phishing kit: named employees, direct dials, mobile numbers, a login page that can be framed on the real origin, and a domain whose name can be forged with no policy to stop it.

The scenario

A mid-sized supplier asked me to look at its website. It sells to clinics and care facilities, so its customers are procurement departments, technical managers and architects — people who receive quotes, invoices and project documents by mail and who are trained to check the sender.

The public site is a pre-rendered JavaScript build served as static files behind a normal web server. A separate application on a subdomain holds the content and the admin interface. Two more hostnames appeared in the certificate register: a staging front end and a staging version of that application.

I looked at the front end first. Then I stopped looking at the front end, because everything interesting was on the other host.

This is a pattern essay, not a case report. There is no country, no product, no hostname, no version, no date and no count from the engagement in it. The mechanics are unchanged; the target is not recoverable.

What the front end does right

It is worth saying this clearly, because the rest of the article is about what went wrong:

  • No third-party requests except the consent tool. No tag manager, no analytics, no advertising pixels, no map embed, no video embed, no web fonts from a CDN. Fonts are self-hosted. The only external origin on the page is the CMP itself.
  • Zero cookies before consent. Measured in a fresh browser profile: home, blog and contact page set nothing at all. Not a consent banner that "blocks" things — there was nothing to block.
  • HTTPS everywhere. HTTP answers with a permanent redirect on every host, HSTS includes subdomains, the certificate chain is valid, TLS 1.0 and 1.1 are refused.
  • Boring hardening that works. nosniff, a referrer policy, TRACE rejected, dotfiles and environment files return 403, no directory listings, no source maps, no framework debug pages, no stack traces.
  • A small API. Three read endpoints and two form endpoints. No user list, no admin endpoint, no half-finished internal routes.

That is better than most sites I look at. It is also why the rest matters: someone cared about this front end. The same care did not reach the host next door.

The second host is the real front door

The CMS lived on api. — a different origin, a different stack, a different set of headers. That is a normal architecture: static site for speed, application for editing. The problem is that teams review the website and stop at the website. The application is not part of "the website" in anyone's checklist.

What the application host was missing, on both production and staging:

  • No X-Frame-Options, no frame-ancestors. The admin login page could be embedded in any page on the internet. A framed login on the real origin, with the real certificate and the real URL bar, is the most convincing credential phishing page there is — and it does not need a lookalike domain.
  • No Content-Security-Policy. On an SPA admin with a session cookie, that removes the last mitigation for any injected script.
  • A version string in the login page. Not a vulnerability by itself; useful for choosing which public issue to try first.

The login itself was CSRF-protected — six token-less POSTs were rejected — and the session cookie was Secure, HttpOnly, SameSite=Lax. Someone did the framework defaults. They just never added the two headers that stop a browser from putting the page in a frame.

The staging twin nobody protects

The staging front end was behind HTTP basic auth. A curious visitor got a password prompt and moved on.

The staging application had no such protection. Same login page, same API, same version string, same framework — reachable by anyone who reads the certificate register. Its robots.txt did not disallow anything either.

This is the most common split I find in mid-sized companies: the environment that gets a password is the one with a nice URL and a visible page, and the environment that holds the database, the admin session and the real attack surface does not. Staging is also where credentials are weakest — reused passwords, test accounts, a database restored from production "just for a moment" — and where nobody watches the logs.

An unprotected staging backend is not a data breach. It is a free training ground for the attack on production.

The API does not know about your filters

The contact page was fed by a public endpoint that returned staff names, roles, direct dials and mobile numbers. That is a deliberate publishing decision: sales wants reachable people. Fine.

Two records were flagged showInContacts: false. The interface honoured the flag and did not render them. The API returned them anyway — both in the list and through the single-record route — including a seeded placeholder with a test address, a one-line phone number and alt text that literally said "alternative text German".

No customer data leaked. What leaked was the assumption: the team believed a boolean in the database was an access control. It was a display setting. Every field you return is published; every filter that runs in the browser is a suggestion.

This is the same pattern as a media library where unpublished images stay downloadable, or a shop API that lists drafts. The object is public as soon as an endpoint can name it.

Forms that trust the browser

The application form accepted a CV and attachments. The client code was careful: allowed extensions, a size limit per file, a total limit, a honeypot field, a privacy checkbox. All of it ran in the browser, and all of it is optional for anyone who posts directly.

What the endpoint looked like from outside:

  • No CSRF token. The API is stateless and accepted the POST without one.
  • Access-Control-Allow-Origin: *. Any website could post to it. There are no credentials to steal, so this is not "CORS with credentials" — it is cross-site write access to a business inbox.
  • Files as base64 inside JSON, in the shape {Name, Type, Content}. The browser reads each file, strips the data-URL prefix and sends the rest as text. Type and name come from the client. Validation, if it exists, has to decode and re-check everything server-side — and the client-side size limit is exactly the kind of rule that gets skipped in that path.
  • 500 Server Error on an empty body, not 422. A validation-first design returns field errors. A server error means the request reached code that expected data — and that each malformed request writes an exception into the log.

I did not upload a file. A live upload writes into the client's system, and the mandate was non-intrusive. So the honest finding is the design, not a proven bypass: the only checks I could verify were the ones in the browser, and the endpoint accepted unauthenticated cross-origin posts.

For a company that hires, a fake application with an attachment is a delivery vehicle that arrives through the one inbox where opening files is part of the job.

Mail is three records, and two were missing

SPF was correct and hard: -all, with the mail provider and a signature service authorised. That blocks envelope spoofing.

Then:

  • no DMARC record — so no policy and no reporting;
  • no DKIM selector — nothing signs the domain;
  • no MTA-STS, no TLS reporting, no CAA.

A forged From: with an attacker-controlled envelope passes SPF for that envelope domain. Without DMARC there is no alignment check, so nothing tells the receiving server that the visible sender is a lie. The receiving side sees a plausible mail from the supplier's real domain, with the supplier's real logo, addressed to a named person whose direct dial is on the website.

That is the money path: a quote correction, an invoice, a change of bank details. Not a technical exploit — a business process attack. And in a supplier relationship where documents move daily, the mail is the product.

Meanwhile: the runtime became the target

While this engagement was being written up, the German BSI published an advisory that belongs in this story, because it lands exactly on the host type described above: the second host that nobody reviews.

On 21 October 2026 the PHP project released emergency patches for two remote code execution flaws in the PHP engine itself — CVE-2026-109618 (out-of-memory corruption) and CVE-2026-109619 (stale global state / use-after-free), both CVSS v4.0 9.2, both exploitable without authentication. Fixed releases: PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35; older branches receive no official fix. The BSI advisory is BITS-H no. 2026-297981-1132 (version 1.1, 21 October 2026, criticality 3/Orange, TLP:CLEAR).

Two details make it more than a patch note:

  • It fires before your code runs. The demonstrated path is Apache with mod_php, where the vulnerable sequence happens during request startup — the application never gets a chance to validate, log or rate-limit anything.
  • It was exploited before it was public. An exploitation attempt was observed four days before disclosure, and attempts rose sharply after the patch release.

For this pattern the lesson is uncomfortable: a static front end can be perfect, and the PHP runtime on the CMS host can still be the weakest component in the estate. The host that only exists to edit content is the one that executes the interpreter on every request — including the login page, the API and the form endpoints from the previous sections.

The same logic applies to every CMS in the estate: the version of the runtime is a property of the host, not of the repository, and nobody owns it unless somebody is assigned to it.

The certificate register is the map

None of the hostnames above were found by guessing. They were in public certificate logs — the same logs that exist to make TLS auditable. Group the entries by name and the estate appears: production, staging, API, an old alias. Structure first, then targets.

The chain

Certificate registerSecond hostStaging twinFrameable admin loginPublic staff APINamed targetsCMS accessForged supplier mailNo DMARC policyInvoice fraudContent manipulationVersion selection
TL;DR: The static front end had no trackers, no cookies before consent and a clean header set. The CMS behind it lived on its own subdomain: a frameable admin login, a staging twin without the basic auth of the staging front end, an API returning records the interface hides, forms validated only in the browser, and a mail domain with SPF hardfail but no DMARC and no DKIM. Not one exploit - a spear-phishing kit assembled from public data.

Findings

SeverityFindingEvidence
HOCHMail domain with SPF hardfail but no DMARC and no DKIM — a forged From is deliverable and nobody reports itAuthoritative DNS: no _dmarc, eight DKIM selectors empty, no MTA-STS, no CAA
MITTELThe staging twin of the admin/API is reachable without the basic auth that protects the staging front end401 on the staging front end, 302 → /login on the staging API
MITTELThe admin login cannot be framed-protected: no X-Frame-Options, no CSP frame-ancestorsResponse headers on production and staging
MITTELNo CSP and no Permissions-Policy on the marketing site or the API host; CMS content itself is cleanHeader capture; 40 records checked, 0 script tags, 0 inline handlers, 0 embeds
MITTELThe public API returns records the interface deliberately hides — in the list and through the single-record routeContacts endpoint vs. single-record request for a flagged placeholder
MITTELForm endpoints accept unauthenticated cross-origin posts, validate only in the browser, and answer 500 instead of 422Access-Control-Allow-Origin: *; empty-body POST; client bundle
MITTELPrivacy policy names a hoster that is not the network serving the site; CMP and mail processor unnamedPolicy text vs. RIPE whois of the public IP
NIEDRIGThe only third-party script (consent tool) loads without an integrity hashDOM inventory: zero integrity attributes
NIEDRIGCanonical and sitemap point at the apex, which 301s to www; one published article missing from the sitemapApex redirect; sitemap diff
NIEDRIGStaging API indexable; no security.txt; no CAA recordEmpty Disallow; 404 on /.well-known/; DNS
GOODFront end: zero third-party requests except the CMP, zero cookies before consent, self-hosted fonts, HTTPS-only with HSTS, dotfiles and environment files 403, no source maps; admin session cookie Secure/HttpOnly/SameSite=Lax, login CSRF-protected, admin API 401Fresh-profile capture, path probes, header capture

How to see it yourself

Everything above is visible from public data:

~/code/bash bash
# the estate, from public certificate logs
curl -s "https://crt.sh/?q=%25.example.tld&output=json" | jq -r '.[].name_value' | sort -u

# the front door versus the second host
curl -sI https://www.example.tld/ | grep -iE 'strict-transport|content-security|x-frame'
curl -sI https://api.example.tld/ | grep -iE 'strict-transport|content-security|x-frame'

# the staging twin: one host protected, the other not
curl -s -o /dev/null -w '%{http_code}\n' https://stage.example.tld/
curl -s -o /dev/null -w '%{http_code}\n' https://api.stage.example.tld/

# the org chart the website publishes
curl -s https://api.example.tld/api/contacts | jq '.[] | {name, email, phone, mobile, showInContacts}'

# mail policy
dig +short TXT _dmarc.example.tld
dig +short TXT selector1._domainkey.example.tld

Two rules for reading the result:

  1. Compare hosts. A site can be excellent and its API host mediocre. Review the estate, not the page.
  2. Check what the endpoint returns, not what the page renders. showInContacts, published, visible, status — every one of those fields is a UI concept until a server-side query enforces it.

Detection: what would have caught this

  • Asset inventory that includes subdomains and environments. Every host in the certificate register with an owner, an environment label and a control set. api.stage would have shown up as "staging, no basic auth".
  • Header baseline per host, not per site. One policy for the marketing host and the admin host, checked automatically. Missing frame-ancestors on a login page is a one-line assertion.
  • Mail policy as a monitored record. DMARC, DKIM and SPF checked weekly; an alert when a record disappears (they often disappear during a mail migration).
  • An API contract test that asserts filters. "Records with showInContacts=false must not appear in /contacts" is a test, not a code review.
  • Form endpoints in the negative-test suite. Empty body must return 422; a cross-origin POST must be rejected or rate-limited; an oversized attachment must be refused before decoding.
  • Runtime inventory per host. Not per repository — per host, including the CMS and the API subdomain. The interpreter that parses every request is the component your application code cannot protect.

What the defender should take away

  • Review the estate, not the website. The CMS host, the API host and the staging twin are part of the perimeter even when they are not linked in the footer.
  • Put frame-ancestors on every login page. It costs one header and removes an entire phishing genre.
  • Give staging the same network control as production, or take it offline. A basic-auth prompt on the pretty host and an open door on the data host is the worst combination.
  • Enforce visibility in the query. A boolean that only the front end respects is documentation, not authorisation.
  • Validate on the server, and prove it. Return 422, cap the body before decoding, sniff the decoded file, drop the base64-in-JSON transport.
  • Publish DMARC and sign your mail. SPF alone tells receivers which servers may send; DMARC tells them what to do when the visible sender lies.
  • Treat the staff directory as a targeting list. Publishing direct dials is a sales decision; publishing them for a domain that can be spoofed is a risk decision.

Closing

The website was not the problem. The website was the part everyone had already fixed — no trackers, no cookies, no third-party scripts, a clean header set and a valid certificate.

The problem was the application behind it: a login page that could be framed, a staging twin that could be browsed, an API that published what the interface hid, forms that trusted the browser, and a mail domain that could be forged with no policy to object. None of that is exotic. All of it is routine, and all of it is enough to build a convincing message addressed to a named person.

The front end was clean. The perimeter was not.

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

Send forged invoices or payment-change requests from the real domain to a named procurement contact, using the direct dial found in the public API.
Phish the admin login inside a frame on the real origin: same URL, same certificate, same design - the victim types the credentials into the real form.
Use the unprotected staging twin as a rehearsal: reused passwords, test accounts, older code and nobody watching the logs.
Deliver malware as a fake job application: attachments are base64 JSON, validated only in the browser, and the endpoint is open to cross-origin posts.
Read records the interface hides: every boolean such as showInContacts is a UI concept until the server query enforces it.
Harvest the public org chart (names, roles, direct dials, mobiles) as targeting data for waves of credible, personalised phishing.

All identifiers removed — sector detail, country, scale, hostnames, versions and dates are deliberately generic, and no figures from the engagement appear here. No client systems were accessed beyond public read requests, no credentials were used, no file was uploaded and no exploitation was performed.

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