Update (2022-02-26): the tool is now public: https://github.com/kazet/wpgarlic

WordPress plugins expose a number of interfaces, such as:

  • AJAX endpoints (/wp-admin/admin-ajax.php)
  • Admin menu pages (/wp-admin/admin.php?page=...)
  • PHP files (in the /wp-content/plugins/ directory),
  • REST routes (/wp-json/...).

These interfaces have a consistent trust boundary: we know where the untrusted input goes and can detect what operations are executed on that input.

For instance, if you visit a .php file, provide appropriate parameters, and cause a file to be removed, you know that it is a vulnerability. You know what parameters you control and what ones you don’t – for example, you may redirect a logged-in admin to an admin menu page with arbitrary GET parameters, but you don’t control their cookies.

Therefore it is possible to semi-automatically scan for multiple classes of vulnerabilities in all WordPress plugins.

I have written a tool that:

  • executes each AJAX endpoint, menu page, REST route, or file multiple times,
  • injects payloads into the GET, POST, etc. arrays or REST parameters (more on how it’s done in the next section),
  • analyses the outputs with an ugly pile of regular expressions1 to detect:
    • calls to WordPress functions (such as wp_delete_post),
    • crashes (“No such file or directory”, “You have an error in your SQL syntax”, …),
    • XSS (echoing a known payload containing " or <),
    • etc.

This method is transferable to other CMS plugin ecosystems but not directly e.g. to Python packages. If a Python package allows you to remove arbitrary files, it may or may not be a vulnerability depending on the package role and your particular setup.

Injecting parameters

In PHP, the _GET, _POST, _SERVER, _COOKIE, and _REQUEST arrays contain various request parameters (e.g. GET and POST data, cookies, server configuration, and headers). I have replaced them with mock objects that allow access to any key - and with defined probability return a payload from a predefined payload list.

A simple mock $_POST array could be created using the following code:

<?php

class Mock implements ArrayAccess {
    function offsetGet($offset) {
        return "payload";
    }

    function offsetExists($offset) {
        return true;
    }

    function offsetSet($offset, $value) { }

    function offsetUnset($offset) { }
}

$_POST = new Mock();

echo $_POST["parameter_name"];

The above snippet will print payload.

Let’s assume that the $_REQUEST array has been mocked in a way similar to the one described above and that an AJAX route is handled by the following function:

public function delete_saved_block() {
	$block_id = (int) sanitize_text_field($_REQUEST['block_id']);
	$deleted_block = wp_delete_post($block_id);
	wp_send_json_success($deleted_block);
}

When $_REQUEST['block_id'] gets accessed, the mock will return a payload, thus allowing to detect that wp_delete_post was called on an attacker-controlled value.

This approach allowed to easily inject payloads even if the parameter name was hard to guess – the tool didn’t distinguish between id and secret_parameter_65e3c14a1d.

Some of the keys needed to be excluded manually (for example $_SERVER['HTTP_AUTHORIZATION'] or $_GET['doing_wp_cron']) because their values were handled by WordPress, and providing them caused plugin code to not be reached.

Besides, with some probability, a random type of array was returned instead of a string payload:

  • a singleton array with a string payload,
  • recursively, an object that allows access to any key,
  • a singleton array: random payload → random payload.

Detecting vulnerabilities

The tool contained checks to detect:

  • various kinds of crashes,
  • potentially dangerous operations,
  • information leaks.

Some of these checks led to a large number of CVEs (such as the XSS checks), some didn’t (e.g. the checks for syntax errors designed to catch eval() on untrusted code).

XSS

To detect XSS, checks were implemented that detected payloads being echoed back (or echoed back with escaping that didn’t prevent XSS, such as prefixing " with \).

Crashes

The following types of crashes were detected:

  • fopen() / file_get_contents() / require() / require_once() / include() / include_once() errors and “No such file or directory” or “failed to open stream” error messages,
  • unlink() error messages,
  • crashes related to call_user_func(),
  • SQL error messages,
  • unserialize() errors,
  • parse / syntax errors to detect eval() calls,
  • “command not found” error message,
  • simplexml_load_string() error messages2.

Information leaks

The output was analysed to observe whether known user e-mails or file names are displayed.

WordPress operations

WordPress has been instrumented to detect:

  • calls to maybe_unserialize,
  • calls to update_option/update_site_option/delete_option,
  • calls to wp_insert_user,
  • calls to wp_insert_post/wp_update_post/wp_delete_post,
  • calls to wp_mail,
  • calls to query (this one yielded an especially large number of false positives that needed additional filtering),
  • calls to get_users (this one has been added after accidentally discovering CVE-2021-25110 where an attacker can leak arbitrary user e-mails via a crafted user search query).

Additional checks

After fuzzing, the admin panel, the homepage, and the post pages were crawled to find occurrences of known payloads. That allowed for instance to detect CVE-2021-24975 in social-networks-auto-poster-facebook-twitter-g.

Update (2022-02-26): additionally, any attempts to access uploaded files are logged, so that they may be checked manually.

Changes to PHP

Patched equality

I have patched PHP so that equality comparison between any value and a known payload returned true with 1/3 probability. Forgive me about this one.

With this patch, I was able to detect vulnerabilities such as:

if ($_GET['action'] == 'please-remove-post') {
    wp_delete_post($_GET['id']);
}

Unfortunately, this resulted in a large number of false positives as well. The false positives were e.g. in the form of:

if (in_array($order, array("ASC", "DESC"), true)) {
    query("(...) ORDER BY $order");
}

The only solution for this problem I have used was browsing through these false positives and cursing. Further research can lead to coming up with other solutions.

Other changes

Besides, I have patched the PHP interpreter so that:

  • When base64_decode was performed on a known payload, this payload was returned again,
  • When json_decode was performed on a known payload, an object was returned that returns payloads when any key was accessed. These were the same objects that served as e.g. $_GET arrays,
  • when a redirect was performed, relevant information was displayed so that Open Redirect vulnerabilities could be detected.

Testing

A test-driven approach was critical during development. Tests checked that the tool would find a known vulnerability. For example, I could write a test to check that:

when fuzzing the wp_ajax_heateor_sss_import_config endpoint of the sassy-social-share plugin in version 3.3.23, the tool should detect that maybe_unserialize() gets called on an attacker-controlled payload.

False positives vs false negatives

This approach yielded a large number of false positives. It was a deliberate decision because I wanted to sort through multiple reports instead of missing vulnerabilities.

An alternative could be to write additional filtering logic. For example, there were multiple reports where an HTML payload was echoed back – but when checking them, I’ve observed that a correct JSON Content-Type header is added. This was one of the cases that could be checked automatically.

Other

Fuzzing was performed inside Docker containers, re-created for every plugin.

It was important to disconnect the network, because a lot of plugins call other web services, and I wanted to avoid sending random payloads there.

I found it also helpful to separate plugin fuzzing and analysis of the outputs. Because the outputs were analyzed by a lot of regular expressions, bugs happened. Therefore a relatively quick rescan allowed to speed up development.

Automating the fuzzing Added: 2022-02-16

Scanning thousands of plugins would not be possible without automating the job. Fortunately, WordPress plugins use consistent interfaces to integrate with WordPress, for example:

  • all REST routes are collected in a central registry, accessible via: rest_get_server()->get_routes(),
  • AJAX actions are created by adding a hook with a name starting with wp_ajax_,
  • there exists one registry with all admin menu pages.

Therefore all REST routes, AJAX actions, and menu actions can be enumerated in the same way regardless of which plugin is scanned. Of course, all PHP files can be easily listed as well.

All plugins can be installed in the same way: I have used WP-CLI – a tool that allows to install, activate, deactivate or delete a plugin from the command-line. The list of plugins could be downloaded automatically from the WordPress plugin registry API.

All of the above techniques made it possible to create a tool that doesn’t require any plugin-specific code.

Results Last updated: 2022-12-17

Because of time constraints, I have focused only on the most popular plugins. As of this moment, the following bugs found by the tool have already been fixed and published:

ID Plugin CVE Number of active installations Type Link
1 woocommerce CVE-2022-0775 5,000,000 Arbitrary comment deletion WPScan
2 updraftplus CVE-2021-25022 3,000,000 Reflected XSS WPScan
3 code-snippets CVE-2021-25008 500,000 Reflected XSS WPScan
4 woocommerce-pdf-invoices-packing-slips CVE-2021-24991 300,000 Reflected XSS WPScan
5 woocommerce-pdf-invoices-packing-slips CVE-2022-2537 300,000 Reflected XSS WPScan
6 ad-inserter CVE-2022-0288 200,000 Reflected XSS WPScan
7 caldera-forms CVE-2022-0879 200,000 Reflected XSS WPScan
8 complianz-gdpr CVE-2022-0193 200,000 Reflected XSS WPScan
9 custom-facebook-feed CVE-2021-25065 200,000 Reflected XSS WPScan
10 favicon-by-realfavicongenerator CVE-2022-0471 200,000 Reflected XSS WPScan
11 gotmls CVE-2022-2599 200,000 Reflected XSS WPScan
12 loginpress CVE-2022-0347 200,000 Reflected XSS WPScan
13 popup-builder CVE-2022-0479 200,000 Reflected XSS WPScan
14 use-any-font CVE-2021-24977 200,000 Arbitrary CSS append + stored XSS WPScan
15 white-label-cms CVE-2022-0422 200,000 Reflected XSS WPScan
16 wp-cerber CVE-2022-0429 200,000 Stored XSS WPScan
17 wp-gdpr-compliance CVE-2022-0147 200,000 Reflected XSS WPScan
18 capability-manager-enhanced CVE-2021-25032 100,000 Arbitrary settings update WPScan
19 chaty CVE-2021-25016 100,000 Reflected XSS WPScan
20 cmp-coming-soon-maintenance CVE-2022-0188 100,000 Possibility to add arbitrary CSS WPScan
21 download-manager CVE-2021-24969 100,000 Stored XSS WPScan
22 download-manager CVE-2021-25069 100,000 Reflected XSS WPScan
23 email-subscribers CVE-2022-0439 100,000 Blind SQL Injection WPScan
24 email-subscribers CVE-2022-3981 100,000 Blind SQL Injection WPScan
25 iubenda-cookie-law-solution CVE-2022-3911 100,000 Privilege escalation WPScan
26 learnpress CVE-2022-0271 100,000 Reflected XSS WPScan
27 menu-image CVE-2022-0450 100,000 Stored XSS WPScan
28 modern-events-calendar-lite CVE-2021-24925 100,000 Reflected XSS WPScan
29 modern-events-calendar-lite CVE-2021-24946 100,000 Blind SQL injection WPScan
30 modern-events-calendar-lite CVE-2021-25046 100,000 Stored XSS WPScan
31 paid-memberships-pro CVE-2021-25114 100,000 Blind SQL Injection WPScan
32 squirrly-seo CVE-2021-25019 100,000 Reflected XSS WPScan
33 ti-woocommerce-wishlist CVE-2022-0412 100,000 Blind SQL Injection WPScan
34 webp-converter-for-media CVE-2021-25074 100,000 Open redirect WPScan
35 woocommerce-products-filter CVE-2021-25085 100,000 Reflected XSS WPScan
36 wpvivid-backuprestore CVE-2021-24994 100,000 Stored XSS WPScan
37 wpvivid-backuprestore CVE-2022-0531 100,000 Reflected XSS WPScan
38 advanced-cf7-db CVE-2021-24905 90,000 Arbitrary file removal WPScan
39 kingcomposer CVE-2021-25048 90,000 Stored XSS WPScan
40 kingcomposer CVE-2022-0165 90,000 Open redirect WPScan
41 social-networks-auto-poster-facebook-twitter-g CVE-2021-24975 90,000 Stored XSS WPScan
42 social-networks-auto-poster-facebook-twitter-g CVE-2021-25072 90,000 CSRF post removal WPScan
43 themify-portfolio-post CVE-2022-0200 80,000 Reflected XSS (logged-in POST 3) WPScan
44 woo-product-feed-pro CVE-2021-24974 80,000 Stored XSS WPScan
45 woo-product-feed-pro CVE-2022-0426 80,000 Reflected XSS (logged-in POST 3) WPScan
46 wp-hide-security-enhancer CVE-2022-2538 80,000 Reflected XSS WPScan
47 feed-them-social CVE-2022-2532 70,000 Reflected XSS WPScan
48 www-xml-sitemap-generator-org CVE-2022-0346 70,000 Reflected XSS and RCE WPScan
49 booking CVE-2021-25040 60,000 Reflected XSS WPScan
50 interactive-3d-flipbook-powered-physics-engine CVE-2022-0423 60,000 Stored XSS WPScan
51 mappress-google-maps-for-wordpress CVE-2022-0208 60,000 Reflected XSS WPScan
52 permalink-manager CVE-2022-0201 60,000 Reflected XSS WPScan
53 post-grid CVE-2022-0447 60,000 Reflected XSS (logged-in POST 3) WPScan
54 powerpack-lite-for-elementor CVE-2021-25027 60,000 Reflected XSS WPScan
55 real-cookie-banner CVE-2022-0445 60,000 CSRF settings reset and deleting all GDPR consents WPScan
56 visual-portfolio CVE-2022-2543 60,000 Arbitrary CSS injection WPScan
57 visual-portfolio CVE-2022-2597 60,000 Arbitrary CSS injection WPScan
58 wd-instagram-feed CVE-2021-25047 60,000 Reflected XSS WPScan
59 woocommerce-currency-switcher CVE-2021-25043 60,000 Reflected XSS WPScan
60 woocommerce-currency-switcher CVE-2022-0234 60,000 Reflected XSS WPScan
61 wp-responsive-menu CVE-2021-24971 60,000 Stored XSS WPScan
62 wp-rss-aggregator CVE-2021-24988 60,000 Stored XSS WPScan
63 wp-rss-aggregator CVE-2022-0189 60,000 Reflected XSS (logged-in POST 3) WPScan
64 ditty-news-ticker CVE-2022-0533 50,000 Reflected XSS WPScan
65 easy-digital-downloads CVE-2022-2389 50,000 CSRF posts deletion WPScan
66 event-tickets CVE-2021-25028 50,000 Open redirect WPScan
67 nimble-builder CVE-2022-0314 50,000 Reflected XSS WPScan
68 simple-membership CVE-2022-0328 50,000 CSRF member deletion WPScan
69 super-socializer CVE-2021-24987 50,000 Reflected XSS WPScan
70 bnfw CVE-2022-0345 40,000 E-mail leak WPScan
71 thirstyaffiliates CVE-2022-0398 40,000 Arbitrary affiliate link creation WPScan
72 tutor CVE-2021-25017 40,000 Reflected XSS WPScan
73 advanced-cron-manager CVE-2021-25084 30,000 Arbitrary cron configuration change WPScan
74 contact-form-7-skins CVE-2021-25063 30,000 Reflected XSS WPScan
75 content-egg CVE-2022-0428 30,000 Reflected XSS (logged-in POST 3) WPScan
76 easy-paypal-donation CVE-2021-24989 30,000 CSRF post removal WPScan
77 futurio-extra CVE-2021-25110 30,000 E-mail leak WPScan
78 google-pagespeed-insights CVE-2022-0431 30,000 Reflected XSS (logged-in POST 3) WPScan
79 insight-core CVE-2021-24950 30,000 Stored XSS + object injection WPScan
80 lead-form-builder CVE-2021-24967 30,000 Stored XSS WPScan
81 master-addons CVE-2022-0327 30,000 Reflected XSS WPScan
82 meks-easy-instagram-widget CVE-2021-24958 30,000 Stored XSS WPScan
83 my-calendar CVE-2021-24927 30,000 Reflected XSS WPScan
84 notificationx CVE-2022-0349 30,000 Blind SQL Injection WPScan
85 photo-gallery CVE-2022-0169 30,000 SQL Injection WPScan
86 protect-wp-admin CVE-2021-24906 30,000 Disabling of plugin security features WPScan
87 pz-linkcard CVE-2021-25012 30,000 Reflected XSS WPScan
88 site-reviews CVE-2021-24973 30,000 Stored XSS WPScan
89 ultimate-faqs CVE-2021-24968 30,000 Possibility to add arbitrary FAQs WPScan
90 video-conferencing-with-zoom-api CVE-2022-0384 30,000 E-mail leak WPScan
91 woo-smart-wishlist CVE-2022-0397 30,000 Reflected XSS (logged-in POST 3) WPScan
92 wp-user-frontend CVE-2021-25076 30,000 SQL injection in admin panel leading to reflected XSS WPScan
93 xcloner-backup-and-restore CVE-2022-0444 30,000 Resetting settings, including encryption key WPScan
94 ad-invalid-click-protector CVE-2022-0190 20,000 SQL injection WPScan
95 ad-invalid-click-protector CVE-2022-0191 20,000 CSRF ban removal WPScan
96 advanced-product-labels-for-woocommerce CVE-2022-0399 20,000 Reflected XSS (logged-in POST 3) WPScan
97 asgaros-forum CVE-2022-0411 20,000 Blind SQL Injection WPScan
98 bwp-google-xml-sitemaps CVE-2022-0230 20,000 Stored XSS WPScan
99 crazy-bone CVE-2022-0385 20,000 Stored XSS WPScan
100 event-calendar-wd CVE-2021-25024 20,000 XSS WPScan
101 event-calendar-wd CVE-2021-25025 20,000 Possibility to add arbitrary events WPScan
102 float-menu CVE-2022-0313 20,000 CSRF menu deletion WPScan
103 gmap-embed CVE-2021-25011 20,000 Arbitrary post removal, plugin settings update WPScan
104 gmap-embed CVE-2021-25081 20,000 Arbitrary post removal, plugin settings update via CSRF WPScan
105 image-hover-effects-ultimate CVE-2021-25031 20,000 Reflected XSS WPScan
106 material-design-for-contact-form-7 CVE-2022-0404 20,000 DoS WPScan
107 miniorange-2-factor-authentication CVE-2022-0229 20,000 DoS WPScan
108 mycred CVE-2021-25015 20,000 Reflected XSS WPScan
109 mycred CVE-2022-0287 20,000 E-mail leak WPScan
110 mycred CVE-2022-0363 20,000 Arbitrary post creation WPScan
111 mystickyelements CVE-2022-0148 20,000 Reflected XSS WPScan
112 navz-photo-gallery CVE-2021-24909 20,000 Reflected XSS WPScan
113 newstatpress CVE-2022-0206 20,000 Reflected XSS WPScan
114 page-views-count CVE-2022-0434 20,000 SQL injection WPScan
115 restaurant-reservations CVE-2021-24965 20,000 Stored XSS WPScan
116 woo-product-slider CVE-2022-2382 20,000 DoS WPScan
117 woocommerce-product-addon CVE-2021-25018 20,000 Stored XSS WPScan
118 wp-accessiblity-helper CVE-2022-0150 20,000 Reflected XSS WPScan
119 wp-stats-manager CVE-2021-24750 20,000 SQL injection WPScan
120 wp-stats-manager CVE-2021-25042 20,000 Stored XSS WPScan
121 wp-stats-manager CVE-2022-0410 20,000 Blind SQL Injection WPScan
122 wplegalpages CVE-2021-25106 20,000 Stored XSS WPScan
123 advanced-page-visit-counter CVE-2021-24957 10,000 Blind SQL injection WPScan
124 advanced-page-visit-counter CVE-2021-25086 10,000 Stored XSS WPScan
125 affiliates-manager CVE-2021-25078 10,000 Stored XSS WPScan
126 akismet-privacy-policies CVE-2021-25071 10,000 Reflected XSS WPScan
127 ari-fancy-lightbox CVE-2022-0161 10,000 Reflected XSS WPScan
128 business-profile CVE-2021-25060 10,000 Stored XSS WPScan
129 coming-soon-page CVE-2022-0164 10,000 Sending any e-mail to all subscribers WPScan
130 coming-soon-page CVE-2022-0199 10,000 Sending any e-mail to all subscribers via CSRF WPScan
131 directorist CVE-2022-2376 10,000 E-mail leak WPScan
132 directorist CVE-2022-2377 10,000 Sending arbitrary e-mails WPScan
133 dropdown-menu-widget CVE-2021-25113 10,000 Stored XSS WPScan
134 duplicate-page-or-post CVE-2021-25075 10,000 Stored XSS WPScan
135 easy-pricing-tables CVE-2021-25098 10,000 CSRF post removal WPScan
136 english-wp-admin CVE-2021-25111 10,000 Open redirect WPScan
137 ibtana-visual-editor CVE-2021-25014 10,000 Stored XSS WPScan
138 ip2location-country-blocker CVE-2021-25095 10,000 Banning arbitrary countries WPScan
139 ip2location-country-blocker CVE-2021-25096 10,000 Ban circumvention WPScan
140 ip2location-country-blocker CVE-2021-25108 10,000 Banning countries via CSRF WPScan
141 link-library CVE-2021-25091 10,000 Reflected XSS WPScan
142 link-library CVE-2021-25092 10,000 CSRF settings reset WPScan
143 link-library CVE-2021-25093 10,000 Arbitrary link removal WPScan
144 modal-window CVE-2021-25051 10,000 CSRF RCE WPScan
145 page-builder-add CVE-2021-25067 10,000 Reflected XSS WPScan
146 portfolio-wp CVE-2021-25090 10,000 Stored XSS WPScan
147 powerpack-addon-for-beaver-builder CVE-2022-0176 10,000 Reflected XSS WPScan
148 qubely CVE-2021-25013 10,000 Arbitrary post removal WPScan
149 rearrange-woocommerce-products CVE-2021-24928 10,000 SQL injection WPScan
150 registrations-for-the-events-calendar CVE-2021-24943 10,000 SQL injection WPScan
151 registrations-for-the-events-calendar CVE-2021-25083 10,000 Reflected XSS WPScan
152 secure-copy-content-protection CVE-2021-24931 10,000 SQL injection WPScan
153 smart-forms CVE-2022-0163 10,000 Downloading form data WPScan
154 spider-event-calendar CVE-2022-0212 10,000 Reflected XSS WPScan
155 stopbadbots CVE-2021-25070 10,000 Blind SQL injection WPScan
156 ultimate-product-catalogue CVE-2021-24993 10,000 Possibility to add arbitrary products WPScan
157 wa-sticky-buttons CVE-2022-2375 10,000 Stored XSS WPScan
158 whmcs-bridge CVE-2021-25112 10,000 Reflected XSS WPScan
159 wicked-folders CVE-2021-24919 10,000 SQL injection WPScan
160 woo-orders-tracking CVE-2021-25062 10,000 Reflected XSS WPScan
161 woocommerce-exporter CVE-2022-0149 10,000 Reflected XSS WPScan
162 woocommerce-store-toolkit CVE-2021-25077 10,000 Reflected XSS WPScan
163 wp-booking-system CVE-2021-25061 10,000 Reflected XSS WPScan
164 wp-coder CVE-2021-25053 10,000 CSRF RCE WPScan
165 wp-coder CVE-2022-2388 10,000 CSRF code deletion WPScan
166 wp-marketing-automation CVE-2022-2387 10,000 Adding automations WPScan
167 wp-photo-album-plus CVE-2021-25115 10,000 Stored XSS WPScan
168 wp-popup-builder CVE-2022-2404 10,000 Reflected XSS WPScan
169 wp-popup-builder CVE-2022-2405 10,000 Arbitrary popup deletion WPScan
170 wp125 CVE-2021-25073 10,000 CSRF ad deletion WPScan
171 wpcargo CVE-2021-25003 10,000 RCE WPScan
172 events-made-easy CVE-2021-25030 7,000 SQL injection WPScan
173 likebtn-like-button CVE-2021-24945 7,000 Sensitive data exposure WPScan
174 likebtn-like-button CVE-2022-0745 7,000 Arbitrary e-mail sending WPScan
175 wp-email-users CVE-2021-24959 7,000 SQL injection + object injection WPScan
176 responsive-vector-maps CVE-2021-24947 6,000 Arbitrary file read WPScan
177 button-generation CVE-2021-25052 5,000 CSRF RCE WPScan

Not all of the vulnerabilities were found directly by the fuzzer. For example, CVE-2021-25096 was found accidentally when writing a PoC for CVE-2021-25095. For some other vulnerabilities, the tool alerts were only part of the vulnerability information – for example, the tool notified that a WordPress option can get updated by any user - and finding the consequences (whether it can lead e.g. to stored XSS) required manual work.

Findings worth mentioning

I won’t make fun of any particular plugin author, however, I think some findings are worth sharing.

is_admin

The WordPress is_admin() function, as you may probably have guessed:

Determines whether the current request is for an administrative interface page.

(from https://developer.wordpress.org/reference/functions/is_admin/)

The documentation warns as well, that it:

Does not check if the user is an administrator; use current_user_can() for checking roles and capabilities.

As you may probably have guessed, it was a source of a couple of vulnerabilities in the form of:

if (is_admin()) {
    /* dangerous action */
}

REST route URLs

Let’s consider the following code:

register_rest_route((...), '/(...)/(?P<id>[\d]+)', array(
    array(
        'methods' => WP_REST_Server::READABLE,
        'callback' => array($this, 'callback'),
        'permission_callback' => '__return_true',
    ),
));

/* ... */

function callback($request) {
    $id = $request['id'];
}

What ID values could be passed to the handler?

The correct answer is: all of them – just use /?rest_route=/(...)/1&id=hehehe.

get_users()

Some plugins allow searching for users by providing a part of an e-mail address. That allows to leak any user’s e-mail using the following steps:

  • Bruteforcing the first letter of the domain name (searching for @a, @b, etc., and checking when the user’s name appears in search results).
  • Remembering the first letter and using it to guess the second letter. Let’s assume the user’s e-mail domain name starts with g. You can then brute force the second letter (@ga, @gb, …).
  • Repeating the above steps for the rest of the e-mail address.

Because of that, I have added a check that alerts when get_users() gets called. Unfortunately, besides finding vulnerabilities of this type, it led to numerous false positives as well.

XSS protection

Don’t do the following:

if (/* potential XSS in $parameter detected */) die('Invalid parameter: ' . $parameter);

Several XSS vulnerabilities were also caused by debugging helpers in the form of:


echo "<!--";

var_dump($_POST);

echo "-->";

CAPTCHA verification

Don’t do this:

if (isset($_POST['captcha'])) {
    /* verify captcha */
}

/* do action that should be CAPTCHA-protected */

I have observed this pattern multiple times, both for CAPTCHAs and nonces.

Conclusions

This was just a proof-of-concept to check whether automatic techniques are a viable method to find WordPress plugin bugs. I am sure it can be improved by e.g.:

  • adding checks to detect other types of dangerous operations,
  • attempting to decrease the number of false positives without a large loss of true positives. The percentage of false positives was one of the main obstacles in this project.

This technique can also be implemented for other plugin ecosystems.

Many of the vulnerabilities I found were easily preventable by modern software engineering practices. In many WordPress plugins, HTML is built using an error-prone pile of echo statements, instead of a template language. Similarly, AJAX endpoints are by default available for all logged-in users or all not logged-in users, instead of requiring the developer to provide a fixed allowlist of roles or permissions (so that they would have to explicitly mark a route as available to all logged-in users). Introducing techniques that make it harder to make mistakes and promoting their use is, unfortunately, something only the WordPress team, not plugin developers, can do.

Footnotes

  1. In retrospect, using a pile of regular expressions to detect crashes in the output wasn’t the best idea. Now I would try to do this differently. 

  2. In retrospect, it is obvious that it doesn’t cover all ways to load XML. This should have been done differently. 

  3. This type of reflected XSS requires cookies to be sent with a POST request, therefore would be harder to exploit due to the SameSite-by-default behavior.  2 3 4 5 6 7 8