Typical Plugin Code Errors
Escaping
Variables and options must be escaped when echo’d
Much related to sanitizing everything, all variables that are echoed need to be escaped when they’re echoed, so it can’t hijack users or (worse) admin screens. There are many esc_*() functions you can use to make sure you don’t show people the wrong data, as well as some that will allow you to echo HTML safely.
At this time, we ask you escape all $-variables, options, and any sort of generated data when it is being echoed. That means you should not be escaping when you build a variable, but when you output it at the end. We call this ‘escaping late.’
Besides protecting yourself from a possible XSS vulnerability, escaping late makes sure that you’re keeping the future you safe. While today your code may be only outputted hardcoded content, that may not be true in the future. By taking the time to properly escape when you echo, you prevent a mistake in the future from becoming a critical security issue.
This remains true of options you’ve saved to the database. Even if you’ve properly sanitized when you saved, the tools for sanitizing and escaping aren’t interchangeable (except for esc_url(), and yes, we know that’s confusing). Sanitizing makes sure it’s safe for processing and storing in the database. Escaping makes it safe to output.
Also keep in mind that sometimes a function is echoing when it should really be returning content instead. This is a common mistake when it comes to returning JSON encoded content. Very rarely is that actually something you should be echoing at all. Echoing is because it needs to be on the screen, read by a human. Returning (which is what you would do with an API) can be json encoded, though remember to sanitize when you save to that json object!
There are a number of options to secure all types of content (html, email, etc). Yes, even HTML needs to be properly escaped.
Remember: You must use the most appropriate functions for the context. There is pretty much an option for everything you could echo. Even echoing HTML safely.
Wrong:
echo '' . $current_tab_title . esc_html__( ' settings updated successfully.', 'sell_media' ) . '';
Correct:
echo esc_html( $current_tab_title . __( ' settings updated successfully.', 'sell_media' ) );
Wrong:
<?php
// ...
echo '<div class="sell-media-admin-payments">';
// ...
Correct:
<?php
// ...
?>
<div class="sell-media-admin-payments">
<?php
// ...
Wrong:
sprintf(
'<ul>
<li>%s: ' . $payments->get_meta_key( $post->ID, 'first_name' ) . ' ' . $payments->get_meta_key( $post->ID, 'last_name' ) . ' ' . '</li>
<li>%s: ' . $payments->get_meta_key( $post->ID, 'email' ) . ' ' . '</li>
<li>%s: ' . $payments->total( $post->ID ) . ' ' . '</li>
</ul>',
__( 'Name', 'sell_media' ),
__( 'Email', 'sell_media' ),
__( 'Total', 'sell_media' )
);
Correct:
<ul>
<li><?php echo esc_html( __( 'Name', 'sell_media' ) ); ?>: <?php echo esc_html( $payments->get_meta_key( $post->ID, 'first_name' ) . ' ' . $payments->get_meta_key( $post->ID, 'last_name' ) . ' ' )?> </li>
<li><?php echo esc_html( __( 'Email', 'sell_media' ) ); ?>: <?php echo esc_html( $payments->get_meta_key( $post->ID, 'email' ) . ' ' ) ?></li>
<li><?php echo esc_html( __( 'Total', 'sell_media' ) ); ?>: <?php echo esc_html( $payments->total( $post->ID ) . ' ') ?></li>
</ul>
Wrong:
<span><?php echo sprintf( esc_attr__( '%s Pricelists', 'sell_media' ), rtrim( $this->tab['tab_title'], 's' )); ?></span>
Correct:
<span><?php echo esc_html( sprintf( __( '%s Pricelists', 'sell_media' ), rtrim( $this->tab['tab_title'], 's' )) ); ?></span>
Wrong:
<form method="post" action="<?php echo $url; ?>">
Correct:
<form method="post" action="<?php echo esc_url( $url ); ?>">
Wrong (sanitize_ is for input, and esc_ is for output):
<input type="text" value="<?php print sanitize_text_field($password) ?>" />
Correct:
<input type="text" value="<?php echo esc_attr($password) ?>" />
Wrong (esc_attr used otside of attribute):
<label for="collection_icon"><?php esc_attr_e( 'Icon', '...' ); ?></label>
Correct:
<label for="collection_icon"><?php esc_html_e( 'Icon', '...' ); ?></label>
Wrong (wp_send_json() already makes echo and wp_die):
echo wp_send_json( $response );
Correct:
wp_send_json( $response );
Another example. Wrong:
echo '<option value="' . esc_attr($name) . '" data-id="' . esc_attr($v['id']) . '" data-price="' . number_format( $v['price'], 2, '.', '') . '" data-qty="1" data-size="' . esc_attr($dimensions) . '">' . esc_attr($name) . ': ' . sell_media_get_currency_symbol() . sprintf( '%0.2f', $v['price'] ) . '</option>';
Correct:
<option value="<?php echo esc_attr($name); ?>"
data-id="<?php echo esc_attr($v['id']); ?>"
data-price="<?php echo esc_attr( number_format( $v['price'], 2, '.', '') ); ?>"
data-qty="1"
data-size="<?php echo esc_attr($dimensions); ?>">
<?php echo esc_html($name . ': ' . sell_media_get_currency_symbol() . sprintf( '%0.2f', $v['price'] ) ); ?></option>
Wrong JSON output:
header( 'Content-type: application/json' );
echo json_encode( $response );
die();
Correct:
wp_send_json( $response );
Wrong usage of wp_kses (missing => true):
echo wp_kses($gpp_tmp_html, array(
'table' => array('class' ),
'tr' => array('class' ),
'th' => array('class' ),
'td' => array('class' ),
'span' => array('class' ),
));
Correct:
echo wp_kses($gpp_tmp_html, array(
'table' => array('class' => true),
'tr' => array('class' => true),
'th' => array('class' => true),
'td' => array('class' => true),
'span' => array('class' => true),
));
Wrong order of esc_():
<?php echo ucwords( esc_attr( $field['name'] ) ); ?>
Correct:
<?php echo esc_attr( ucwords( $field['name'] ) ); ?>
Wrong ouput of select option example:
echo '<option value="' . esc_attr( $key ) . '" ' . $selected . '>' . esc_attr( $v ) . '</option>';
Correct:
<?php // ... ?>
<option value="<?php echo esc_attr( $key ); ?>" selected="<?php echo esc_attr( $selected );?>"><?php echo esc_html( $v ); ?></option>
<?php // ... ?>
Data Must be Sanitized, Escaped, and Validated
When you include POST/GET/REQUEST/FILE calls in your plugin, it’s important to sanitize, validate, and escape them. The goal here is to prevent a user from accidentally sending trash data through the system, as well as protecting them from potential security issues.
SANITIZE: Data that is input (either by a user or automatically) must be sanitized as soon as possible. This lessens the possibility of XSS vulnerabilities and MITM attacks where posted data is subverted.
VALIDATE: All data should be validated, no matter what. Even when you sanitize, remember that you don’t want someone putting in ‘dog’ when the only valid values are numbers.
ESCAPE: Data that is output must be escaped properly when it is echo’d, so it can’t hijack admin screens. There are many esc_*() functions you can use to make sure you don’t show people the wrong data.
To help you with this, WordPress comes with a number of sanitization and escaping functions. You can read about those here:
https://developer.wordpress.org/plugins/security/securing-input/
https://developer.wordpress.org/plugins/security/securing-output/
Remember: You must use the most appropriate functions for the context. If you’re sanitizing email, use sanitize_email(), if you’re outputting HTML, use esc_html(), and so on.
An easy mantra here is this:
Sanitize early
Escape Late
Always Validate
Clean everything, check everything, escape everything, and never trust the users to always have input sane data. After all, users come from all walks of life.
Wrong:
if ( ! empty( $_POST['collection_password'] ) && $_POST['collection_password'] == $password ){
//...
}
Correct:
if ( isset( $_POST['collection_password'] ) && sanitize_text_field( $_POST['collection_password'] ) == $password ){
//...
}
Wrong (partial sanitizing):
if ( isset( $_POST[ $markup_id ] ) && '' != $_POST[ $markup_id ] ) {
$attrs[ $markup_id ] = sanitize_text_field( $_POST[ $markup_id ] );
}
Correct:
if ( isset( $_POST[ $markup_id ] ) && '' != sanitize_text_field( $_POST[ $markup_id ] ) ) {
$attrs[ $markup_id ] = sanitize_text_field( $_POST[ $markup_id ] );
}
Wrong (non-sanitized file path):
$path = sell_media_get_import_dir() . '/' . $_POST['dir'] . '/';
Correct:
$path = sell_media_get_import_dir() . '/' . sanitize_file_name($_POST['dir']) . '/';
Wrong (writing to $_POST):
if ( ! isset( $_POST['tab'] ) ) {
$_POST['tab'] = 'newest';
}
Correct:
$tab = 'newest';
if ( isset( $_POST['tab'] ) && '' != sanitize_text_field($_POST['tab']) ) {
$tab = sanitize_text_field($_POST['tab']);
}
Tested Up To Value is Out of Date, Invalid, or Missing
The tested up to value in your plugin is not set to the current version of WordPress. This means your plugin will not show up in searches, as we require plugins to be compatible and documented as tested up to the most recent version of WordPress.
Please update your readme to show that it is tested up to the most recent version of WordPress. You cannot set it beyond the current version, as that will similarly cause your plugin not to be available on searches.
Wrong:
plugin-name/readme.txt:6:Tested up to: 5.3.2
Correct:
plugin-name/readme.txt:6:Tested up to: 5.8.2
Including Libraries Already In Core
Your plugin has included a copy of a library that WordPress already includes.
WordPress includes a number of useful libraries, such as jQuery, Atom Lib, SimplePie, PHPMailer, PHPass, and more. For security and stability reasons, plugins may not include those libraries in their own code, but instead must use the versions of those libraries packaged with WordPress.
You can see the list of jQuery Libraries here:
While we do not YET have a decent public facing page to list all these libraries, we do have a list here:
It’s fine to locally include add-ons not in core, but please ONLY add those additional files. For example, you do not need the entire jquery UI library for one file. If your code doesn’t work with the built-in versions of jQuery, it’s most likely a noConflict issue.
Example:
plugin-name/assets/plugins/jquery/jquery.min.js
plugin-name/assets/plugins/jquery/jquery.slim.min.js
plugin-name/assets/plugins/jquery-steps/lib/jquery-1.11.1.min.js
plugin-name/assets/plugins/jquery-steps/lib/jquery-1.9.1.min.js
plugin-name/assets/plugins/jquery-steps/lib/jquery-1.10.2.min.js
plugin-name/assets/plugins/jquery.flot/jquery.js
plugin-name/assets/plugins/jquery.maskedinput/lib/jquery-1.8.3.min.js
plugin-name/assets/plugins/jquery.maskedinput/lib/jquery-1.9.0.min.js
plugin-name/assets/plugins/jquery-ui-slider/external/jquery/jquery.js
plugin-name/assets/plugins/jquery-ui/
plugin-name/assets/plugins/jquery-ui-slider/jquery-ui.min.js
Including a zip file
We do not permit plugins to include zip files. It was probably an oversight, but please remove any and all zip files from your plugin.
Example:
plugin-name/assets/plugins/jquery-steps/build/jquery.steps-1.1.0.zip
Included Unneeded Folders
Your plugin includes folders and files that are not required for the running of your plugin. Some examples are:
- vendor folders (bower, node, grunt, etc)
- documentation
- demos
- unit tests
The entire library isn’t going to be used by your users, and having all those files included is just extra weight to a plugin.
If you’re trying to include the human-readable version of your own code (in order to comply with our guidelines) remember that we permit you to put links to them in your readme.
If you’ve made a Block plugin and used npm and webpack to compress and minify it, you can either include the source code within the published plugin or provide access to a public maintained source that can be reviewed, studied, and yes, forked.
But you can, and should, safely remove those other folders from your plugins.
Example:
plugin-name/assets/plugins/flag-icon-css/index.html
Calling core loading files directly
Including wp-config.php, wp-blog-header.php, wp-load.php directly via an include is not permitted.
These calls are prone to failure as not all WordPress installs have the exact same file structure. In addition it opens your plugin to security issues, as WordPress can be easily tricked into running code in an unauthenticated manner.
Your code should always exist in functions and be called by action hooks. This is true even if you need code to exist outside of WordPress. Code should only be accessible to people who are logged in and authorized, if it needs that kind of access. Your plugin’s pages should be called via the dashboard like all the other settings panels, and in that way, they’ll always have access to WordPress functions. https://developer.wordpress.org/plugins/hooks/
If you need to have a ‘page’ accessed directly by an external service, you should use query_vars and/or rewrite rules to create a virtual page which calls a function.
https://developer.wordpress.org/reference/hooks/query_vars/
https://codepen.io/the_ruther4d/post/custom-query-string-vars-in-wordpress
If you’re trying to use AJAX, please read this: https://developer.wordpress.org/plugins/javascript/ajax/
Examples:
plugin-name/ajax.php:9:require_once DIR . '/../../../wp-load.php';
plugin-name/upload_settings_xlsx.php:9:require_once DIR . '/../../../wp-load.php';
plugin-name/sp-logger.php:8:require_once DIR . '/../../../wp-load.php';
Out of Date Libraries
It could be
At least one of the 3rd party libraries you’re using is out of date. Please upgrade to the latest stable version for better support and security. We do not recommend you use beta releases.
WP Team
it means, libraries should be updated.
Examples:
plugin-name/assets/plugins/chart.js/Chart.min.js
plugin-name/assets/plugins/chart.js/Chart.bundle.min.js:3: * Chart.js
plugin-name/assets/plugins/bootstrap/css/bootstrap.min.css:2: * Bootstrap v4.1.3 (https://getbootstrap.com/)
plugin-name/assets/plugins/bootstrap/js/bootstrap.bundle.min.js:2: * Bootstrap v4.3.1 (https://getbootstrap.com/)
plugin-name/assets/plugins/bootstrap/js/bootstrap-rtl.js:2: * Bootstrap v4.3.1 (https://getbootstrap.com/)
plugin-name/assets/plugins/bootstrap/js/bootstrap.min.js:2: * Bootstrap v4.1.3 (https://getbootstrap.com/)
plugin-name/assets/js/bootstrap.min.js:2: * Bootstrap v4.5.2 (https://getbootstrap.com/)
plugin-name/assets/js/bootstrap.min.css:2: * Bootstrap v4.5.2 (https://getbootstrap.com/)
Please use wp_enqueue commands
Your plugin is not correctly including JS and/or CSS. You should be using the built in functions for this: https://developer.wordpress.org/reference/functions/wp_enqueue_script/
https://developer.wordpress.org/reference/functions/wp_enqueue_style/
And remember you can use this function to add inline javascript:
https://developer.wordpress.org/reference/functions/wp_add_inline_script/
As of WordPress 5.7, you can pass attributes like async, nonce, and type by using new functions and filters:
If you’re trying to enqueue on the admin pages you’ll want to use the admin enqueues
https://developer.wordpress.org/reference/hooks/admin_enqueue_scripts/
https://developer.wordpress.org/reference/hooks/admin_print_scripts/
https://developer.wordpress.org/reference/hooks/admin_print_styles/
Examples:
plugin-name/footer.php:3:<script src="<?= plugin_dir_url( FILE ); assets/plugins/jquery/jquery.min.js"></script><!-- Bootstrap Bundle js -->
plugin-name/footer.php:4:<script src="<?= plugin_dir_url( FILE ); assets/plugins/bootstrap/js/bootstrap.bundle.min.js"></script><!-- Ionicons js -->
plugin-name/footer.php:5:<script src="<?= plugin_dir_url( FILE ); assets/plugins/ionicons/ionicons.js"></script><!-- Moment js -->
plugin-name/footer.php:6:<script src="<?= plugin_dir_url( FILE ); assets/plugins/moment/moment.js"></script><!-- P-scroll js -->
plugin-name/footer.php:7:<script src="<?= plugin_dir_url( FILE ); assets/plugins/perfect-scrollbar/perfect-scrollbar.min.js"></script>
plugin-name/footer.php:8:<script src="<?= plugin_dir_url( FILE ); assets/plugins/perfect-scrollbar/p-scroll.js"></script><!-- eva-icons js -->
Do not use PHP Short Tags
The primary issue with PHP’s short tags is that PHP managed to choose a tag (<?) that was used by another syntax: XML.
With the option enabled, you aren’t able to raw output the xml declaration without getting syntax errors:
<?xml version=”1.0″ encoding=”UTF-8″ ?>
This is a big issue when you consider how common XML parsing and management is.
We know as of PHP 5.4, <?= … ?> tags are supported everywhere, regardless of short tags settings. This should mean they’re safe to use in portable code but in reality that has proven not to be the case. Also it’s possible to disable short-tags, which means your plugin will unexpectedly break. Basically the risk here is way higher than the benefits.
At this time, we ask that no plugin use PHP short tags, for sanity.
Examples:
plugin-name/pages/settings-product.php:23: <script src="<?= SP_PLUGIN_DIR_URL; assets/js/bootstrap.min.js" integrity="sha384-B4gt1jrGC7Jh4AgTPSdUtOBvfO8shuf57BaghqFfPlYxofvL8/KUEfYiJOMMV+rV" crossorigin="anonymous"></script>
plugin-name/pages/settings-product.php:24: <script src="<?= SP_PLUGIN_DIR_URL; assets/js/moment.min.js" integrity="sha512-qTXRIMyZIFb8iQcfjXWCO8+M5Tbc38Qi5WzdPOYZHIlZpzBHG3L3by84BBBOiRGiEb7KKtAOAs5qYdUiZiQNNQ==" crossorigin="anonymous"></script>
plugin-name/pages/po_create_po.php:158: <script src="<?= SP_PLUGIN_DIR_URL; assets/js/bootstrap.min.js" integrity="sha384-B4gt1jrGC7Jh4AgTPSdUtOBvfO8shuf57BaghqFfPlYxofvL8/KUEfYiJOMMV+rV" crossorigin="anonymous"></script>
Nonces and User Permissions Needed for Security
Please add a nonce to your AJAX calls to prevent unauthorized access.
Keep in mind, check_admin_referer alone is not bulletproof security. Do not rely on nonces for authorization purposes. Use current_user_can() in order to prevent users without the right permissions from accessing things.
If you use wp_ajax to trigger submission checks, remember they also need a nonce check.
You also must avoid checking for post submission outside of functions. Doing so means the check runs on every single load of the plugin which means every single person who views any page on a site using your plugin will check for a submission. Doing that makes your code slow and unwieldy for users on any high-traffic site, causing instability and crashes.
The following links may assist you in development:
https://developer.wordpress.org/plugins/security/nonces/
https://developer.wordpress.org/plugins/settings/settings-api/
https://developer.wordpress.org/plugins/javascript/ajax/#nonce
Using CURL Instead of HTTP API
WordPress comes with an extensive HTTP API that should be used instead of creating your own curl calls. It’s both faster and more extensive. It’ll fall back to curl if it has to, but it’ll use a lot of WordPress’ native functionality first.
https://developer.wordpress.org/plugins/http-api/
Please note: If you’re using CURL in 3rd party vendor libraries, that’s permitted. It’s in your own code unique to this plugin (or any dedicated WordPress libraries) that we need it corrected.
Wrong (cURL used):
// curl usage
$result = curl_exec($curl);
Correct (use wp_remote_… functions):
$result = wp_remote_...();
Incorrect Stable Tag
In your readme, your ‘Stable Tag’ does not match the Plugin Version as indicated in your main plugin file.
Readme:
plugin-name/readme.txt:6:Stable tag: 4.3
Plugin File:
plugin-name/plugin-name.php:7:Version: 1.0
Your Stable Tag is meant to be the stable version of your plugin, not of WordPress. For your plugin to be properly downloaded from WordPress.org, those values need to be the same. If they’re out of sync, your users won’t get the right version of your code.
We recommend you use Semantic Versioning (aka SemVer) for managing versions:
https://en.wikipedia.org/wiki/Software_versioning
https://semver.org/
Please note: While currently using the stable tag of trunk currently works in the Plugin Directory, it’s not actually a supported or recommended method to indicate new versions and has been known to cause issues with automatic updates.
We ask you please properly use tags and increment them when you release new versions of your plugin, just like you update the plugin version in the main file. Having them match is the best way to be fully forward supporting.
Problem With Your Plugin Name
We cannot accept this plugin as is because your name needs to be fixed. Don’t worry! We can fix it, but we need your help.
The name you submitted was this: Test Name Plugin
That created the slug of this: test-name-plugin
We recommend this slug instead: test-name
The reason for this is we have some requirements about plugin names, like you can’t include the word ‘plugin’ or ‘wordpress’ in them (it’s redundant after all), but also because names that are too long, or too short, or too generic are really annoying.
When a name is over 4 words, it’s frustrating for users and developers alike and makes for an extremely poor experience. This is especially true of a plugin with stop words like ‘a’ or ‘an’ or ‘the’ or even ‘by.’
Plugin names like ‘cars’ and ‘users’ aren’t permitted because their names are too generic. Try to use a better name like ‘car organize’ or ‘user management’ — something that’s more specific to the exact nature of your plugin. Generic plugin names are usually used to try and scam SEO.
Using special characters (ones that aren’t [a-z][0-9]) in the name of your plugin is also a bad idea. By using characters outside the American English alphabet, you get VERY weird results on other sites, and it’s just not something we support (code wise).
How do we sanitize arrays or esc json codes, function calls?
You DO sanitize arrays when you save them, yes. We recommend array_map() or array_walk().
If you cannot use those, then the only way is to break apart the array and sanitize each individual portion to a NEW variable.
The logic there is that instead of assuming you know every item in an array (which may be subverted by a hack), you only sanitize the items you KNOW you need. With saving JSON, again, you would decode it into an array and sanitize that.
Since you don’t want to ever save a JSON formatted variable to the database anyway, it’s not an unnecessary move. When you ECHO you only have to sanitize json IF you decode.
So echo wp_json_encode ($variable); is fine as is, but echo json_DEcode( $variable); has to be escaped. Note: You do want to use wp_json_encode() and not just json_encode() whenever possible.
WP Team
Your plugin cannot be reopened yet
What to do if your plugin blocked, after being published?
All closed plugins are required to pass a security and guideline check before they may be reopened, in order to reduce the negative impact on users. This is especially true now that a few ‘security firms’ have taken it on themselves to zero-day and release all known vulnerabilities for any closed plugin, regardless of cause. We hope that requiring this will protect you from them.
You are required to do the following:
– Perform a complete review of your entire plugin to ensure it’s properly up to date
– Correct all issues listed.
– Ensure the ‘tested up to’ version in your readme is the latest release of WordPress (if you fail to do this, we cannot review nor reopen your plugin as doing so would cause your plugin to become unfindable in our search system)
– Increase your plugin version (you only need to do this once – if you’ve already done so, you do not need to again)
– Upload your corrected plugin to SVN (don’t worry – even with a new version, no one will get updated until the plugin is reopened). If there is any reason you feel your plugin should be reopened in light of these issues, please let us know. There are always reasonable exceptions (like needing time to replace a library, or not being able to use curl or enqueues), and we are absolutely willing to hear them.
Our goal is to prevent your reputation and your users from being hurt due to hostile security teams 0-daying you. We will re-review your entire plugin again once we receive your email. Please remember that we cannot review an emailed zip.
You must use SVN properly for us to proceed. We appreciate your patience and understanding in this process.
WP Team
Guidelines:
https://make.wordpress.org/plugins/
https://developer.wordpress.org/plugins/wordpress-org/detailed-plugin-guidelines/