TL;DR: Attaching a store installs a second application with its own public read interface. I found super-admin flags in the user listing, cart identifiers readable without a session, and a script loader that strips integrity attributes.
The scenario
I ran this as a black-box review: no credentials, only the publicly visible surface. Here is what I saw before I touched anything.
A media and lifestyle platform with a monetised catalogue, an events calendar, multiple forms and an external store integration, on a current content platform with a full commerce plugin. Assessment covered the public content surface, the store interface, mail authentication, header policy, third-party scripts and user enumeration.
I removed all identifying information. The target is described only by sector and technology class so the pattern can be reused.
The pattern
The pattern I recognised — and that I keep finding in similar estates:
Attaching a store to a content site installs a second application. It has its own read interface, its own data model, its own session handling and its own idea of what a customer is, and it is added by a plugin whose job is to sell, not to be audited. The store interface in particular is designed to be public: it serves catalogue data to the front end without authentication, because that is what a shop frontend needs.
That design is defensible for catalogue data and indefensible for everything around it. The read interface exposes products, stock state, prices and, depending on configuration, the identifiers that connect a cart to a customer and an order to an address. It answers before the plugin checks whether the caller should see any of it. The result reads as a normal API and is missed by reviews that focus on the content platform.
The second structural issue is the optimizer. Performance plugins delay script loading and, in doing so, strip the integrity attributes from the scripts they delay. This is a genuine trade-off between caching and verification, and it is invisible in a plugin inventory: the site has subresource integrity configured, and it has none. When the recommendation is to add integrity attributes, the fix is not to add attributes; the fix is to replace the loader.
Findings
| Severity | Finding | Evidence |
|---|---|---|
| HOCH | Super-admin accounts identified in the public user listing | Read interface flags administrative accounts |
| HOCH | Store read interface exposes catalogue and cart identifiers | Product, stock and cart keys readable without a session |
| HOCH | No security headers including on cart and checkout paths | Protection absent on the highest-value routes |
| MEDIUM | Subresource integrity removed by the script delay loader | Integrity configured and simultaneously stripped |
| MEDIUM | Remote procedure call endpoint open on the origin | Login amplification and internal port scanning available |
| MEDIUM | External scripts loaded without integrity | Six third-party origins with no verification |
The attack path
Reproduction in my lab
Every command below targets a lab container I control. Nothing here is aimed at a live system.
Read the store interface on your own shop, unauthenticated
# target: your own shop on localhost
# 1. does the read interface answer without a session
$ curl -s 'http://localhost:8080/wp-json/wc/store/products?per_page=2' | python3 -m json.tool | head -40
# 2. what does the public user listing expose about roles
$ curl -s 'http://localhost:8080/wp-json/wp/v2/users' \
| python3 -c 'import json,sys
for u in json.load(sys.stdin):
print(u["id"], u["slug"], u.get("meta", {}))'
# 3. are the headers present on the highest-value routes
$ for p in / /cart/ /checkout/ /product/; do
printf '%-12s %s\n' "$p" \
"$(curl -sI "http://localhost:8080$p" | grep -ciE 'content-security-policy|x-frame-options|x-content-type-options')/4"
done
Detection in your own estate
Check whether your own loader removes integrity attributes
$ for f in $(curl -s https://example.org/ | grep -oE '/assets/[A-Za-z0-9._-]+\.js' | sort -u); do
curl -s "https://example.org$f"
done > /tmp/bundle.txt
# a loader that strips verification is looking like this
$ grep -oE 'removeAttribute\("integrity"\)|removeAttribute\(\x27integrity\x27\)' /tmp/bundle.txt | head
$ grep -c 'integrity' /tmp/bundle.txt
# and how many external scripts lack the attribute in the served HTML
$ curl -s https://example.org/ | grep -oE '<script[^>]*src="https?://[^"]*"[^>]*>' \
| grep -vc integrity
Remediation
Protect the highest-value routes explicitly
# the cart and checkout deserve their own rule, not the site default
location ~* ^/(cart|checkout|my-account)/ {
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "no-referrer" always;
add_header Content-Security-Policy "default-src 'self'; frame-ancestors 'none'; script-src 'self' https://cdn.example.com; object-src 'none'" always;
proxy_pass http://backend;
}
If you need deferred loading, do not let it remove verification
// the correctness fix is to keep the attribute and defer honestly
// build-time integrity, hashed per release
import { createHash } from 'node:crypto';
import { readFileSync } from 'node:fs';
const SRI_ALGO = 'sha384';
export function sriFor(file) {
const hash = createHash(SRI_ALGO).update(readFileSync(file)).digest('base64');
return `${SRI_ALGO}-${hash}`;
}
// never strip it at runtime: an attacker who controls the response can also
// rewrite the loader that was supposed to verify it
document.querySelectorAll('script[defer-src]').forEach((el) => {
el.setAttribute('src', el.getAttribute('defer-src'));
el.removeAttribute('defer-src');
});
Once an attacker gains access, they can perform the following actions:
Takeaways
- Attaching a store installs a second application. Audit it as one.
- A shop read interface is public by design; check what else it exposes.
- A performance plugin can make integrity structurally impossible. Replace the loader.
- Protect cart and checkout explicitly; the site default is not enough.