TL;DR: A manufacturer with a clean-looking web presence was publishing far more than it intended. Public certificate logs named 68 customer brands across 194 tenant hostnames, 40 of which still resolved. The primary domain had no DMARC policy at all, so its name could be used in forged supplier mail. The WordPress REST API reflected any origin with credentials, the staff directory was readable without a login, two customer demo hosts had no usable TLS trust, and the product portal's config exposed 40+ endpoints plus a public Power BI report with live service KPIs. The lab then showed which POST surfaces were actually dangerous — and which only looked that way.
The scenario
A German machine-software manufacturer asked me to look at its public web estate. Not one website: a corporate WordPress site, a product portal on a separate hub domain, a service-hub family, customer demo tenants under the main domain, a developer estate, and a public Confluence. The brief was simple: find what an attacker sees before they touch a login.
That last part matters. Most audits start at the front door — login pages, forms, application logic. This one started at the public register: certificate transparency. Everything below was collected without authentication, without exploitation, and without sending a single request to the customer tenants.
I removed all identifying information. The target is described by sector and technology class. Customer names, employee names, hostnames and credentials do not appear in this article.
The pattern
Certificate transparency is a customer list
Every TLS certificate is logged publicly. That is the point of CT: you can verify which certificates exist for a domain. The side effect is that hostnames become a public inventory — and in a multi-tenant business, hostnames are customer names.
The estate contained 340 CT entries for the main domain. Grouping them by tenant family produced:
- 194 tenant hostnames
- 68 distinct customer brands
- 40 hosts that still resolve today
- production, demo and QA families that are trivially distinguishable by name
The same logs also named the delivery toolchain — GitLab, Harbor, Nexus, SonarQube, ReportPortal, Testrail — and the environments around them. A competitor, a fraudster or a supplier can read this without touching the company. There is no alert for reading a public register.
A hidden login does not hide the staff
The WordPress login was hidden well: wp-login.php and /wp-admin/ redirected to a 404 page, XML-RPC returned 403, uploads were not listable. That is better than most.
Then I asked the REST API for the user list. It returned 12 accounts with names, and the administrator's profile description contained a real name. Two more paths — oEmbed and the ?author=1 redirect — returned the same directory. The login was hidden; the workforce was not.
CORS with credentials turns the API into a cross-site surface
The REST API reflected any Origin back in Access-Control-Allow-Origin while sending Access-Control-Allow-Credentials: true. It did the same for Origin: null, which covers sandboxed iframes and local files, and it was reachable both via /wp-json/ and the ?rest_route= alias.
WordPress nonces limit the practical blast radius, so I did not rate this as remote code execution. But the configuration is wrong in a way that multiplies every other problem: any website a logged-in employee visits can issue credentialed requests to the company's own API.
Mail trust was split, and one half was missing
The primary domain published no DMARC record — NXDOMAIN on two public resolvers. The secondary domain published p=none, reported to a third party, and its SPF authorised a shared agency host plus two server IPs. For a supplier into industrial customers, that is the realistic money path: a forged invoice or bank-detail change from the real domain, with no policy telling the recipient to distrust it.
TLS trust broke exactly where customers look
Two customer-facing demo hosts had no usable TLS:
- one presented a self-signed CA from 2007 using SHA-1, with a 20-year leaf certificate;
- the other served a certificate for an unrelated external domain, producing
ERR_CERT_COMMON_NAME_INVALID; - both answered on plain HTTP with
nginx/1.28.3andPHP/8.5.5in the headers.
Browsers show a full-page warning. Some users click through. Others fall back to HTTP. Both behaviours are trained on pages that look like the product.
The product portal published its own map
The portal's CSP allowed 'unsafe-eval', trusted whole wildcard subdomain families, loaded a jsDelivr path without SRI and set child-src *. The policy also named 42 customer and partner domains — a second customer list, this time in a response header.
Its public config.js added 40+ internal and customer-facing endpoints, feature flags for unreleased products, an Azure Function lookup, a Novu app id and a publish-to-web Power BI URL. Rendered in a browser, without login, the report showed service-case volume, reaction and resolution times, failure priorities and the connected/unconnected fleet split.
The least glamorous findings were the useful ones
A public Confluence sandbox held 142 pages, including a work log with 165 revisions, internal migration to-dos and draft links. The WordPress media API exposed 538 items and 32 documents, including an unattached 2024 participant list as an XLSX. A hosting panel (Froxlor) answered on a public host, and three login apps (tpm, smtpgui, transfer.it) set session cookies without Secure, HttpOnly or SameSite — one of them also sent an invalid X-Frame-Options: ALLOW-FROM and a malformed Referrer-Policy.
None of that is an exploit. All of it is material for one.
Findings
The full set: 24 findings — 2 high, 14 medium, 6 low, 2 info. No exploitable 2026 CVE matched the observed versions.
| Severity | Finding | Why it matters |
|---|---|---|
| HIGH | Customer list in certificate transparency — 68 brands, 194 tenant hosts, 40 resolving, plus the delivery map | Commercial intelligence and targeting material without a single request to the target |
| HIGH | No DMARC on the primary domain — .de only p=none, SPF authorises a shared agency host | Forged invoices and bank-detail changes stay deliverable under the real name |
| MEDIUM | CORS with credentials on /wp-json/ and ?rest_route= — any origin, including null | Any visited site can issue credentialed API requests from a logged-in browser |
| MEDIUM | Staff enumeration — 12 accounts via REST, oEmbed and ?author=1, admin real name in the description | Spear phishing and credential stuffing start with a real name |
| MEDIUM | Customer demo hosts with broken TLS — 2007 SHA-1 self-signed CA, 20-year leaf; foreign certificate; HTTP fallback | Browser warnings at the exact place customers expect a product |
| MEDIUM | Portal CSP — unsafe-eval, wildcard subdomains, jsDelivr without SRI, child-src *, no frame-ancestors | A script injection anywhere in the portal runs at full effect |
| MEDIUM | 42 customer/partner domains in the CSP | A second customer list in a public response header |
| MEDIUM | Public config.js — 40+ endpoints, feature flags, Azure lookup, Novu id | The full product and integration map, cacheable for a year |
| MEDIUM | Public Power BI report — service-case KPIs, priorities, fleet split | Operational performance data visible to competitors and customers |
| MEDIUM | Grafana 13.1.0 public — /metrics 768 KB without auth, /login without XFO/CSP, patch lag | Metrics reveal rhythms; the version selects the exploit |
| MEDIUM | WordPress hardening gaps — no HSTS, no CSP, no Permissions-Policy; version leaks; 516 REST routes | No second line of defence, and the version map is free |
| MEDIUM | Confluence sandbox public — 142 pages, work log, migration to-dos | Internal process and release cadence become readable |
| MEDIUM | Participant list XLSX via the open media API | Personal data for targeted social engineering |
| MEDIUM | Froxlor hosting panel public + three login apps with missing cookie flags | Credential stuffing target; one hit means hosting, mail or files |
| MEDIUM | Jitsi build 8206 (2022/23) vs 11248 (2026), no token auth | An old edge on a meeting service is a perfect phishing pretext |
| MEDIUM | SonarQube 26.1.0 vs 26.10.0 | Nine monthly releases behind, version public |
| LOW | Dev/infra metadata — Nexus 3.96.3-01, registry proxy catalogue, Harbor systeminfo | Exact versions for exploit selection; no private repositories exposed |
| LOW | Hygiene bundle — no security.txt, no CAA, HSTS preload claim not listed, Fluent Forms one patch behind | Small gaps that compound |
| LOW | Dev storage in production — privacy policy and BIMI logo on a dev account; dev host in the CSP | If the dev account is retired or compromised, production legal assets change |
| LOW | Synapse 1.161/1.162 vs 1.163.0, public admin version endpoint | Version leak plus an admin endpoint without auth |
| LOW | Auth0 legacy grants — implicit and password enabled; login page itself hardened | Larger attack surface; the login page is a genuine plus |
| LOW | Azure lookup function — constant organisation id for every address | No user enumeration observed; write path untested |
| INFO | Detection vacuum — no WAF/rate limit across 171 requests; later a silent TCP blackhole | Detection is network-level and silent, not application-level |
| INFO | Public demo apps and a test page — no data, extra surface | Hygiene and attack surface |
The lab: which POST surfaces were actually open
This is where the audit changed shape. The live WordPress origin started silently dropping my egress after roughly 200 read-only requests — no 429, no 403, just TCP timeouts while an independent fetch from another network returned 200. That is a detection control, and it meant the POST surfaces could not be tested against production.
So I rebuilt the stack in an isolated lab: WordPress 7.1.3, Novamira 1.9.2, SEOPress 10.3, Fluent Forms 6.2.16, MCP Adapter 0.7.1. Then I tested every endpoint that looked dangerous.
| Surface | Lab result | Verdict |
|---|---|---|
Novamira POST /upload | 401 missing token; 401 forged signature; 200 only with a valid HMAC token | Token-gated — no anonymous upload |
Novamira POST /admin-access | 401 without one-time token and nonce | No anonymous admin access |
SEOPress MCP POST /register | 201 with client id and secret | Open dynamic client registration, by design |
SEOPress MCP POST /token | 401 without a registered client | No anonymous token |
| SEOPress MCP authorize | PKCE S256 required; explicit consent screen showing client, redirect URI and account | No auto-approve; residual consent-phishing risk |
| Fluent Forms submit | 200 and an entry created without a nonce | Spam surface — no upload fields on the target form |
| MCP Adapter / Abilities | 401 | Correctly closed |
| Power BI | single-page report; no hidden pages | The one page is the exposure |
| Azure lookup | constant organisation id for every address | Not user enumeration |
The lesson is not "the plugins are unsafe". It is that a public route is not the same thing as an open route. Two of the scariest-looking endpoints were token- and consent-gated; one boring form endpoint accepted anonymous writes. You cannot tell which is which from the route list.
What I got wrong
Three claims did not survive verification, and I removed them:
- A certificate "mismatch" on the whiteboard host. The CN differed, but the SAN included the host. Browser and curl accepted it. CN is not the check; SAN is.
- A JWT in the portal config. It was the public jwt.io demo token, not a credential.
- A public Docker registry catalogue. It was a pull-through cache of public upstream images — no private repositories.
- The Azure lookup function. It returned the same organisation id for every address, so it was not user enumeration. The write path remained untested.
Four false positives removed before disclosure. That is the job: the finding you do not publish is as important as the one you do.
What the defender should take away
- Treat CT as a publication channel. Customer names in hostnames are convenient and permanent. Opaque tenant identifiers or wildcard certificates remove the list; monitoring CT tells you what you have already published.
- Publish DMARC and enforce it. A primary domain with no policy is a spoofing surface with your name on it.
- CORS is a policy, not a default. Reflect only the origins you own, never with credentials, and test both route shapes.
- A hidden login is not a hidden directory. Lock the users endpoint, oEmbed and author redirects, or accept that the staff list is public.
- Dashboards are data. A publish-to-web link in a config is a data release; render it before you ship it.
- Pin what you load.
unsafe-eval, wildcard subdomains and unversioned CDN paths turn every other bug into a bigger one. - Test the POST surfaces in a lab. A route list tells you what exists, not what is protected.
Closing
The most valuable data in this audit was not behind a login. It was in a public register, a public config and a public dashboard — three places nobody thinks of as "the perimeter".
The login page was fine. The register was not.
Once an attacker gains access, they can perform the following actions:
All identifiers removed. No customer systems were accessed, no credentials were used, and no exploitation was performed. The lab mirror is isolated and synthetic.