~/blog/woocommerce-store-api-exposurePOSTED
by Shady Nathan Tawfik · September 19, 2026 · 5 min

Your Online Shop Exposes A Public API You Did Not Write

A lifestyle and commerce site on a current content platform with a store attached. Seventeen findings, and the store interface was the one nobody audited because it looked like a feature.

Interactive[blog-2.0]

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.

TL;DR: A lifestyle and commerce site on a current content platform with a store attached. Seventeen findings, and the store interface was the one nobody audited because it looked like a feature.

Findings

SeverityFindingEvidence
HOCHSuper-admin accounts identified in the public user listingRead interface flags administrative accounts
HOCHStore read interface exposes catalogue and cart identifiersProduct, stock and cart keys readable without a session
HOCHNo security headers including on cart and checkout pathsProtection absent on the highest-value routes
MEDIUMSubresource integrity removed by the script delay loaderIntegrity configured and simultaneously stripped
MEDIUMRemote procedure call endpoint open on the originLogin amplification and internal port scanning available
MEDIUMExternal scripts loaded without integritySix third-party origins with no verification

The attack path

Content platformCommerce plugin installedStore read interfaceCatalogue, stock and cart keysNo authorisation on the read pathPerformance pluginStrips integrity attributesThird-party scripts unverifiable

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

~/code/bash bash
# 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

~/code/bash bash
$ 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

~/code/nginx nginx
# 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

~/code/ts ts
// 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:

Super-admin accounts identified in the public user listing
Store read interface exposes catalogue and cart identifiers
No security headers including on cart and checkout paths
Subresource integrity removed by the script delay loader
Remote procedure call endpoint open on the origin
External scripts loaded without integrity

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.

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