WP Manifestindependent plugin directory
manifest / forms / qterest

QTE Rest WP Registry grade D. High-severity findings · 5 findingsWP RegistryHigh-severity findings · 5 findingsOpen the reportD

This plugin can be used for making forms, newsletter signup (mailchimp) and search.

★ 2stars
498release downloads
≈20active sites
8composer installs
v1.7.0latest · 2 years ago

Install

Download qterest.zip From the v1.7.0 release on GitHub, Feb 21, 2024

Upload it under Plugins → Add New Plugin → Upload Plugin, then activate.

Install straight from GitHub with WP-CLI:

wp plugin install https://github.com/qteab/qterest/releases/download/v1.7.0/qterest.zip

On Packagist as qteab/qterest:

composer require qteab/qterest
Updates itself. It ships its own updater (Plugin Update Checker), so new versions appear under Dashboard → Updates.

This plugin can be used for making forms, newsletter signup (mailchimp) and search.

Installation

Follow these steps to install the plugin.

  1. Download the latest release as a zip.
  2. Go over to your wordpress admin panel.
  3. Go to "Plugins" and "Add New"
  4. Press "Upload Plugin" and select the file you downloaded earlier.

The plugin is now installed.

Contact

This part of the plugin handles the forms.

Creating forms

Creating forms can be done with qterest_render_form(array $args, bool $echo = true)

Arguments (array)

The following arguments are accepted

$args = [
    'wrapper_class' => "qterest-form-container", // (string) Overrides the default wrapper class
    'form_class' => "qterest-form", // (string) Overrides the default form class
    'form_row_class' => "qterest-form-row", // (string) Overrides the default form row class
    'form_misc_class' => "qterest-form-misc", // (string) Overrides the default form misc class
    'form_title' => "Contact form", // (string) Adds a h3-tag with the given content
    'error_messages_class' => "qterest-error-messages", // (string) Overrides the default error message class
    'success_message_class' => "qterest-success-messages", // (string) Overrides the default success message class
    'form_fields_class' => "qterest-form-fields", // (string) Overrides the default form fields class
    'submit_label' => __("Submit", 'qterest'), // (string) Overrides the default submit label
    'submit_class' => 'button submit', // (string) Overrides the default submit class
    'fields' => $fields, // (array) Check the next section
]

Fields

The following fields are accepted

$fields = [
    [
        'type' => 'text', // REQUIRED (string) Possible values are text, email, file, tel, hidden and textarea
        'name' => 'my_text_field', // REQUIRED (string) HTML Attribute name
        'placeholder' => 'Text', // (string) HTML Attribute placeholder
        'value' => 'some_value', // (string) HTML Attribute value
        'label' => 'Text', // (string) Label text
        'class' => 'form-control', // (string) Overrides default class
        'required' => true, // (string) Is the field required?
        'toggles_on' => 'my_checkbox_field', // (string) The id of the field that you want to toogle this field
    ],
    [
        'type' => 'checkbox', // REQUIRED (string) Possible values are checkbox and radio
        'name' => 'my_checkbox_field', // REQUIRED (string) HTML Attribute name
        'value' => 'yes', // (string) HTML Attribute value
        'label' => 'Checkbox', // (string) Label text
        'class' => 'form-control', // (string) Overrides default class
        'toggles' => true, // (bool) Can this field toggle other fields? ONLY checkbox
    ],
    [
        'type' => 'select', // REQUIRED (string) Possible values are select
        'name' => 'my_text_field', // REQUIRED (string) HTML Attribute name
        'placeholder' => 'Text', // (string) HTML Attribute placeholder
        'value' => 'some_value', // (string) HTML Attribute value
        'label' => 'Text', // (string) Label text
        'class' => 'form-control', // (string) Overrides default class
        'toggles_on' => 'my_checkbox_field', // (string) The id of the field that you want to toogle this field
        'options' => [
            [
                'name' => 'My first option', // REQUIRED (string) The name of this option
                'value' => 'my_first_option', // REQUIRED (string) The value of this option
            ],
            [
                'name' => 'My second option', // REQUIRED (string) The name of this option
                'value' => 'my_second_option', // REQUIRED (string) The value of this option
            ]
        ]
    ],
]

Misc

It's also possible to put misc in the fields array. These miscs are accepted

$fields = [
    [
        'type' => 'title',
        'text' => 'Hejsan',
        'size' => '3' // This will be a h3 element
    ],
    [
        'type' => 'paragraph',
        'text' => 'Lorem ipsum dolor sit'
    ],
    [
        'type' => 'link',
        'href' => 'https://google.com',
        'text' => 'Google.se'
    ]
]

