Follow along at!

Viewing Notes

How to see this presentation

Some hotkeys:

  • Arrow keys to navigate (slide on touch devices)
  • H = highlight any code snippets
  • P = toggle speaker notes (if any)
  • F = fullscreen viewing
  • W = toggle widescreen
  • O = see an overview
  • ESC = toggles off these goodies


  • Motivation
  • XSS and SQLi
    • Validating & Escaping Inputs
    • wp_kses
    • Safer DB Queries
  • CSRF
    • Nonces
  • Odds and Ends


  • WordPress is a big target
    • 60.4% of all sites that run a CMS
    • ~23.8% of all sites
  • As part of the ecosystem, plugins have a responsibility to be secure
  • As plugin authors, it is our job to make sure we write secure plugins

Motivation: Some Headlines

motivation1 motivation2 motivation3 motivation4

Motivation: Quotes from the Peanut Gallery

By itself the headline could be used to report on WordPress every day. —ArsTechnica commenter

Wow, who the f***(redacted) writes code like that? There’s no HTML-encoding and it’s echoing a huge concatenation of strings. No wonder this plugin is swiss cheese. —ArsTechnica commenter

Sometimes I wonder is it not easier just to roll my own code and create my own website rather than to depend on WordPress… —ArsTechnica commenter

Some Insight into the Problem

  • WordPress is written in PHP
  • PHP is dynamically/weakly typed
    • Variables are can “on the fly” convert from one type to another (e.g. integer to a string, or vice versa)
  • Vast majority of what we work with are strings
    • DB queries
    • HTML output
    • Form input

XSS and SQLi

Example: XSS

<a href="<?php echo $_SERVER['REQUEST_URI'];?>"><?php echo $title;?></a>

User requests:"><script>alert('Porkchop Sandwiches!');</script>



Preventing XSS and SQLi with Input Validation and Sanitization

  • Cross-site Scripting (XSS)
    • Attack that injects malicious scripts into the victim site
  • SQL Injection (SQLi)
    • Attack that injects malicious query via input data from a client
  • Input Validation
    • Ensure input data is of expected type, within expected value range
  • Input Sanitizing (Output Escaping)
    • Ensures input (output) string contains only valid characters for the given context

Input Validation and Sanitization

  • Input Validation
    • Validate early (before using an input)
    • intval(), absint(), isset()
  • Input Sanitization
    • May remove parts of the input/change meaning
    • sanitize_email(), sanitize_text_field(), sanitize_file_name(), and more…
    • wp_kses()
    • Can also use the esc_* functions

Example: Input Validation

<input id="my-age" maxlength="3" name="my-age" type="text" />


$age = $_REQUEST['my-age'];
update_user_meta( $user_id, 'age', $age );


