If you keep a watch on software security newsletters or blogs like the Wordfence blog, you’ll know there are a good number of new detected defects and vulnerabilities on a regular basis, even on well known plugins and software. It’s worth looking into the details of how this happens especially if you work on PHP software from time to time. Thankfully there are public records which let you compare to look at how these are fixed:
Checking into previous security holes
Pulling differences
The benefit of much of the software you’ll find in WordPress or other open source environments is that there is a standard repository of most of the versions in a similar format and consistent download selector at the bottom of the “advanced view”.
To quickly download and compare two different versions, you can use this simple python script:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import urllib.request
import urllib.parse
import shutil
import os
plugin = input('What plugin do you want to analyze? slug=')
verA = input('What version A? ')
verB = input('What version B? ')
print('Downloading...')
zipA = '%s.%s.zip' % (plugin, verA )
zipB = '%s.%s.zip' % (plugin, verB )
urllib.request.urlretrieve('https://downloads.wordpress.org/plugin/%s.%s.zip' % ( plugin, verA), zipA)
urllib.request.urlretrieve('https://downloads.wordpress.org/plugin/%s.%s.zip' % ( plugin, verB), zipB)
print('Extracting...')
shutil.unpack_archive(zipA, zipA.replace('.zip',''))
shutil.unpack_archive(zipB, zipB.replace('.zip',''))
print('Diff:')
os.system('diff --recursive ./%s.%s ./%s.%s ' % (plugin, verA, plugin, verB) )
This will give output like the following in a terminal – if you want to see the differences in the recent All Export plugin update, for example:
$ python3 ./analyze.py
What plugin do you want to analyze? slug=wp-all-export
What version A? 1.4.0
What version B? 1.4.1
Downloading...
Extracting...
Diff:
diff --recursive ./wp-all-export.1.4.0/wp-all-export/controllers/admin/export.php ./wp-all-export.1.4.1/wp-all-export/controllers/admin/export.php
83a84,86
> if ($this->input->post('is_submitted')) {
> check_admin_referer('choose-cpt', '_wpnonce_choose-cpt');
> }
153,154d155
< check_admin_referer('choose-cpt', '_wpnonce_choose-cpt');
<
158c159
< wp_redirect(esc_url_raw(add_query_arg('action', 'options', $this->baseUrl)));
---
> wp_redirect(esc_url_raw(add_query_arg(['action' => 'options','_wpnonce_options' => wp_create_nonce('options')], $this->baseUrl)));
161c162
< wp_redirect(esc_url_raw(add_query_arg('action', 'template', $this->baseUrl)));
---
> wp_redirect(esc_url_raw(add_query_arg(['action' => 'template','_wpnonce_template' => wp_create_nonce('template')], $this->baseUrl)));
175a177,179
> check_admin_referer( 'template', '_wpnonce_template' );
>
>
234d237
< check_admin_referer('template', '_wpnonce_template');
304c307
< wp_redirect(esc_url_raw(add_query_arg('action', 'options', $this->baseUrl)));
---
> wp_redirect(esc_url_raw(add_query_arg(['action' => 'options','_wpnonce_options' => wp_create_nonce('options')], $this->baseUrl)));
313c316
< wp_redirect(esc_url_raw(add_query_arg(array('page' => 'pmxe-admin-manage', 'pmxe_nt' => urlencode(__('Options updated', 'pmxi_plugin'))) + array_intersect_key($_GET, array_flip($this->baseUrlParamNames)), admin_url('admin.php'))));
---
> wp_redirect(esc_url_raw(add_query_arg(array('page' => 'pmxe-admin-manage', 'pmxe_nt' => urlencode(__('Options updated', 'pmxi_plugin')),'_wpnonce_options' => wp_create_nonce('options')) + array_intersect_key($_GET, array_flip($this->baseUrlParamNames)), admin_url('admin.php'))));
356a360,361
> check_admin_referer( 'options', '_wpnonce_options' );
>
424,425d428
<
< check_admin_referer('options', '_wpnonce_options');
diff --recursive ./wp-all-export.1.4.0/wp-all-export/controllers/controller.php ./wp-all-export.1.4.1/wp-all-export/controllers/controller.php
118c118
< if ( ! wp_verify_nonce( $nonce, '_wpnonce-download_feed' ) && !isset($_GET['google_feed']) ) {
---
> if ( ! wp_verify_nonce( $nonce, '_wpnonce-download_feed' ) ) {
diff --recursive ./wp-all-export.1.4.0/wp-all-export/helpers/pmxe_functions.php ./wp-all-export.1.4.1/wp-all-export/helpers/pmxe_functions.php
23c23,26
< return ( strpos($path, $uploads['basedir']) === false and ! preg_match('%^https?://%i', $path)) ? $uploads['basedir'] . $path : $path;
---
>
> // If the path isn't http(s) and doesn't start with the basedir, add the basedir.
> return ( strncmp($path, $uploads['basedir'], strlen($uploads['basedir'])) !== 0 and ! preg_match( '%^https?://%i', $path ) ) ? $uploads['basedir'] . $path : $path;
>
diff --recursive ./wp-all-export.1.4.0/wp-all-export/readme.txt ./wp-all-export.1.4.1/wp-all-export/readme.txt
4,5c4,5
< Tested up to: 6.3
< Stable tag: 1.4.0
---
> Tested up to: 6.4
> Stable tag: 1.4.1
184a185,187
>
> = 1.4.1 =
> *security improvement
diff --recursive ./wp-all-export.1.4.0/wp-all-export/views/admin/export/index.php ./wp-all-export.1.4.1/wp-all-export/views/admin/export/index.php
300c300
< <?php wp_nonce_field('choose-cpt', '_wpnonce_choose-cpt'); ?>
---
> <?php wp_nonce_field('choose-cpt', '_wpnonce_choose-cpt'); ?>
302c302
< <span class="wp_all_export_continue_step_two"></span>
---
> <span class="wp_all_export_continue_step_two"></span>
diff --recursive ./wp-all-export.1.4.0/wp-all-export/views/admin/export/options.php ./wp-all-export.1.4.1/wp-all-export/views/admin/export/options.php
123c123
< <a href="<?php echo esc_url(apply_filters('pmxi_options_back_link', add_query_arg('action', 'template', $this->baseUrl), $this->isWizard)); ?>" class="back rad3"><?php esc_html_e('Back', 'wp_all_export_plugin') ?></a>
---
> <a href="<?php echo esc_url(apply_filters('pmxi_options_back_link', add_query_arg(['action'=>'template','_wpnonce_template' => wp_create_nonce('template')], $this->baseUrl), $this->isWizard)); ?>" class="back rad3"><?php esc_html_e('Back', 'wp_all_export_plugin') ?></a>
diff --recursive ./wp-all-export.1.4.0/wp-all-export/views/admin/manage/index.php ./wp-all-export.1.4.1/wp-all-export/views/admin/manage/index.php
188,189c188,189
< <span class="edit"><a class="edit" href="<?php echo esc_url(add_query_arg(array('id' => $item['id'], 'action' => 'template'), $this->baseUrl)) ?>"><?php esc_html_e('Edit Template', 'wp_all_export_plugin') ?></a></span> |
< <span class="edit"><a class="edit" href="<?php echo esc_url(add_query_arg(array('id' => $item['id'], 'action' => 'options'), $this->baseUrl)) ?>"><?php esc_html_e('Settings', 'wp_all_export_plugin') ?></a></span> |
---
> <span class="edit"><a class="edit" href="<?php echo esc_url(add_query_arg(array('id' => $item['id'], 'action' => 'template','_wpnonce_template' => wp_create_nonce('template')), $this->baseUrl)) ?>"><?php esc_html_e('Edit Template', 'wp_all_export_plugin') ?></a></span> |
> <span class="edit"><a class="edit" href="<?php echo esc_url(add_query_arg(array('id' => $item['id'], 'action' => 'options','_wpnonce_options' => wp_create_nonce('options')), $this->baseUrl)) ?>"><?php esc_html_e('Settings', 'wp_all_export_plugin') ?></a></span> |
229c229
< href="<?php echo esc_url(add_query_arg(array('id' => $item['id'], 'action' => 'options'), $this->baseUrl)) ?>"
---
> href="<?php echo esc_url(add_query_arg(array('id' => $item['id'], 'action' => 'options','_wpnonce_options' => wp_create_nonce('options')), $this->baseUrl)) ?>"
243c243
< href="<?php echo esc_url(add_query_arg(array('id' => $item['id'], 'action' => 'options'), $this->baseUrl)) ?>"
---
> href="<?php echo esc_url(add_query_arg(array('id' => $item['id'], 'action' => 'options','_wpnonce_options' => wp_create_nonce('options')), $this->baseUrl)) ?>"
251c251
< href="<?php echo esc_url(add_query_arg(array('id' => $item['id'], 'action' => 'options'), $this->baseUrl)) ?>"
---
> href="<?php echo esc_url(add_query_arg(array('id' => $item['id'], 'action' => 'options','_wpnonce_options' => wp_create_nonce('options')), $this->baseUrl)) ?>"
diff --recursive ./wp-all-export.1.4.0/wp-all-export/views/admin/manage/update.php ./wp-all-export.1.4.1/wp-all-export/views/admin/manage/update.php
99c99
< <a href="<?php echo esc_url(apply_filters('pmxi_options_back_link', add_query_arg('id', $item->id, add_query_arg('action', 'template', $this->baseUrl)), $isWizard)); ?>" class="back rad3"><?php esc_html_e('Edit Template', 'wp_all_export_plugin') ?></a>
---
> <a href="<?php echo esc_url(apply_filters('pmxi_options_back_link', add_query_arg('id', $item->id, add_query_arg(['action'=>'template','_wpnonce_template' => wp_create_nonce('template')], $this->baseUrl)), $isWizard)); ?>" class="back rad3"><?php esc_html_e('Edit Template', 'wp_all_export_plugin') ?></a>
diff --recursive ./wp-all-export.1.4.0/wp-all-export/wp-all-export.php ./wp-all-export.1.4.1/wp-all-export/wp-all-export.php
6c6
< Version: 1.4.0
---
> Version: 1.4.1
62c62
< define('PMXE_VERSION', '1.4.0');
---
> define('PMXE_VERSION', '1.4.1');
Note the differences here include a WordPress nonce, which is an important security measure that can be added in minutes while building the plugin.
Another example – The Events Calendar
Let’s look at the diff of a patched reported vulnerability for The Events Calendar:
$ python3 ./analyze.py
What plugin do you want to analyze? slug=the-events-calendar
What version A? 6.2.8
What version B? 6.2.8.1
Downloading...
Extracting...
Diff:
diff --recursive ./the-events-calendar.6.2.8/the-events-calendar/common/src/Tribe/Field.php ./the-events-calendar.6.2.8.1/the-events-calendar/common/src/Tribe/Field.php
884c884
< * @since TBD
---
> * @since 5.1.15
diff --recursive ./the-events-calendar.6.2.8/the-events-calendar/common/src/Tribe/JSON_LD/Abstract.php ./the-events-calendar.6.2.8.1/the-events-calendar/common/src/Tribe/JSON_LD/Abstract.php
91a92,101
> // Double check that the user can read this post.
> if ( ! current_user_can( 'read', $post->ID ) ) {
> return [];
> }
>
> // Ensure this post is not password protected.
> if ( post_password_required( $post ) ) {
> return [];
> }
>
...
Here you can see another type of check, that checks the permissions of current user! This is always an important check for all pages that have privileged information or actions.
Another patch: SQL Injection, WP Mail Log
Wordfence also noted a patch to this plugin, which you can find among the diff of the two versions in the same manner:
265,266c298
< $query_cols = [ 'id', 'subject', 'message', 'headers', 'attachments', "DATE_FORMAT(sent_date, '%Y/%m/%d %H:%i:%S') as sent_date, attachments_file as files" ];
< $entry_query = 'SELECT distinct ' . implode( ',', $query_cols ) . ' FROM ' . $table_name . ' WHERE id=' . $id;
---
> $query_cols = [ 'id', 'subject', 'message', 'headers', 'attachments', "DATE_FORMAT(sent_date, '%Y/%m/%d %H:%i:%S') as sent_date", "attachments_file as files" ];
267a300,308
> // write query using wpdb->prepare
> $entry_query = $wpdb->prepare(
> "SELECT DISTINCT id, subject, message, headers, attachments, DATE_FORMAT(sent_date, '%%Y/%%m/%%d %%H:%%i:%%S') as sent_date ,attachments_file as files FROM %i WHERE id=%d",
> $table_name,
> $id
> );
$wpdb->prepare is an important feature for sql cleaning although it had often been misused or simply used with one argument – check the documentation!
Looking forward
Are you concerned about other similar security holes in popular WordPress plugins that you use? If you find a security vulnerability in the first 20 days of December, there is an increased bug-bounty offered by Wordfence! As always, please let software developers and researchers know privately and don’t publish details publicly if you find a security vulnerability in a well-known plugin.