Skip to content
Permalink
Browse files Browse the repository at this point in the history
🔒️ Use nonce to prevent CSRF
Closes #6
  • Loading branch information
MatzeKitt committed Apr 20, 2023
1 parent b63e1f7 commit cf0012f
Show file tree
Hide file tree
Showing 3 changed files with 153 additions and 9 deletions.
71 changes: 71 additions & 0 deletions assets/js/form.js
Expand Up @@ -6,9 +6,77 @@ document.addEventListener( 'DOMContentLoaded', () => {
const forms = document.querySelectorAll( '.wp-block-form-block-form' );

for ( const form of forms ) {
getNonce( form );
form.addEventListener( 'submit', submitForm );
}

/**
* Get a nonce via Ajax.
*
* @since 1.0.2
* @param {HTMLElement} form
*/
function getNonce( form ) {
const formData = new FormData();
const xhr = new XMLHttpRequest();

formData.set( 'action', 'form-block-create-nonce' );
formData.set( 'form_id', form.querySelector( '[name="_form_id"]' ).value );

xhr.open( 'POST', formBlockData.ajaxUrl, true );
xhr.send( formData );
xhr.onreadystatechange = () => {
if ( xhr.readyState !== 4 ) {
return;
}

if ( xhr.status === 200 || xhr.status === 201 ) {
try {
const response = JSON.parse( xhr.responseText );

if ( response.success ) {
let nonceField = form.querySelector( '[name="_wpnonce"]' );

if ( ! nonceField ) {
nonceField = document.createElement( 'input' );
nonceField.name = '_wpnonce';
nonceField.type = 'hidden';

form.appendChild( nonceField );
}

nonceField.value = response?.data?.nonce;
}
else if ( response?.data?.message ) {
// server-side error message
setSubmitMessage( form, 'error', response?.data?.message );

// disable submit button if nonce creation was not successful
const submitButton = form.querySelector( '[type="submit"]' );

if ( submitButton ) {
submitButton.disabled = true;
}
}
else {
// generic error message
setSubmitMessage( form, 'error', formBlockData.i18n.backendError );
}
}
catch ( error ) {
// invalid data from server
setSubmitMessage( form, 'error', formBlockData.i18n.backendError );
console.error( error );
}
}
else {
// request completely failed
setSubmitMessage( form, 'error', formBlockData.i18n.requestError );
console.error( xhr.responseText );
}
}
}

/**
* Submit the form.
*
Expand Down Expand Up @@ -73,6 +141,9 @@ document.addEventListener( 'DOMContentLoaded', () => {
setSubmitMessage( form, 'error', formBlockData.i18n.backendError );
console.error( error );
}

// get a new nonce for another request
getNonce( form );
}
else {
// request completely failed
Expand Down
70 changes: 67 additions & 3 deletions inc/form-data/class-data.php
Expand Up @@ -24,10 +24,40 @@ final class Data {
* Initialize the class.
*/
public function init(): void {
add_action( 'wp_ajax_form-block-create-nonce', [ $this, 'create_nonce' ] );
add_action( 'wp_ajax_form-block-submit', [ $this, 'get_request' ] );
add_action( 'wp_ajax_nopriv_form-block-create-nonce', [ $this, 'create_nonce' ] );
add_action( 'wp_ajax_nopriv_form-block-submit', [ $this, 'get_request' ] );
}

/**
* Create a nonce via Ajax.
*
* @since 1.0.2
*/
public function create_nonce(): void {
if ( empty( $_POST['form_id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
wp_send_json_error(
[
'message' => __( 'The form could not be prepared to submit requests. Please reload the page.', 'form-block' ),
]
);
}

$id = sanitize_text_field( wp_unslash( $_POST['form_id'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing

if ( ! $this->is_valid_form_id( $id ) ) {
wp_send_json_error();
}

wp_send_json_success(
[
'nonce' => wp_create_nonce( 'form_block_submit_' . $id ),
],
201
);
}

/**
* Get form data.
*
Expand Down Expand Up @@ -93,7 +123,17 @@ public static function get_instance(): Data {
* Get the request data.
*/
public function get_request(): void {
if ( ! isset( $_POST['_form_id'] ) || ! isset( $_POST['_town'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
if ( empty( $_POST['_wpnonce'] ) ) {
/**
* Fires after verifying that the nonce is empty or absent.
*/
do_action( 'form_block_empty_nonce' );

// explicitly return success so that bad actors cannot learn
wp_send_json_success();
}

if ( ! isset( $_POST['_form_id'] ) || ! isset( $_POST['_town'] ) ) {
/**
* Fires after a request is considered invalid.
*/
Expand All @@ -103,6 +143,18 @@ public function get_request(): void {
wp_send_json_success();
}

$this->form_id = sanitize_text_field( wp_unslash( $_POST['_form_id'] ) );

if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ), 'form_block_submit_' . $this->form_id ) ) {
/**
* Fires after a request has an invalid nonce.
*/
do_action( 'form_block_invalid_nonce' );

// explicitly return success so that bad actors cannot learn
wp_send_json_success();
}

if ( $this->is_honeypot_filled() ) {
/**
* Fires after a request is considered invalid due to a filled honeypot.
Expand All @@ -113,8 +165,6 @@ public function get_request(): void {
wp_send_json_success();
}

$this->form_id = sanitize_text_field( wp_unslash( $_POST['_form_id'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing

/**
* Fires before data has been validated.
*
Expand Down Expand Up @@ -196,6 +246,20 @@ private function is_honeypot_filled(): bool {
return $is_filled;
}

/**
* Check wheter a form ID is valid. That means, there are form fields stored.
*
* @since 1.0.2
*
* @param string $form_id The form ID to check
* @return bool Weter a form ID is valid
*/
public function is_valid_form_id( string $form_id ): bool {
$maybe_data = (array) get_option( 'form_block_data_' . $form_id, [] );

return ! empty( $maybe_data['fields'] );
}

/**
* Send form submission to the recipients.
*
Expand Down
21 changes: 15 additions & 6 deletions inc/form-data/class-validation.php
Expand Up @@ -16,6 +16,17 @@ final class Validation {
*/
public static $instance;

/**
* @since 1.0.2
* @var array List of field names used by the system
*/
private $system_field_names = [
'_form_id',
'_town',
'_wpnonce',
'action',
];

/**
* Validate form fields by allowed names.
*
Expand Down Expand Up @@ -128,11 +139,7 @@ private function by_attributes( $value, array $attributes ): array {
private function get_allowed_names( array $form_data ): array {
Form_Block::get_instance()->reset_block_name_attributes();

$allowed_names = [
'_form_id',
'_town',
'action',
];
$allowed_names = $this->system_field_names;

foreach ( $form_data['fields'] as $field ) {
$field_name = Form_Block::get_instance()->get_block_name_attribute( $field );
Expand Down Expand Up @@ -195,7 +202,9 @@ public function fields(): array {
}
// phpcs:enable

unset( $validated['_form_id'], $validated['action'], $validated['_town'] );
foreach ( $this->system_field_names as $name ) {
unset( $validated[ $name ] );
}

// remove empty fields
foreach ( $validated as $key => $value ) {
Expand Down

0 comments on commit cf0012f

Please sign in to comment.