$age = absint( $_REQUEST['my-age'] ); //Age should not be negative
if( 0 < $age && 200 > $age) { //People typically don't live to 200
	update_user_meta( $user_id, 'age', $age );

Example: Input Sanitization

<input id="job-title" type="text" name="job-title" />


$job-title = $_REQUEST['job-title'];
update_user_meta( $user_id, 'job-title', $job-title );


$job-title = sanitize_text_field( $_REQUEST['job-title'] );
update_user_meta( $user_id, 'job-title', $job-title );

A Few Words on wp_kses()

wp_kses( $string, $allowed_html, $allowed_protocols )
  • $string—The string to strip of bad tags
  • $allowed_html—An array of allowed HTML elements (tags and properties)
  • $allowed_protocols—An array of allowed protocols (e.g. http, https, ftp…)
  • Returns the input $string containing only the allowed HTML tags
  • wp_kses_allowed_html($context)—Returns predefined array of allowed tags depending on context (typically ‘post’)

Preventing SQLi

  • For most cases, use WordPress API calls when interacting with the database
    • get_post(), update_post(), get_metadata(), update_metadata(), etc…
  • If you have to directly access the DB, use $wpdb
    • $wpdb::insert(), $wpdb::update(), $wpdb::delete(), $wpdb::replace()
    • If you find yourself writing SQL statements, stop
      • Make sure you prepare them with $wpdb::prepare()
      • $wpdb::query() or any of the $wpdb::get_*() functions (e.g. $wpdb::get_results()) need their SQL statements prepared

Output Escaping

  • Can’t necessarily trust what we get from external sources
    • database
    • user input
  • Should escape as late as possible
  • While many WordPress functions will escape for you, this is not always the case
    • add_query_arg() and remove_query_arg() do not escape their output!
  • Will not change the meaning of the escaped string
  • Examples: esc_html(), esc_attr(), esc_url(), esc_js(), and more…

Example: Output Escaping


<a href="<?php echo add_query_arg( 'foo', 'bar' ); ?>"><?php echo $title;?></a>


<a href="<?php echo esc_url( add_query_arg( 'foo', 'bar' ) ); ?>">
	<?php echo esc_html( $title ); ?>


Example: CSRF

  • Example URL:
    • Using the current logged in user (say Bob), this would delete Alice’s account
  • Attacker (Chuck) could trick Bob into deleting Alice’s account using:
<img src=""
	width="0" height="0" border="0" />
  • Placed in:
    • an email directly to Bob
    • a site Bob frequents
  • If Bob is still logged in at and views the “image”, he will inadvertantly delete Alice’s account

Preventing CSRF with Nonces

  • Cross-Site Request Forgery (CSRF)
    • Method of tricking privileged users into performing potentially malicious actions
  • Nonce: A number used only “once”
    • Used to mitigate CSRF attacks

Creating a Nonce

wp_nonce_url( $actionurl, $action, $name )
  • $actionurl — The URL to protect
  • $action — The action name, be descriptive (defaults to -1)
  • $name — The nonce name (defaults to _wpnonce)
  • Returns the $actionurl with a nonce appended to the end and run through esc_html()
  • Useful for protecting urls that perform an action (e.g. an undo link in a settings page)

Creating a Nonce Continued

wp_nonce_field( $action, $name, $referer, $echo )
  • $action — The action name, be descriptive (defaults to -1)
  • $name — The name of the hidden nonce input field that will be created (defaults to _wpnonce)
  • $referer — Whether or not to create the companion hidden referer input field (defaults to true)
  • $echo — Whether or not to echo the nonce form field (defaults to true)
  • Echos or returns a hidden form input field with a nonce
  • Useful for protecting forms (e.g. save settings button)
wp_create_nonce( $action )
  • Returns a string containing the nonce

Checking a Nonce

wp_verify_nonce( $nonce, $action )
  • $nonce — The nonce value to check
  • $action — The action name used when the nonce was created (defaults to -1)
  • Useful for checking nonces on the frontend or in the administration screen
check_admin_referer( $action, $query_arg )
  • $action — The action name used when creating the nonce (defaults to -1)
  • $query_arg — The name of HTTP query argument to check for the nonce value (defaults to _wpnonce)
  • Useful for checking nonces in the administration screen

Example: Nonces

Create Nonce

wp_nonce_field( 'my_mm_license_activate', 'my_mm_license_activate_nonce' );

Checking Nonce

$nonce_field = 'my_mm_license_activate_nonce';
if( !check_admin_referer( 'my_mm_license_activate', $nonce_field ) )

Some Final Things to Note

Common Mistakes

  • is_admin() does not check if the user is an admin
    • This returns true if in the dashboard (/wp-admin)
    • Instead, use current_user_can('capability_name')
  • Do not trust the contents of $_SERVER
    • While Chrome and Firefox now URL encode what they send, don’t trust it

PHP Features to Avoid

  • PHP supports several ways to execute arbitrary code:
  • Do not use any of these


  • Don’t trust external inputs (users, database, etc.)
  • Validate early
  • Escape late



John Havlik (@mtekkmonkey)

Find these slides at