While WordPress core is well-tested and widely used, it allows plugins to be installed. Those plugins can be developed by, well, anyone! They enable many significant enhancements to the core platform but also have the potential to compromise the security of the entire website, even when they are not activated.
This begs the question, as a developer, how can you ensure that you do not introduce vulnerabilities into a plugin you develop?
Before we begin, it’s good to note that WordPress has official developer documentation, which provides some excellent security advice in various sections, such as here and here. In fact, much of this article is a collation of the information that is available in that documentation!
Perform WordPress Plugin Security Audits
If you’ve already published a WordPress plugin, the first step to increasing its security is performing a security audit. This will give you a decent idea of where any weaknesses may lie within the plugin in its current state, and guide your next steps. The steps taken for auditing a plugin will vary depending on who is doing the audit, but they will typically involve some or all of the items listed below.
- Evaluating all input areas on the plugin
- Examining requests made by the plugin
- Inspecting the source code
- Reviewing permissions and data storage on the plugin
- Examining data validation and sanitization
- Examining data escaping / secure output
As with any project, it’s best to implement these checks on every release. It may be best to hire a security firm to perform these checks for you, but it also helps to have in-house SAST and DAST scans, which can catch a lot of vulnerability types automatically pre-release.
Avoid Injection Vulnerabilities
Injection vulnerabilities occur when user input is injected into an application and misinterpreted as code. There are many different types of injection vulnerabilities, to name a few:
- SQL injection – where user input becomes part of a SQL query
- Code injection – where user input is interpreted as server-side code
- Command injection – where user input is interpreted as a server-side system command
- Cross-Site Scripting – where user input is interpreted as client-side code
The remediation for each type of vulnerability is different, but the underlying rule is that user input should not be trusted. We’ll cover more about handling user input safely later!
Use Nonces
A “nonce” is a security token that prevents Cross-Site Request Forgery (CSRF) attacks. In any situation where an authenticated state-changing or sensitive request is being parsed by the plugin, a nonce should be used. A nonce can be easily created and added to a URL or form by using built-in WordPress functions:
- wp_nonce_url()
- wp_nonce_field()
- wp_create_nonce()
Requests can then be verified using built-in WordPress functions:
- check_admin_referer()
- check_ajax_referer()
- wp_verify_nonce()
Sanitize User Input
Sanitization ensures that user input is what we expect. For example, if we ask for a number, we should ensure that we are not provided with a letter. Some basic examples of commonly used validation functions include:
- isset() and empty() to check whether a variable exists and isn’t blank
- mb_strlen() or strlen() to check that a string has the expected number of characters
- preg_match(), strpos() to check for occurrences of certain strings
- count() to check how many items are in an array
- in array() to check whether something exists in an array
Of course there are many more complex examples. WordPress and PHP provide a series of functions to help us with common tasks, and the function names are fairly self explanatory:
- sanitize_email()
- sanitize_file_name()
- sanitize_hex_color()
- sanitize_hex_color_no_hash()
- sanitize_html_class()
- sanitize_key()
- sanitize_meta()
- sanitize_mime_type()
- sanitize_option()
- sanitize_sql_orderby()
- sanitize_text_field()
- sanitize_textarea_field()
- sanitize_title()
- sanitize_title_for_query()
- sanitize_title_with_dashes()
- sanitize_user()
- sanitize_url()
- wp_kses()
- wp_kses_post()
Where possible, it is always better to use existing validation functions than roll your own. It is surprisingly difficult to write sanitization functions without missing some edge cases!
Escape Output
Equally important is escaping output. The type of escaping we use is dependent on the context in which the output is being printed. For example, the escaping required for some text within the attribute of a HTML tag would be different to the type of escaping required for use with inline JavaScript. Once again, PHP and WordPress provide a bunch of built-in functions to help:
- esc_html() – Use anytime an HTML element encloses a section of data being displayed.
- esc_attr() – Use on everything else that’s printed into an HTML element’s attribute.
- esc_js() – Use for inline Javascript.
- esc_xml() – Use to escape XML block.
- esc_url() – Use on all URLs, including those in the src and href attributes of an HTML element.
- esc_url_raw() – Use when storing a URL in the database or in other cases where non-encoded URLs are needed.
- esc_textarea() – Use this to encode text for use inside a textarea element.
- wp_kses() – Use to safely escape for all non-trusted HTML (post text, comment text, etc.
- wp_kses_data() – Alternative version of wp_kses() that allows only the HTML permitted in post comments.
- wp_kses_post() – Alternative version of wp_kses() that automatically allows all HTML that is permitted in post content.
Before using any of these functions, it’s important to be aware of any gotchas, because it is quite easy to introduce a vulnerability by using them incorrectly, or in the wrong context.
Don’t be Fooled by is_admin()
You would be forgiven for thinking that the is_admin() function will check if the current user has administrative privileges. This is not the case!
Despite what the name implies, is_admin() actually determines whether the current request is for an administrative interface page. If you’re hoping to check if the user is an administrator, use current_user_can(), which checks user roles and capabilities.
Understand User Roles and Capabilities
Having a clear understanding of user roles and capabilities is paramount to WordPress plugin security. This is what will determine which users have access to which data/functionality. By default, WordPress has six roles:
- Super Admin
- Administrator
- Editor
- Author
- Contributor
- Subscriber
WordPress stores User Roles and Capabilities in the options table. Custom user roles and capabilities can be added by a plugin.
Adding Custom User Roles
As a reference, the code below adds a simple custom role to the WordPress instance. This code snippet is lifted directly from the WordPress documentation. You will see that this code also assigns capabilities (for example, reading posts or uploading files) to that role.
function wporg_simple_role() { add_role( 'simple role', 'Simple Role', array( 'read' => true, 'edit posts' => true, 'upload files' => true, ), ); } // Add the simple role. add_action( 'init', 'wporg_simple_role' );
Adding Custom User Capabilities
Capabilities are the specific permissions you assign to each user role. Administrators, for example, have the manage_options capability which gives them the ability to view, edit and save options on the website. Editors and the subsequent user roles lack this capability which prevents them from administering site-wide options.
If the default user capabilities are not enough, you can define custom ones. Simply use get_role() to get the role object. Then use add_cap() on that object to add a new capability. Expanding on the previous example:
function wporg_simple_role_caps() { // Gets the simple_role role object. $role = get_role( 'simple_role' ); // Add a new capability. $role->add_cap( 'edit_others_posts', true ); } // Add simple_role capabilities, priority must be after the initial role definition. add_action( 'init', 'wporg_simple_role_caps', 11 );
Use SAST and DAST Solutions
There are many automated tools and services available to discover vulnerabilities in your code automatically. Even the best ones tend to produce a lot of false positives and false negatives, but they also do catch some vulnerabilities.
SAST stands for “Static Application Security Testing”. This form of testing uses automation to analyze your code directly. DAST stands for “Dynamic Application Security Testing”. This form of testing uses automated, simulated attacks against the front-end of the application, rather than the code itself. It is recommended that both are used.
Implementing these solutions does set a great minimum bar, and helps to uncover vulnerabilities that may otherwise go undetected, a word of warning though, these solutions are not a replacement for manual security testing by a professional. Rather, they should be treated as an additional security measure.
Stay Informed About Security Updates and News
Vulnerabilities in WordPress plugins are being discovered every day. It is necessary for developers to be informed with the latest security updates and news to stay aware of the security risks that may affect them.
Keeping an eye on vulnerability databases such as this one can be a great way to get familiar with the types of vulnerabilities that are often discovered. As a developer, it also pays to be aware of cybersecurity news in a more general sense, to keep a pulse on when some newly released attack vector or vulnerability may affect a project that you are working on.
This also goes for WordPress plugin users. As a user, one way to achieve this is simply to monitor for updates within the WordPress platform’s administrative panel. When a critical vulnerability is found in a WordPress plugin, it is often exploited very quickly on a wide range of websites. For this reason it is also a good idea to enable automatic updates.
Final Thoughts
Congratulations on reading this post, the first step in your secure development journey! There are plenty more things to know and learn, and the best place to start is usually the WordPress documentation.
The security of your customer’s websites is literally in your fingertips; good luck, and stay safe!