I have implemented a security system for a WordPress site form that involves generating a unique token for every form submission. The current flow is:
When a user goes to the website with the form, several security measures kick in, including the generation of a unique token.
Once the form is submitted, a new token isn’t generated. This means that if a user made some errors or wants to re-submit for any reason, they’d need to refresh the entire page to generate a new token.
Given the following code logic for token generation, validation, and cleanup:
Database Table Creation for Storing Tokens:
function create_token_table() {
global $wpdb;
$charset_collate = $wpdb->get_charset_collate();
$sql = "CREATE TABLE IF NOT EXISTS `{$wpdb->base_prefix}CF7_unique_tokens` (
id mediumint(9) NOT NULL AUTO_INCREMENT,
token varchar(255) NOT NULL,
salt varchar(16) NOT NULL,
timestamp datetime DEFAULT '0000-00-00 00:00:00' NOT NULL,
PRIMARY KEY (id)
) $charset_collate;";
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
dbDelta($sql);
}
add_action('init', 'create_token_table');
Generating a Unique Token:
function generate_token() {
if (!function_exists('random_bytes') || !function_exists('openssl_encrypt') || !defined('AUTH_KEY')) {
return false;
}
$token = bin2hex(random_bytes(32));
$salt = random_bytes(8);
$salt_hex = bin2hex($salt);
$encrypted_token = openssl_encrypt($token, 'aes-256-cbc', AUTH_KEY, 0, $salt_hex);
global $wpdb;
$success = $wpdb->insert($wpdb->prefix . 'CF7_unique_tokens', [
'token' => $encrypted_token,
'salt' => $salt_hex,
'timestamp' => current_time('mysql'),
]);
return $success ? $encrypted_token : false;
}
Cleaning Up Old Tokens:
function cleanup_old_tokens() {
global $wpdb;
$expiration = date('Y-m-d H:i:s', strtotime('-24 hours'));
$wpdb->query($wpdb->prepare("DELETE FROM `{$wpdb->prefix}CF7_unique_tokens` WHERE timestamp < %s", $expiration));
}
add_action('init', 'cleanup_old_tokens');
Adding the Generated Token to Form as a Hidden Field:
function add_token_hidden_field($hidden_fields) {
$token = generate_token();
if (!$token) {
return $hidden_fields;
}
$hidden_fields['form_token'] = $token;
$hidden_fields['user_agent'] = $_SERVER['HTTP_USER_AGENT'];
return $hidden_fields;
}
add_filter('wpcf7_form_hidden_fields', 'add_token_hidden_field');
Validating the Submitted Token:
function validate_token_and_keywords($spam) {
global $wpdb;
if (!isset($_POST['form_token'])) {
return true;
}
$token = $_POST['form_token'];
if (!is_token_valid($token, $wpdb)) {
return true;
}
delete_token($token, $wpdb);
return $spam;
}
Deleting a Token from the Database:
function delete_token($token, $wpdb) {
$wpdb->delete("{$wpdb->prefix}CF7_unique_tokens", array('token' => $token));
}
Checking Token’s Validity:
function is_token_valid($token, $wpdb) {
$result = $wpdb->get_row($wpdb->prepare("SELECT * FROM `{$wpdb->prefix}CF7_unique_tokens` WHERE token = %s", $token));
if (!$result) {
return false;
}
return true;
}
I’d like to address two main concerns:
- How can I prevent the user from needing to refresh the entire page?
- How can I ensure that the user can submit the form again (after the first submission) without token-related issues?
In essence, I want the form to be user-friendly by allowing re-submissions without a full page refresh but also ensure it remains secure against spam or malicious attacks. Any recommendations on the best approach or adjustments to the current logic would be greatly appreciated.