Security Hardening

WordPress Intermediate/Advanced

Chapter 8 · Security Hardening

WordPress Fundamentals 8 described roles as "named bundles of capabilities" and promised the real mechanism would come later, in real code. This chapter is that payoff — plus three more mechanisms giving WordPress-specific teeth to security principles this site has already taught in general: request forgery protection, output escaping, and safe database queries.

Capability Checks — current_user_can()

<?php if ( current_user_can( 'publish_posts' ) ) { // show the publish button } if ( current_user_can( 'manage_options' ) ) { // show admin-only settings } ?>

This is the exact function WordPress Fundamentals 8 promised: real code checking a specific capability, not a role name. "Editor" was never checked directly anywhere in WordPress core — every permission decision comes down to a call exactly like this one.

Hiding a button is not the same as securing an action
Hiding an "Edit" link from the dashboard for a user without the right capability is a usability nicety, not a security boundary. The actual privileged action itself — the code that would edit or delete something — must independently check current_user_can() before doing anything, regardless of what was or wasn't shown in the interface. A user who reaches the underlying action URL directly, bypassing the hidden button entirely, must still be blocked by the check itself.

Nonces — WordPress's Own CSRF Protection

A nonce ("number used once") is a token WordPress generates and verifies to confirm a request genuinely originated from the expected page and user — this is WordPress's own concrete implementation of exactly the defense CSRF — Cross-Site Request Forgery covers in general.

<!-- generating, inside a form --> <?php wp_nonce_field( 'my_action_name', 'my_nonce_field' ); ?>
<?php // verifying, on submission if ( ! wp_verify_nonce( $_POST['my_nonce_field'], 'my_action_name' ) ) { wp_die( 'Security check failed.' ); } ?>
Combine both checks together
A genuinely secure privileged action checks both the nonce (proving the request came from the right place) and current_user_can() (proving the requester is actually allowed to do this) — neither one alone is a complete defense.

Output Escaping — WordPress's Own Answer to XSS

esc_html(), esc_attr(), and esc_url() are concrete, CMS-specific instances of exactly the context-dependent output encoding XSS — Cross Site Scripting already covers in general — different contexts need different escaping.

<?php echo '<h1>' . esc_html( $title ) . '</h1>'; echo '<a href="' . esc_url( $link ) . '">Click here</a>'; echo '<input value="' . esc_attr( $value ) . '">'; ?>
FunctionContext
esc_html()Plain text inside an HTML element's body
esc_attr()Text placed inside an HTML attribute
esc_url()A URL, most often inside an href or src attribute
The wrong escaping function is still a real vulnerability
Using esc_html() on data being placed inside an attribute (rather than esc_attr()) doesn't provide the correct protection for that specific context — matching exactly the "encode for the context, not generically" lesson XSS — Cross Site Scripting already taught in the abstract.

$wpdb->prepare() — WordPress's Own Parameterized Queries

$wpdb->prepare() is WordPress's own direct implementation of the parameterized-query defense SQL Injections and Defences covers in general — never concatenate user input directly into a SQL string, in WordPress exactly as anywhere else.

<?php global $wpdb; $results = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}posts WHERE post_author = %d", $user_id ) ); ?>

%d, %s, and %f are placeholders for integers, strings, and floats respectively — $wpdb safely substitutes the actual value in, exactly the same underlying protection as any other language's parameterized query API.

One Pattern, Four Mechanisms

The synthesis this chapter has been building toward
Every mechanism in this chapter is the same underlying move: a general security principle already taught elsewhere on this site, given a specific, concrete WordPress implementation. Capability checks are least-privilege access control, per Database Security's own material. Nonces are CSRF protection. Output escaping is context-aware XSS defense. $wpdb->prepare() is SQL injection prevention. None of these are WordPress inventing new security ideas — they're WordPress giving already-general principles a real, callable API, echoing OWASP Top 10's own framing and directly connecting back to the hardening discipline Setting Up a Web Server on Debian applied at the server layer.

Hands-On Exercises

Exercise 1

A developer hides a "Delete Post" button from users without the right role, but the underlying delete action itself never calls current_user_can(). Explain the real, exploitable gap this leaves.

📄 View solution
Exercise 2

A developer uses esc_html() to output a URL inside an href attribute instead of esc_url(). Explain why this is still a real vulnerability, connecting your answer to this chapter's own context-dependent escaping material.

📄 View solution
Exercise 3

Explain why this chapter describes $wpdb->prepare() as "WordPress giving an already-general principle a real, callable API" rather than a uniquely WordPress security concept.

📄 View solution

Chapter 8 Quick Reference

  • current_user_can() — the real mechanism behind roles, delivering on WordPress Fundamentals 8's own promise; hiding UI is not a substitute for checking it directly in the privileged action
  • Nonces (wp_nonce_field(), wp_verify_nonce()) — WordPress's own CSRF protection; combine with capability checks, never rely on either alone
  • esc_html()/esc_attr()/esc_url() — context-dependent output escaping, WordPress's own answer to XSS
  • $wpdb->prepare() — WordPress's own parameterized queries, WordPress's own answer to SQL injection
  • All four are WordPress-specific implementations of general principles already taught in this site's own security courses, not new inventions
  • Next chapter: Performance & Caching