In the first lesson of this module, you learned about the main security principles that you
should follow when developing a custom plugin or theme. In this lesson, you're going to learn how
to apply these principles when developing your WordPress plugins and themes by fixing an
insecurely coded form submission plugin. Start, download the Learn Plugin Security plugin,
and install the plugin to your local development environment. Then, open the main plugin file in
your code editor and take a look at the code. At the top of the main plugin file, some constants
are set up which are used elsewhere in the plugin. The first two are used to define page slugs that
the plugin will use to redirect to. For this plugin functionality to work, these pages need
to exist with the correct slugs. Next, a callback function is registered on the plugin activation
hook. This sets up a custom form submissions table in the database. After that, the plugin's
admin JavaScript and frontend style CSS files are enqueued. Next, a shortcode is registered,
which is used to display a form on the frontend. After that, a callback function is hooked into the
WP action, which is what the plugin uses to process the form submission. Next, the plugin
registers an admin submenu, which displays a list of form submissions. Next, there is a function
which renders the admin page when clicking on the submenu link. Then, there is a function that the
admin submenu uses to fetch the form submissions from the database. Lastly, there is a callback
function that's hooked into a WP Ajax hook, which is the Ajax endpoint the plugin uses to delete
form submissions from the admin submenu page. The admin JavaScript file handles the Ajax request to
delete form submissions from the submissions page. The frontend style CSS file is used when the user
enters a class attribute for the shortcode. It defaults to red, but the user can change this to
blue. It simply adds a border to the form. When the shortcode is added to a postal page,
it renders the form and users can submit their details. When the form is submitted,
it will redirect either to the success or error page, depending on whether the form submission
was successful or not. Then, in the dashboard, admins can view the form submissions and delete them.
First common vulnerability we're going to look for is SQL injection.
SQL injection happens when values being inputted are not properly sanitized,
allowing for any SQL commands using the inputted data to potentially be executed on the database.
First place we need to tackle a possible SQL injection vulnerability is in the function that
processes the form data. We need to make sure that any post data is sanitized before being used in
the query. We also need to either use the WPDB prepare or insert functions instead of just the
query function. This will ensure that the name and email field values are both sanitized as they are
accepted from the form submission request and that they are sanitized before being used to store the
record in the database. While this might seem like overkill, if you just sanitize the inputs and the
code is later changed to use the values in a different way, you could still be vulnerable
to a SQL injection. The other place we need to prevent SQL injection is in the function that
deletes the form submission. Again, we need to make sure that any post data is sanitized before used in
the query and we either need to use the WPDB prepare or delete functions. Because the ID of
the form submission is an integer, we can use the PHP typecasting functionality to make sure it's
always cast as an integer. The next common vulnerability we're going to look for is
cross-site scripting. Cross-site scripting happens when a nefarious party injects JavaScript into a
web page which can be used to launch multiple different attacks or malicious activities from
the website. You can avoid cross-site scripting vulnerabilities by escaping your outputs, stripping
out any unwanted data. Your code should escape dynamic content with the proper function depending
on the type of content being escaped. Let's look at some places where data is being output and make
sure that it's being escaped properly. The first place is in the rapid div of the form shortcode
callback. Here, the class attribute of the div is rendered based on the attributes passed to the
shortcode. This is a potential cross-site scripting vulnerability as the class attribute is not
escaped. Note that you should specifically use the ESC_ATTR function to escape HTML attributes.
Next, we have the function which renders the admin page.
Here, the submission name, submission email, and submission ID should be escaped.
In this example, you can use ESC_HTML because this is the correct function to use any time an HTML
object encloses a section of data being displayed. Always pay close attention to what each escaping
function does, as some will remove HTML while others will permit it. You must use the most
appropriate function to the content and context of what you're echoing. Finally, you cast the ID to
an integer again, as that is being used in a data attribute. You could also use the ESC_ATTR function
we used earlier, but in this case, casting it to an integer is perfectly fine. The next vulnerability
to prevent is cross-site request forgery. Cross-site request forgery has been a nefarious party tricks
a user into performing an unwanted action within a web application that they are authenticated in.
When developing with WordPress, becoming familiar with WordPress nonces is a must to help prevent
cross-site request forgery. A nonce is a number used once and provides a way to verify that the
origin of the request is legitimate. You create a nonce when you need to verify that the request
is legitimate. You output or pass the nonce to wherever needs to make the request, and finally,
you verify the nonce when the request is made. There are two possible cross-site request forgery
vulnerabilities in this plugin. The first is when the form is submitted and the data is processed.
To fix this, we need to add a nonce to the form being rendered in the shortcode,
and then verify it when the form is submitted. In the form, we use the wp_nonce_field function
to add a hidden field with the nonce. Notice how you pass in an action and a name to this function.
The action is used to identify the nonce and the name is the name of the field that will be added
to the form. If you inspect the form, you can see the nonce field, which is using the name you pass
to the function and the nonce value. Then, in the form_submission function, you can verify the nonce
using the wp_verify_nonce function, passing in the value of the nonce field and the action.
It's usually a good idea to also check that the nonce field has been passed in the request
and then verify that the nonce is valid. A better way to do this code would be to check if the
nonce field doesn't exist or if the nonce field is not verified and then redirect to the error page.
Here, we are checking that the nonce field has been passed in the request and then verifying
that the nonce is valid. If the nonce is not passed or is invalid, we redirect to the error page.
The other place you need to prevent CSRF is in the Ajax callback used to delete the form submission.
To fix this, first you need to manually create a nonce using wp_create_nonce
and then pass this nonce to the JavaScript layer using wp_localize_script.
Notice how you pass the nonce as a property of the wp_learn_ajax object, which also contains the Ajax URL.
This is what is used in the jQuery post request to make the request happen.
Then we need to include the nonce in the jQuery post request.
Note how you specify the nonce in the data passed to the post request as _ajax_nonce.
This is the name that WordPress expects when processing an Ajax request.
Lastly, in the Ajax callback function, verify the nonce using the handy check_ajax_referer function.
Notice that the string passed to check_ajax_referer is the same string passed to wp_create_nonce when
creating the nonce. If the check_ajax_referer function fails, it will cause execution to stop,
so you don't need to check the result of this function.
There's one more vulnerability in this plugin and it's a broken_access_control vulnerability.
Broken_access_control is when a user is able to access a resource they should not be able to
access. For example, a user might be able to access an admin function even though they are not an
administrator. In this example, you have a broken_access_control vulnerability in the Ajax callback
function. At the present moment, any authenticated user could make a request to this Ajax request
URL with the right data and it would delete a form submission. To fix this, you can use the
WordPress Roles and Capabilities API to check that the user has the correct permissions to delete a
form submission. In this case, it could be just as simple as checking that the user is an admin user.
Here, you're using the current user can function to check whether the user has the manage_options
permission, which is specifically an administrator permission. If this check fails, it will simply
return an authentication error result. Note that we're doing two checks here, one against
cross-site request forgery and one for access control. In this example, the order of execution
is not super important, but in general, it's a good idea to check for cross-site request forgery
first and then check for access control. There's one additional security vulnerability in this
plugin. It's a tough one to spot, but all instances of WP redirect should be replaced with WP safe
redirect. This is because the code is redirecting to a local URL and WP safe redirect checks whether
the location it's using is an allowed host if it has an absolute path. This prevents the possibility
of malicious redirects if the redirect location is ever attacked. To read more about fixing common
vulnerabilities in WordPress code, make sure to bookmark the page on common vulnerabilities in the
WordPress developer documentation on security, as well as the example function using capability
checks, data validation, secure input, output, and nonces.