Example form

This is a example of a very basic contact form

qterest_render_form([
    'form_title' => 'Simple contact form',
    'submit_label' => __('Send', 'text_domain')
    'fields' => [
        [
            'type' => 'text',
            'name' => 'name',
            'label' => 'Name',
            'placeholder' => 'Sven Svensson',
        ],
        [
            'type' => 'email',
            'name' => 'email', // This field is required by the plugin
            'label' => 'Email *',
            'required' => true,
            'placeholder' => 'sven.svensson@company.com',
        ],
        [
            'type' => 'textarea',
            'name' => 'message',
            'label' => 'Message',
            'placeholder' => 'Hello! I have a question about...',
        ],
        [
            'type' => 'checkbox',
            'name' => 'gdpr',
            'label' => 'I agree to the following <a href="google.se">terms</a>',
            'required' => true,
        ]
    ]
]);

Filters and Actions

There is a few filters and actions available for you to customize som things

Messages

Example for customizing messages

function theme_custom_qterest_messages($messages, $params) {

    $messages['success'] = __("Thank you! We will contact you as soon as possible!", 'text_domain');

    return $messages;

}
add_filter('qterest_contact_messages', 'theme_custom_qterest_messages', 10, 2);

The following messages can be changed

  • name_empty
  • email_empty
  • email_invalid
  • failed
  • success
  • mail_subject
  • mail_body
  • mail_to

Form submission data can be accessed through the $params parameter

Filter to remove attachments after request

You can use the following filter to remove attachments after a request

function theme_remove_attachment_after_request($remove, $params, $post_id) {
    // You may access $params or $post_id if you wish to remove attachments conditionally
    $remove = true;
    return $remove;
}
add_filter('qterest_contact_remove_attachments_after_request', 'theme_remove_attachment_after_request', 10, 3);

Formatting keys (ADMIN)

When looking at a contact request you can see that the name of the field identifies the value. This can be changed by adding a filter to display a better name for the customer. The filter name looks like this qterest_format_key_${key}.

function theme_format_keys_bulk($key) {
    return "Does this person have a dog?";
}
add_filter('qterest_format_key_has_dog', 'theme_format_key_has_dog');

You can also bulk format keys with the filter qterest_format_bulk_keys.

function theme_format_bulk_keys($key) {
    return [
        'has_dog' => "Does this person have a dog?",
        'email' => __("Email address", 'text_domain'), // TIP: You can use localization here if the site is multi language
    ]
}
add_filter('qterest_format_bulk_keys', 'theme_format_bulk_keys');

Fields

You can change the HTML of the fields with this filter. The filter name looks like this qterest_contact_field_${field_type}_html

Example of custom checkbox field

function theme_custom_checkbox_field_html($field, $args) {
    $newField = "<label for=\"$args[id]\">$args[label]</label>";
    $newField = "<input id=\"$args[id]\" type=\"$args[type]\" name=\"$args[name]\" value=\"$args[value]\"";
    return $newField;
}
add_filter('qterest_contact_field_text_html', 'theme_custom_checkbox_field_html', 2, 10);

Frontend

QTE Rest provides basic javascript to make the forms asynchronous. It also triggers events if the form submission succeeds or fails.

There is 2 events available to listen on.

  • qterestSubmitted
  • qterestError

Example usage of the events

$('.qterest-form').on('qterestSubmitted', function (event, data) {
    $('.somediv').html(`<h1>${data.success_msg}</h1>`) // qterestError contains error_msg instead
})

MailChimp

MailChimp is very esay to use with this plugin. All you have to do is to add a API key in the settings page and select the list you want the users to be added to.

Subscribe form

There is a helper function available for rendering MailChimp forms.

qterest_render_mailchimp_form(string $input_label, string $submit_label, bool $echo = false);

Example

qterest_render_mailchimp_form("Email", "Subscribe");

Filter

If you need to change the arguments of the field you can use the filter qterest_mailchimp_field_arguments.

Example usage of the filter

add_filter('qterest_mailchimp_field_arguments', 'theme_mailchimp_field_arguments');
function theme_mailchimp_field_arguments( $field ) {
    $field['type'] = 'text';
    return $field;
}

Coming soon

  • Search Documentation

Future plans

  • Form builder in React
  • Frontend form in React

