CVE-2023-30616 (PHP, published 2023-04-20T18:15Z)
Description: value: Form block is a wordpress plugin designed to make form creation easier. Versions prior to 1.0.2 are subject to a Cross-Site Request Forgery due to a missing nonce check. There is potential for a Cross Site Request Forgery for all form blocks, since it allows to send requests to the forms from any website without a user noticing. Users are advised to upgrade to version 1.0.2. There are no known workarounds for this vulnerability.
Severity: nan (CVSS v3 base score: 6.5)
Weakness: CWE-352 \u2014 Cross-Site Request Forgery (CSRF)
The web application does not, or can not, sufficiently verify whether a well-formed, valid, consistent request was intentionally provided by the user who submitted the request.
Repository: https://github.com/epiphyt/form-block
Files changed:
assets/js/form.js
inc/form-data/class-data.php
inc/form-data/class-validation.php
Commit message: 🔒️ Use nonce to prevent CSRF
Closes #6


Vulnerable code:
		if ( ! isset( $_POST['_form_id'] ) || ! isset( $_POST['_town'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
		$this->form_id = sanitize_text_field( wp_unslash( $_POST['_form_id'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
		
		$allowed_names = [
			'_form_id',
			'_town',
			'action',
		];
		unset( $validated['_form_id'], $validated['action'], $validated['_town'] );

Fixed code:
		getNonce( form );
	/**
	 * 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 );
			}
		}
	}
	
					
					// get a new nonce for another request
					getNonce( form );
		add_action( 'wp_ajax_form-block-create-nonce', [ $this, 'create_nonce' ] );
		add_action( 'wp_ajax_nopriv_form-block-create-nonce', [ $this, 'create_nonce' ] );
	/**
	 * 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
		);
	}
	
		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'] ) ) {
		$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();
		}
		
	/**
	 * 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'] );
	}
	
	/**
	 * @since	1.0.2
	 * @var		array List of field names used by the system
	 */
	private $system_field_names = [
		'_form_id',
		'_town',
		'_wpnonce',
		'action',
	];
	
		$allowed_names = $this->system_field_names;
		foreach ( $this->system_field_names as $name ) {
			unset( $validated[ $name ] );
		}