- WordPress REST API: Flexible, but Not Secure by Default
- Three Half-Baked Security Practices I’ve Encountered (and Avoided)
- 1. Using a nonce in HTML on a fully cached page
- 2. Storing user IP addresses in transients for rate limiting
- 3. Writing to the database without any validation
- My Approach: Keep It Simple, Clear, and Intentional
- 1. Public, read-only endpoints
- 2. Endpoints that write data
- Real-World Example: Init View Count
- Should You Worry About View Spam?
- Conclusion
WordPress REST API: Flexible, but Not Secure by Default
Since version 4.7, WordPress has included a built-in REST API. Plugins and themes can easily register custom routes that return JSON or even write data to the system. This is ideal for modern use cases like SPAs or headless apps, but here’s the catch: WordPress doesn’t secure your routes for you.
Every custom endpoint you create will behave exactly how you code it — whether it’s open to spammy POST requests, accessible to external scripts, or returning sensitive content. Without proper validation, your API might be quietly exposing your site to long-term damage.
Three Half-Baked Security Practices I’ve Encountered (and Avoided)
1. Using a nonce in HTML on a fully cached page
This is the classic mistake. A developer generates a nonce with wp_create_nonce('wp_rest'), injects it into frontend JS using wp_localize_script(), and assumes everything is secure. But if the page is cached (via WP Rocket, Cloudflare “cache everything”, or similar), that HTML — including the nonce — is frozen in time.
When the nonce expires (usually after 12 hours), every frontend request using that stale value fails authentication. The feature stops working. No visible error. No user feedback. I’ve personally debugged plugins that broke three days after launch — simply because the nonce was cached in static HTML.
2. Storing user IP addresses in transients for rate limiting
Another common tactic: “Let’s store the IP in a transient to throttle repeated requests.” Sounds smart, until you scale. A site with one million daily views could generate one million transients — all inside the wp_options table. What should be a lightweight config store turns into a data swamp.
Even worse, delete_expired_transients() doesn’t always run reliably (especially if wp_cron is slow or disabled). And some hosting providers block bulk deletions. WP DB Cleaner can’t save you once things go too far.
3. Writing to the database without any validation
Some plugins register routes like this:
register_rest_route('plugin/v1', '/count', [
'methods' => 'POST',
'callback' => 'plugin_count',
'permission_callback' => '__return_true',
]);
Then immediately write to the database without checking if the post_id exists, if the post is published, or if the post type is valid. The data might look correct — but it was accepted without any real control.
My Approach: Keep It Simple, Clear, and Intentional
I split REST API routes into two categories:
1. Public, read-only endpoints
- No nonce required
- No permission checks
- Only return public data: title, ID, permalink, thumbnail
- Always provide filters so developers can override queries if needed
2. Endpoints that write data
- Never trust the client blindly
- Never log IPs, user IDs, or sensitive info
- Only accept valid
publishposts and whitelisted post types - If writing data, keep it minimal — like incrementing a view count
Real-World Example: Init View Count
In my plugin Init View Count, I register a POST route /initvico/v1/count. It’s called from the frontend after the user scrolls or waits for a certain delay. The only data it writes is a single number: the view count for that post.
I don’t use a nonce — and that’s a conscious decision. Pages might be fully cached, especially with Cloudflare or static caching plugins. Embedding a nonce in HTML is pointless if the entire page is stored and served unchanged for hours. Instead, I secure the route by:
- Only counting views for existing posts in
publishstatus - Only accepting post types defined in settings
- Providing a
init_plugin_suite_view_count_should_countfilter for developers to override behavior - Never saving any user-identifiable data
Should You Worry About View Spam?
Some developers fear that someone might write a script to spam view counts. But for public content, where nothing sensitive is written or exposed, is that really a threat? I don’t think so. If someone automates fake views, they’re boosting your traffic metrics. They’re helping your content look popular. That’s not an attack — that’s free exposure.
And if you truly need protection? Use Cloudflare. Their bot detection and rate-limiting features are designed for this exact problem. No extra code needed.
Conclusion
Security isn’t about throwing random authentication everywhere. True security means understanding how your system works, identifying what needs protection, and applying defense where it matters. A misplaced security check won’t stop hackers — but it might break your site, degrade performance, or waste weeks of debugging.
Don’t settle for half-baked security. Either do it right, or don’t do it at all. Secure your REST API with purpose, with clarity, and with minimal overhead — that’s how sustainable plugins are built.
Comments