This plugin is built and maintained by Noah Olsson

Releases

Newest 25 of 39 recorded releases. Each count is every asset in that release; expand a row for the breakdown.

Tag
Published
Assets
Downloads
v1.7.0 latest
Feb 21, 2024 2 years ago
qterest.zip
90
Jan 30, 2024 2 years ago
qterest.zip
10
Jun 26, 2023 3 years ago
qterest.zip
52
Jun 20, 2023 3 years ago
qterest.zip
7
Mar 16, 2023 3 years ago
qterest.zip
43
Feb 23, 2023 3 years ago
qterest.zip
17
Sep 8, 2022 4 years ago
qterest.zip
38
Jun 14, 2021 5 years ago
qterest.zip
56
Sep 25, 2020 6 years ago
qterest.zip
36
Aug 28, 2020 6 years ago
qterest.zip
6
Aug 27, 2020 6 years ago
qterest.zip
11
Aug 26, 2020 6 years ago
qterest.zip
4
Aug 26, 2020 6 years ago
qterest.zip
9
Aug 25, 2020 6 years ago
qterest.zip
9
Aug 11, 2020 6 years ago
qterest.zip
10
Jul 1, 2020 6 years ago
qterest.zip
9
Jun 30, 2020 6 years ago
qterest.zip
13
Jun 15, 2020 6 years ago
qterest.zip
10
Jun 11, 2020 6 years ago
qterest.zip
8
Jun 11, 2020 6 years ago
qterest.zip
5
May 13, 2020 6 years ago
qterest.zip
5
May 12, 2020 6 years ago
qterest.zip
4
Mar 31, 2020 6 years ago
qterest.zip
6
Mar 9, 2020 6 years ago
qterest.zip
5
Mar 9, 2020 6 years ago
qterest.zip
3
All releases on GitHub →

Active-site estimate ≈20 comes from the median of recent superseded releases. Method.

D grade

Security

QTE Rest 1.7.0 · audited by WP Registry

High-severity findings.

3 high 2 medium
Audited release
1.7.0
Findings
5
Worst severity
high
Content hash
22924fc5bd636f67f6103ef1…

