Why It’s Important
You attempt to pull up your website but find that it’s not there anymore. Downtime is bad! Hopefully you have a backup and can restore it quickly.
You notice that you start showing up in search results for Viagra and other male enhancement drugs. This can be bad for business if your website is not specifically selling these drugs.
Your application is sending out emails to all of your members with links to download a computer virus. Nobody wants that.
Your application is hacked and the personal information of your members (their names, addresses, phone numbers, and email addresses) is exposed.
Your website is hacked and is used to infect other websites with malware. This is the quickest way to get delisted from Google search results and other important directories.
Security Basics
Update Frequently
Don’t Use the Username “admin”
Use a Strong Password
Examples of Bad Passwords
password
password123
pa55w0rd
123456789
qwerty
batman
mustang
letmein
usmarine (Brian was in the Marines)
brianmessenlehner (Brian’s first and last name)
jason&kim050507 (Jason’s name, his wife’s name, and their anniversary)
Dalya-Brian (kids’ names)
ThaiShortiMaxx (pets’ names)
IAMAWESOME! (everybody knows this, so it could be easy to guess)
Examples of Good Passwords
U$s(#8H27@!
!lik32EaTF1$h&CHIp5
#Uk@nN0tBr3akTh1s$h1t!!!
[0mG-LoL-R0Fl-T0T3$CraY]!
Hardening WordPress
Don’t Allow Admins to Edit Plugins or Themes
define
(
'DISALLOW_FILE_EDIT'
,
true
);
Change Default Database Tables Prefix
Make a database backup just in case you mess this up!
Open wp-config.php and change
$table_prefix = wp_;
to$table_prefix = anyprefix_;
.Update the existing table names in your database to include your new prefix with the following SQL commands using phpMyAdmin or any SQL client such as MySQL Workbench:
rename
table
wp_commentmeta
to
anyprefix_commentmeta
;
rename
table
wp_comments
to
anyprefix_comments
;
rename
table
wp_links
to
anyprefix_links
;
rename
table
wp_options
to
anyprefix_options
;
rename
table
wp_postmeta
to
anyprefix_postmeta
;
rename
table
wp_posts
to
anyprefix_posts
;
rename
table
wp_terms
to
anyprefix_terms
;
rename
table
wp_term_relationships
to
anyprefix_term_relationships
;
rename
table
wp_term_taxonomy
to
anyprefix_term_taxonomy
;
rename
table
wp_usermeta
to
anyprefix_usermeta
;
rename
table
wp_users
to
anyprefix_users
;
Note
update
anyprefix_options
set
option_name
=
replace
(
option_name
,
'wp_'
,
'anyprefix_'
);
update
anyprefix_usermeta
set
meta_key
=
replace
(
meta_key
,
'wp_'
,
'anyprefix_'
);
Move wp-config.php
Hide Login Error Messages
add_filter
(
'login_errors'
,
function
(
$message
)
{
return
"Invalid username or password."
;
}
);
Note
Hide Your WordPress Version
`<meta name="generator" content="WordPress 3.8.1" />`
add_filter
(
'the_generator'
,
'__return_null'
);
Note
Don’t Allow Logins via wp-login.php
Add the following rewrite rule to your .htaccess file:
RewriteRule
^
new
-
login
$
wp
-
login
.
php
Note that
/new-login/
will be the URL you can use to actually log in to wp-admin. You can change this to whatever you want.In your theme functions.php file or in a custom plugin, add this code:
function
schoolpress_wp_login_filter
(
$url
,
$path
,
$orig_scheme
)
{
$old
=
array
(
"/(wp-login\.php)/"
);
$new
=
array
(
"new-login"
);
return
preg_replace
(
$old
,
$new
,
$url
,
1
);
}
add_filter
(
'site_url'
,
'schoolpress_wp_login_filter'
,
10
,
3
);
function
schoolpress_wp_login_redirect
()
{
if
(
strpos
(
$_SERVER
[
"REQUEST_URI"
],
'new-login'
)
===
false
)
{
wp_redirect
(
site_url
()
);
exit
();
}
}
add_action
(
'login_init'
,
'schoolpress_wp_login_redirect'
);
Add Custom .htaccess Rules for Locking Down wp-admin
order
deny
,
allow
allow
from
127.0
.
0.1
#(repeat this line for multiple IP addresses)
deny
from
all
order
allow
,
deny
deny
from
127.0
.
0.1
#(repeat this line for multiple IP addresses)
allow
from
all
AuthType
Basic
AuthName
"restricted area"
AuthUserFile
/
path
/
to
/
protected
/
dir
/.
htpasswd
require
valid
-
user
SSL Certificates and HTTPS
- SSL
Stands for “Secure Sockets Layer” and is the technology that encrypts data that is transferred to and from a web page.
- HTTP
Stands for “Hypertext Transfer Protocol.” This is the standard protocol for serving web pages without encryption.
- HTTPS
Stands for “HTTP Secure.” This is the protocol for serving web pages with SSL encryption.
Installing an SSL Certificate on Your Server
Security by default. You might imagine that only your login and checkout pages really need to be secure, but what happens when your site is updated to show a login form in the sidebar? Now every page on your site needs to be secure. If your entire site is served over HTTPS, you won’t accidentally introduce an unencrypted form anywhere on the site.
Internet consumers are trained to look for that padlock (see Figure 8-2). Both savvy and nonsavvy users will feel better seeing it. Further, modern browsers will show some pretty scary warnings if parts of your site are not served over HTTPS.
Google and other search engines have started boosting sites that are served entirely over HTTPS in their search rankings.
There is no longer a CPU hit to your server when you use HTTPS. The web server stack has been updated to better handle HTTPS and frankly expect it, so you can no longer use page load times as an excuse to disable HTTPS on your site.
Using one directory for HTTPS and HTTP traffic
WordPress Login and WordPress Administrator over SSL
define
(
'FORCE_SSL_LOGIN'
,
true
);
define
(
'FORCE_SSL_ADMIN'
,
true
);
Note
Debugging HTTPS Issues
function
my_https_filter
(
$s
)
{
if
(
is_ssl
())
return
str_replace
(
"http:"
,
"https:"
,
$s
);
else
return
str_replace
(
"https:"
,
"http:"
,
$s
);
}
add_filter
(
'bloginfo_url'
,
'my_https_filter'
);
add_filter
(
'wp_list_pages'
,
'my_https_filter'
);
add_filter
(
'option_home'
,
'my_https_filter'
);
add_filter
(
'option_siteurl'
,
'my_https_filter'
);
add_filter
(
'logout_url'
,
'my_https_filter'
);
add_filter
(
'login_url'
,
'my_https_filter'
);
add_filter
(
'home_url'
,
'my_https_filter'
);
if
(
isset
(
$_SERVER
[
'HTTP_X_FORWARDED_PROTO'
])
&&
$_SERVER
[
'HTTP_X_FORWARDED_PROTO'
]
==
'https'
)
{
$_SERVER
[
'HTTPS'
]
=
'on'
;
}
Avoiding SSL Errors with the “Nuclear Option”
constant
(
'MY_SITE_DOMAIN'
,
'yoursite.com'
);
function
my_NuclearHTTPS
()
{
ob_start
(
"my_replaceURLsInBuffer"
);
}
add_action
(
"init"
,
"my_NuclearHTTPS"
);
function
my_replaceURLsInBuffer
(
$buffer
)
{
global
$besecure
;
//only swap URLs if this page is secure
if
(
is_ssl
())
{
/*
okay swap out all links like these:
* http://yoursite.com
* http://anysubdomain.yoursite.com
* http://any.number.of.sub.domains.yoursite.com
*/
$buffer
=
preg_replace
(
'/http\:\/\/([a-zA-Z0-9\.\-]*'
.
str_replace
(
'.'
,
'\.'
,
MY_SITE_DOMAIN
)
.
')/i'
,
'https://$1'
,
$buffer
);
}
return
$buffer
;
}
Note
Back Up Everything!
Note
Scan, Scan, Scan!
Useful Security Plugins
Spam-Blocking Plugins
Akismet
Bad Behavior
Backup Plugins
BackupBuddy
VaultPress
Firewall/Scanner Plugins
WordFence
All In One WP Security & Firewall
Exploit Scanner
Login and Password-Protection Plugins
Limit Login Attempts
AskApache Password Protect
Writing Secure Code
Check User Capabilities
user_can( $user, $capability )
$user
A required integer of a user ID or an object of the user.
$capability
A required string of the capability or role name.
current_user_can( $capability )
$capability
A required string of the capability or role name.
current_user_can_for_blog( $blog_id, $capability )
$user
A required integer of a blog ID.
$capability
A required string of the capability or role name.
function
schoolpress_admin_check
()
{
global
$user_ID
;
if
(
!
user_can
(
$user_ID
,
'administrator'
)
)
{
wp_redirect
(
site_url
()
);
}
}
add_action
(
'admin_init'
,
'schoolpress_admin_check'
);
Note
Custom SQL Statements
Data Validation, Sanitization, and Escaping
- Validating
The process of making sure the data received from the end user is in the correct format you expect it to be in. You want to validate data before saving it into the database.
- Sanitizing
The process of cleaning data received from the end user before saving it to the database or using it in your app.
- Escaping
The process of cleaning data you may already have before displaying it to the end user, saving it to the database, or passing it off to an API.
Note
esc_url( $url, $protocols = null, $_context = ‘display’ )
$url
A required string of the URL that needs to be cleaned.
$protocols
An optional array of whitelisted protocols. Defaults to
array( http, https, ftp, ftps, mailto, news, irc, gopher, nntp, feed, telnet, mms, rtsp, svn )
if not specifically set.$context
An optional string of how the URL is being used. Defaults to
display
, which sends the URL throughwp_kses_normalize_entities()
and replaces&
with&
and'
with'
.
esc_url_raw( $url, $protocols = null )
esc_html( $text )
$text
A required string of the text you want to escape HTML tags on.
esc_js( $text )
$text
A required string of the text you want to escape single quotes, HTML special characters ( " < > & ), and fix line endings on.
esc_attr( $text )
$text
A required string of the text you want to escape HTML attributes on.
esc_textarea( $text )
$text
A required string of the text you want to escape HTML on.
sanitize_option( $option, $value )
$option
A required string of the name of the option.
$value
A required string of the unsanitized option value you wish to sanitize.
sanitize_text_field( $str )
$str
The required string you want to sanitize.
sanitize_user( $username, $strict = false )
$username
A required string of the username to be sanitized.
$strict
An optional Boolean that, if set to
true
, will limit the username to specific characters.
sanitize_title( $title, $fallback_title = '' )
$title
A required string of the title to be sanitized.
$fallback_title
An optional string to use if the title is empty.
sanitize_email( $email )
$email
The email address to be sanitized.
sanitize_file_name( $filename )
$filename
Required string of the filename to be sanitized.
wp_kses( $string, $allowed_html, $allowed_protocols = array () )
$string
A required string that you want filtered through
kses
.$allowed_html
A required array of allowed HTML elements.
$allowed_protocols
An optional array of allowed protocols in any URLs in the string being filtered. The default allowed protocols are
http
,https
,ftp
,mailto
,news
,irc
,gopher
,nntp
,feed
,telnet
,mms
,rtsp
, andsvn
. This covers all common link protocols, except forjavascript
, which should not be allowed for untrusted users.
wp_kses_post( $data )
$data
A required string that you want filtered through
kses
.
// pretend a user added an email address "jason @ stranger$tudios.com"
$user_email
=
'jason @ stranger$tudios.com'
;
// we can check if this is a valid email
$valid_email
=
is_email
(
$user_email
);
// we know it's not because it's set to nothing from is_email()
if
(
!
$valid_email
)
echo
'invalid email<br />'
;
// let's try again with sanitizing the email
$user_email
=
'jason @ stranger$tudios.com'
;
// use sanitize_email() to try to fix any invalid email
$user_email
=
sanitize_email
(
$user_email
);
$valid_email
=
is_email
(
$user_email
);
if
(
!
$valid_email
)
echo
'invalid email<br />'
;
else
echo
'valid email: '
.
$user_email
;
Nonces
Generate a nonce string every time a page is loaded.
Add the nonce string as a hidden element on the form.
When processing a submitted form, generate the nonce the same way and check that it matches the one submitted from the form.
Generate a nonce string every time a page is loaded.
Add the nonce string as a parameter to the URL.
When processing the request, generate the nonce the same way and check that it matches the one submitted through the URL.
wp_create_nonce( $action = -1 )
$action
An optional string or int that describes what action is being taken for the nonce created. You should always set an action to be more secure:
function
schoolpress_footer_create_nonce
(){
$nonce
=
wp_create_nonce
(
'random_nonce_action'
);
$url
=
add_query_arg
(
array
(
'sp_nonce'
=>
$nonce
)
);
echo
'<p><a href="'
.
$url
.
'">Verify this Nonce</a></p>'
;
}
add_action
(
'wp_footer'
,
'schoolpress_footer_create_nonce'
);
wp_verify_nonce( $nonce, $action = -1 )
$nonce
A required string of the nonce value being used to verify.
$action
An optional string or int that should be descriptive to what is taking place and should match the action from when the nonce was created.
function
schoolpress_init_verify_nonce
(){
if
(
isset
(
$_GET
[
'sp_nonce'
]
)
&&
wp_verify_nonce
(
$_GET
[
'sp_nonce'
],
'random_nonce_action'
)
)
{
echo
'You have a valid nonce!'
;
}
else
{
echo
'You have an invalid nonce!'
;
}
}
add_action
(
'init'
,
'schoolpress_init_verify_nonce'
);
check_admin_referer( $action = -1, $query_arg = '_wpnonce’ )
$action
An optional string, but you should specify a nonce action to be verified.
$query_arg
An optional string of the query argument that has the nonce as its value.
// checking the same nonce "sp_nonce" that was created earlier
function
schoolpress_init_check_admin_referer
(){
if
(
isset
(
$_GET
[
'sp_nonce'
]
)
&&
check_admin_referer
(
'random_nonce_action'
,
'sp_nonce'
)
)
{
echo
'<p>You have a valid nonce!</p>'
;
}
else
{
echo
'<p>You have an invalid nonce!</p>'
;
}
}
add_action
(
'init'
,
'schoolpress_init_check_admin_referer'
);
wp_nonce_url( $actionurl, $action = -1 )
$actionurl
A required string of the URL to which to add a nonce action.
$action
An optional string for the action name. You should always set this.
// simple url with querystring example
function
schoolpress_footer_nonce_url
(){
$url
=
wp_nonce_url
(
add_query_arg
(
array
(
'action'
=>
'get_users'
)
),
'get_users_nonce'
);
echo
'<p><a href="'
.
esc_url
(
$url
)
.
'">Get Users</a></p>'
;
}
add_action
(
'wp_footer'
,
'schoolpress_footer_nonce_url'
);
// querystring action
function
schoolpress_footer_nonce_url_action
(){
// check if querystring action is get_users and for the nonce
if
(
isset
(
$_GET
[
'action'
]
)
&&
'get_users'
==
$_GET
[
'action'
]
&&
check_admin_referer
(
'get_users_nonce'
)
)
{
echo
'Your action: '
.
esc_html
(
$_GET
[
'action'
]
);
// or get your users and display them here...
}
}
add_action
(
'init'
,
'schoolpress_footer_nonce_url_action'
);
wp_nonce_field( $action = -1, $name = ''_wpnonce'', $referer = true , $echo = true )
$action
An optional string for the action name. You should always set this.
$name
An optional string for the nonce name. You should always set this.
$referer
An optional Boolean of whether to set the referer field for validation. The default value is
true
.$echo
An optional Boolean of whether to display or return a hidden form field. The default value is
true
.<?
php
// simple submission form example
function
schoolpress_footer_form
(){
?>
<form method="post">
<?php
// create our nonce
wp_nonce_field
(
'email_list_form'
,
'email_list_form_nonce'
);
?>
<h3>Join our email list</h3>
Email Address: <input type="text" name="email_address">
<input type="submit" name="submit_email" value="Submit" />
</form>
<?php
}
add_action
(
'wp_footer'
,
'schoolpress_footer_form'
);
// form action
function
schoolpress_footer_form_action
(){
if
(
isset
(
$_POST
[
'submit_email'
]
)
&&
isset
(
$_POST
[
'email_address'
]
)
&&
check_admin_referer
(
'email_list_form'
,
'email_list_form_nonce'
)
)
{
echo
'You submitted: '
.
esc_html
(
$_POST
[
'email_address'
]
);
// or process your form here...
}
}
add_action
(
'init'
,
'schoolpress_footer_form_action'
);
?>
check_ajax_referer( $action = -1, $query_arg = false, $die = true )
$action
An optional string of the nonce action being referenced.
$query_arg
An optional string of where to look for nonce in
$_REQUEST
.$die
An optional Boolean of whether you want the Ajax script to die if an invalid nonce is found.
1 In technical terms, “super-duper long” is equal to about 4 GB of data.
2 The wp_verify_nonce()
function will return 1
if the nonce is under 12 hours old. If the nonce is between 12 and 24 hours old, it will return 2
. If it is older than 24 hours old, it will return false
. This way you can test whether the result evaluates to true
or, to check for a slightly fresher nonce, you could check if it is equal to 1
exactly.