Headline
CVE-2023-37979: Multiple Vulnerabilities in WordPress Ninja Forms Plugin - Patchstack
Unauth. Reflected Cross-Site Scripting (XSS) vulnerability in Saturday Drive Ninja Forms Contact Form plugin <= 3.6.25 versions.
This blog post is about Ninja Forms plugin vulnerabilities. If you’re a Ninja Forms user, please update the plugin to at least version 3.6.26.
Patchstack Developer and Business users are protected from the vulnerability. You can also sign up for the Patchstack Community plan to be notified about vulnerabilities as soon as they become disclosed.
For plugin developers, we have security audit services and Threat Intelligence Feed API for hosting companies.
About the Ninja Forms plugin
The plugin Ninja Forms versions 3.6.25 and below, free version), which has over 900,000 active installations is known as the more popular forms builder plugin in WordPress. This plugin is developed by Saturday Drive.
This plugin is a free form builder plugin for WordPress that enables us to build just about any type of form we could imagine. Ranging from simple contact forms to event registrations, file uploads, payments, and more.
The security vulnerability
This plugin suffers from multiple vulnerabilities. The first vulnerability is a POST-based reflected XSS. This vulnerability could allow any unauthenticated user to steal sensitive information to, in this case, privilege escalation on the WordPress site by tricking privileged users to visit the crafted website. The described vulnerability was fixed in version 3.6.26 and assigned CVE-2023-37979.
The second and third vulnerabilities are a broken access control on form submissions export feature. This vulnerability allows Subscriber and Contributor role user to export all of the Ninja Forms submissions on a WordPress site. The described vulnerability was also fixed in version 3.6.26 and assigned CVE-2023-38393 and CVE-2023-38386.
Reflected XSS
The initial entry-point of this vulnerability exist in the route function:
public function route()
{
register_shutdown_function( array( $this, 'shutdown' ) );
$method = strtolower( $_SERVER['REQUEST_METHOD'] );
/*
* Request Method Override
* Allows for a POST request to function as another Request Method
* by passing a `method_override` value as a request parameter.
* For example, some servers do not support the DELETE request method.
*/
if( 'post' == $method and isset( $_REQUEST[ 'method_override' ] ) ){
$method = sanitize_text_field( $_REQUEST[ 'method_override' ] );
}
if( ! method_exists( $this, $method ) ){
$this->_errors[] = esc_html__( 'Endpoint does not exist.', 'ninja-forms' );
$this->_respond();
}
/**
* This call get the $_REQUEST info for the call(post, get, etc.)
* being called.
*/
$request_data = $this->get_request_data();
try {
$data = $this->$method($request_data);
$this->_respond( $data );
-------------------------- CUT HERE --------------------------
The above route function could be called from an ajax action named nf_batch_process. Notice on the function that we could override the $method using $_REQUEST[ ‘method_override’ ] parameter if the original HTTP method performed is a POST request. The code then will call this->$method() with $request_data as input parameter. The $request_data value is constructed from get_request_data function :
protected function get_request_data()
{
$request_data = array();
if (isset($_REQUEST['batch_type']) && $_REQUEST['batch_type']) {
$request_data['batch_type'] = WPN_Helper::sanitize_text_field($_REQUEST['batch_type']);
}
if (isset($_REQUEST['data']) && $_REQUEST['data']) {
// @TODO: Find a way to safely sanitize this later.
// sanitize_text_field overcorrects, breaking "actual" data.
$request_data['data'] = $_REQUEST['data'];
}
if (isset($_REQUEST['security']) && $_REQUEST['security']) {
$request_data['security'] = WPN_Helper::sanitize_text_field($_REQUEST['security']);
}
if (isset($_REQUEST['action']) && $_REQUEST['action']) {
$request_data['action'] = WPN_Helper::sanitize_text_field($_REQUEST['action']);
}
return $request_data;
}
The interesting part is which function could we call on the $method variable. Turns out that we could call the _respond function itself which originally act as a wrapper function to output data :
protected function _respond( $data = array() )
{
if( empty( $data ) ){
$data = $this->_data;
}
if( isset( $this->_data['debug'] ) ) {
$this->_debug = array_merge( $this->_debug, $this->_data[ 'debug' ] );
}
if( isset( $this->_data['errors'] ) && $this->_data[ 'errors' ] ) {
$this->_errors = array_merge( $this->_errors, $this->_data[ 'errors' ] );
}
// allow for accessing and acting on $data before responding
do_action( 'ninja_forms_before_response', $data );
$response = array( 'data' => $data, 'errors' => $this->_errors, 'debug' => $this->_debug );
echo wp_json_encode( $response );
wp_die(); // this is required to terminate immediately and return a proper response
}
Notice that the function will output $response that could be constructed from our $data variable with echo wp_json_encode(). This will still make HTTP response output as text/html. Looking back at the get_request_data function, we could inject the XSS payload via $_REQUEST[‘data’] since there is no sanitization or escaping implemented.
Subscriber+ Broken Access Control
The underlying vulnerability exist in the processing function:
public function processing() {
// Get our passed arguments. These come from the querysting of the processing page.
if ( isset ( $_REQUEST['args'] ) ) {
$this->args = WPN_Helper::sanitize_text_field($_REQUEST['args']);
if ( isset ( $this->args['redirect'] ) ) {
if( wp_validate_redirect( $this->args['redirect'] ) ){
$this->redirect = wp_sanitize_redirect( $this->args['redirect'] );
}
}
} else {
$this->args = array();
}
// Get our current step.
$this->step = isset ( $_REQUEST['step'] )? esc_html( $_REQUEST['step'] ) : 'loading';
// Get our total steps
$this->total_steps = isset ( $_REQUEST['total_steps'] )? esc_html( $_REQUEST['total_steps'] ) : 0;
// If our step is loading, then we need to return how many total steps there are along with the next step, which is 1.
if ( 'loading' == $this->step ) {
$return = $this->loading();
if ( ! isset ( $return['step'] ) ) {
$saved_step = get_user_option( 'nf_step_processing_' . $this->action . '_step' );
if ( ! empty ( $saved_step ) ) {
$this->step = $saved_step;
} else {
$this->step = 1;
}
$return['step'] = $this->step;
}
if ( ! isset ( $return['complete'] ) ) {
$return['complete'] = false;
}
} else { // We aren't on the loading step, so do our processing.
$return = $this->step();
-------------------------- CUT HERE --------------------------
The processing function is set to be a function handler of nf_download_all_subs ajax action and there is no proper permission check. The function will call $this->step() function which will export all of the submission to the $this->args[‘filename’] we specified.
Contributor+ Broken Access Control
The underlying vulnerability exists in the export_listen function:
-------------------------- CUT HERE --------------------------
if (isset ($_REQUEST['download_file']) && !empty($_REQUEST['download_file'])) {
// Open our download all file
$filename = esc_html($_REQUEST['download_file']);
$upload_dir = wp_upload_dir();
$file_path = trailingslashit($upload_dir['path']) . $filename . '.csv';
if (file_exists($file_path)) {
$myfile = file_get_contents($file_path);
} else {
$redirect = esc_url_raw(remove_query_arg(array('download_file', 'download_all')));
wp_redirect($redirect);
die();
}
unlink($file_path);
$form_name = Ninja_Forms()->form(absint($_REQUEST['form_id']))->get()->get_setting('title');
$form_name = sanitize_title($form_name);
$today = date('Y-m-d', current_time('timestamp'));
$filename = apply_filters('ninja_forms_download_all_filename', $form_name . '-all-subs-' . $today);
header('Content-type: application/csv');
header('Content-Disposition: attachment; filename="' . $filename . '.csv"');
header('Pragma: no-cache');
header('Expires: 0');
echo $myfile;
die();
}
-------------------------- CUT HERE --------------------------
The function is set to be a function handler of load-edit.php hook which could be triggered when a user for example visits the /wp-admin/edit.php endpoint. Since there is no proper permission check, by default, user with minimum role of Contributor could export form submissions.
The patch
For the reflected XSS issue, the vendor decide to restrict which method that user could access from the function. With this patch, it is enough to prevent user to directly call the _respond function to trigger the XSS. The patch can be seen below :
For the Subscriber+ broken access control, adding a permission check should also fix the reported issue. The patch can be seen below :
For the Contributor+ broken access control also, adding a permission check should also fix the reported issue. The patch can be seen below :
Conclusion
For some cases, plugin or theme code need to call certain function or class from user supplied string. Always try to check and restrict which function or class the user could directly call. Also pay extra attention to an export data action and always implement permission or access control check to the related functions.
Timeline
22 June, 2023We found the vulnerability and reached out to the plugin vendor.
04 July, 2023Ninja Forms version 3.6.26 was published to patch the reported issue.
27 July, 2023Security advisory article publicly released.
Help us make the Internet a safer place
Making the WordPress ecosystem more secure is a team effort, and we believe that plugin developers and security researchers should work together.
- If you’re a plugin developer, join our mVDP program that makes it easier to report, manage and address vulnerabilities in your software.
- If you’re a security researcher, join Patchstack Alliance to report vulnerabilities & earn rewards.
Related news
WordPress Ninja Forms plugin version 3.6.25 suffers from a cross site scripting vulnerability.
Multiple security vulnerabilities have been disclosed in the Ninja Forms plugin for WordPress that could be exploited by threat actors to escalate privileges and steal sensitive data. The flaws, tracked as CVE-2023-37979, CVE-2023-38386, and CVE-2023-38393, impact versions 3.6.25 and below, Patchstack said in a report last week. Ninja Forms is installed on over 800,000 sites. A brief description