Findings

  • high Unauthenticated stored XSS in Contact Requests admin metaboxes

    xss_stored

    POST /wp-json/qte/v1/contact is registered with permission_callback __return_true. handle_contact() runs SanitizeParams::sanitizeParams() which strip_tags()s values but leaves array keys untouched, then wp_insert_post() stores serialize($params) in request_content and update_post_meta(request_ip_address, get_client_ip()). get_client_ip() prefers HTTP_CLIENT_IP / HTTP_X_FORWARDED_FOR with no sanitization. When an administrator opens the resulting contact_requests post, contact_custom_box_html() unserializes request_content and echoes $formatted_key and $formatted_value into a <table> with no esc_html/esc_attr/esc_url; values whose first four characters are 'http' are placed in <a href="$value">. contact_custom_side_box_html() echoes request_ip_address raw. Practical payloads: JSON key <img src=x onerror=alert(document.domain)> (keys are not tag-stripped); a field value http://x.com" onmouseover="alert(1) (tags stripped but quotes/events are not); header X-Forwarded-For: <svg/onload=alert(1)>. This is server-rendered admin HTML, not innerHTML-only, so script/event-handler tags execute in wp-admin and can create users or install plugins. reCAPTCHA, when configured, is skipped unless g-recaptcha-response is present, so enabling captcha does not block the write.

    includes/custom-edit-page.php:46-74,111-122

    Recommendation

    Escape keys and values with esc_html() in the metabox; use esc_url() for hrefs and only treat values as links after esc_url() succeeds. Sanitize parameter keys (sanitize_key) at write time. Sanitize get_client_ip() with rest_is_ip_address()/sanitize_text_field() and echo with esc_html(). Require a valid reCAPTCHA response whenever reCAPTCHA is enabled (do not gate on isset).

  • high Authenticated (Subscriber+) arbitrary attachment download

    file_access

    FileHandler::registerHooks() adds wp_ajax_download_qte_rest_file with no nopriv twin (login required) and downloadFile() then checks only that $_GET['file_id'] is set. There is no current_user_can, no nonce, and no check that the attachment lives under uploads/qte-rest or belongs to a contact_requests parent. get_attached_file() + readfile() streams the file as application/octet-stream. Contact-form uploads are stored in qte-rest with an Apache 'Deny from all' .htaccess specifically so they are not web-accessible; this ajax action is the intended (admin-only) read path, but any Subscriber or WooCommerce customer can enumerate sequential file_id values and download those private submissions plus every other media-library file. The download URL printed in the admin metabox (getDownloadUrl) has no nonce either. Note: media_handle_upload() is passed post_data meta_query => qte_rest_file, which is not a valid wp_insert_post field, so the qte_rest_file meta used to hide files from the media modal is never actually set.

    classes/Uploads/FileHandler.php:25-28,113-141

    Recommendation

    Require current_user_can('edit_post', $attachment_id) or manage_options, verify a nonce, and refuse files whose _wp_attached_file path is not under the qte-rest directory. Cast file_id with absint(). Persist qte_rest_file via add_post_meta after media_handle_upload instead of stuffing meta_query into $post_data.

  • high Author+ stored XSS via [qterest-form] field attributes

    xss_stored

    handle_qterest_form_shortcode parses pipe-delimited shortcode content into field args (type, name, value, label, placeholder, class, options) and passes them to qterest_render_form() → render_field() / render_misc(). Those helpers interpolate $args['name'], $args['type'], $args['value'], $args['label'], $args['placeholder'], option values, and misc href/text directly into HTML attribute and element context with no esc_attr/esc_html. Authors (capability_type-level publish) and Contributors whose posts are published can plant [qterest-form] content such as type=text|name=email" onfocus="alert(document.domain)|label=Email. WordPress kses does not strip this because it is not HTML at save time; the shortcode emits a live <input> on the frontend for every visitor. Labels in the documented examples even encourage raw <a href> HTML inside label text, and render_label() concatenates $text unescaped.

    includes/form-functions.php:22-23,87-125,149-163

    Recommendation

    Escape every interpolated field: esc_attr() for name/id/class/type/placeholder/value/href attributes and esc_html() for visible label/option/misc text. Allowlisting type to the documented set (text, email, file, tel, hidden, textarea, checkbox, radio, select) prevents type= breakout. Do not treat shortcode content as trusted HTML unless the author has unfiltered_html.

  • medium Unauthenticated MailChimp audience override and forced re-subscribe

    unsafe_input

    POST /wp-json/qte/v1/mailchimp/add-subscriber is public (__return_true). After reading qterest_field_mailchimp_mail_list from options, handle_mailchimp_add_subscriber() replaces it with $params['audience_id'] when present, then interpolates that string into MailChimp::post("/lists/$mailchimpMailListID/members") using the site's API key. The DrewM client concatenates api_endpoint + '/' + $method, so audience_id is a path segment (other /lists/{id}/... methods can be aimed at). Members are created with status subscribed (skipping pending/double opt-in). If MailChimp returns title == 'Member Exists', the handler PUT-updates that email to status subscribed, re-opting an address that had unsubscribed. Observable: unauthenticated callers add or force-resubscribe arbitrary emails on any list in the connected MailChimp account (and can ignore the admin-selected list). The public shortcode form only posts email; this is a crafted JSON/form request.

    classes/Controllers/RestController.php:248-330

    Recommendation

    Ignore client-supplied audience_id (or allowlist it against lists returned by the API key). Sanitize the list id as an alphanumeric MailChimp id. Honor the list's double opt-in (status pending) and do not PUT unsubscribed members back to subscribed from a public endpoint.

  • medium reCAPTCHA is skipped when g-recaptcha-response is omitted

    auth_bypass

    is_recaptcha_enabled() is true when both site and secret keys are stored. Both public REST handlers only validate captcha when that flag is set AND $params['g-recaptcha-response'] is set. An unauthenticated client that omits the field (JSON without the key, or a direct POST) never calls Recaptcha::validateResponse and proceeds to create contact_requests, handle $_FILES, wp_mail the notification address, and hit MailChimp. Observable: the admin-configured anti-abuse control does not apply to scripted clients; it only challenges the official form.js/mailchimp.js submit buttons. Combined with the stored-XSS write path this means enabling reCAPTCHA does not require solving a challenge.

    classes/Controllers/RestController.php:102-109,254-261

    Recommendation

    When is_recaptcha_enabled(), fail closed: require a non-empty g-recaptcha-response and a successful siteverify, then return invalid_recaptcha. Do not treat a missing field as 'captcha not used'.

WP Registry hashes the installable build and reports on that exact bytes-for-bytes copy. Embargoed findings are withheld until they are disclosed, so a clean verdict means nothing public is outstanding. WP Manifest does not audit code itself.