The most common finding in our WordPress security reviews is not an outdated plugin. It is an unauthenticated introspection endpoint pointed at a content model holding unpublished work, user metadata and, occasionally, pricing logic that was never meant to leave the building.
GraphQL is not a security boundary
A typed API makes a good shape for the front end and a poor shape for the trust model. REST hides its surface behind endpoints that are tedious to enumerate; GraphQL hands you a schema that a client can introspect in one request, and that schema includes every field the plugin can reach, not just the fields you meant to expose.
Two things follow. First, an attacker can discover your entire content model by asking. Second, if that model contains an expensive field, they can ask for it a thousand times and your origin pays for every one.
Four controls we apply to every build
- Disable introspection in production. Not because it is unbreakable, but because it removes the free reconnaissance step. Enable it in staging, where your own developers are the only clients.
- Cap query depth and complexity. A depth limit of 7 and a complexity budget scored per query stops the classic nested-relationship amplification attack without affecting real editorial queries.
- Restrict the schema. Use capability-aware field registration so an anonymous request cannot resolve a field gated to
edit_posts, even if it knows the name. - Rate limit at the edge, keyed on IP, before the request reaches WordPress. The origin should never see the traffic it cannot serve.
None of this is exotic. It is four lines of configuration and one middleware, and it changes the shape of the problem from "can they reach WordPress" to "can they make it expensive".
Publishing a content API is not the same as publishing your content model. The gap between those two things is where the incidents live.
Preview is the exception that proves the rule
Draft preview needs an authenticated path, which means you need a token that authenticates a server-side query without being a cookie that can be replayed. We mint a short-lived, route-scoped token at the editor's request and verify it against the CMS on use.
That token is the most sensitive credential in the system, and it is the one most often implemented as "just use a Basic auth header in a client component". If you are about to do that, stop: a bearer token in client-side JavaScript is a bearer token in your browser history.
Tagged
- WPGraphQL
- Security
- API
- WordPress