Skip to content

Instantly share code, notes, and snippets.

@jeromeabel
Last active May 9, 2023 20:04
Show Gist options
  • Select an option

  • Save jeromeabel/ba0f1d0fb2128dccd7bd06bfdab8958e to your computer and use it in GitHub Desktop.

Select an option

Save jeromeabel/ba0f1d0fb2128dccd7bd06bfdab8958e to your computer and use it in GitHub Desktop.

Contact Form Guide - React / PHP

Steps to add validation and security

  • Add a Captcha reCAPTCHA to prevent bots from submitting the form
  • Add server-side validation to prevent malicious submissions or code injections
  • Use a validation library to check that the fields are correctly filled in (for example, a valid email address or a valid telephone number).
  • Avoid displaying sensitive information on the form (for example, not displaying the recipient's email address)
  • Limit the number of submissions based on IP address (for example, allow no more than 5 submissions per hour).
  • Sanitize user input to prevent code injection attacks (for example, using the filter_var() function to sanitize input).

Send HTTP POST to a PHP script and fetch JSON data

index.html

Build a simple HTML form :

<form id="myForm">
  <label for="firstName">First Name :</label>
  <input type="text" id="firstName" name="firstName" required>
  <input type="submit" value="Send">
</form>

script.js

Use Javascript to make asynchronous call to the PHP script, and get response :

const form = document.querySelector('#myForm');

form.addEventListener('submit', async (event) => {
  event.preventDefault();
  const formData = new FormData(form);
  try {
    const response = await fetch('json.php', { method: 'POST', body: formData });
    if (!response.ok) { 
      throw new Error('Une erreur s\'est produite');
    }
    const data = await response.json();
    console.log(data); // Console.log the data. You could also change the content of a HTML element
    } catch (error) { 
      console.error(error);
    }
});

json.php

The PHP script get the "firstName" field, filter it and send back HTTP Response. Note that to avoid Cross-Origin issue when calling a script outside its domain location, we use non restrictive directives. Useful for experimentation, but avoid it in production of course.

<?php
	// ! Security issue : Comment these 3 lines after testing 
	header("Access-Control-Allow-Origin: *");
	header("Access-Control-Allow-Methods: POST");
	header("Access-Control-Allow-Headers: Content-Type");

	// Get one field of the form
	$firstName = filter_input(INPUT_POST, 'firstName', FILTER_SANITIZE_STRING);
	$msg = '✌️ Message sent successfully from ' . $firstName . ' !';

	// Send JSON back
	header('HTTP/1.1 200 OK');
	header('Content-Type: application/json');
	echo json_encode(array('success' => true, 'message' => $msg));
	exit();
?>

When you upload all these files in the same folder, you can omit CORS issues.

With React / TS

You can start a project with Vite, React and TS. Write your code, and build static files inside the "dist" folder. If you put these files in the same directory of your website, it should work also unlike the previous example.

index.html generated during the build process Note, that you may change the patch of css and js files in the index.html generated to make it relative. Like this, with the "./" notation :

<script type="module" crossorigin src="./assets/index-c75a72e7.js"></script>
<link rel="stylesheet" href="./assets/index-d06c86f6.css">

Form.tsx

An example of Form with React and TypeScript

import { useState } from "react";

export default function Form() {
  const [responseMessage, setResponseMessage] = useState("");

  async function submit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault();
    const formData = new FormData(e.currentTarget);
    const response = await fetch("json.php", {
      method: "POST",
      body: formData,
    });
    const data = await response.json();
    console.log(data);
    if (data.message) {
      setResponseMessage(data.message);
    }
  }

  return (
    <form onSubmit={submit}>
      <label>
        First Name
        <input type="text" id="firstName" name="firstName" required />
      </label>
      <label>
        Email
        <input type="email" id="email" name="email" required />
      </label>
      <label>
        Message
        <textarea id="message" name="message" required />
      </label>
      <button>Send</button>
      {responseMessage && <p>{responseMessage}</p>}
    </form>
  );
}

Add reCaptcha

You might want to improve security and add reCaptcha from Google.

  • Sign up to google recaptcha
  • Choose the version you like : v2 with checkbox, v2 invisible, v3.
  • Copy public key to paste in your html/js file : YOUR_PUBLIC_KEY
  • Copy secret key to paste in your php file : YOUR_PRIVATE_KEY

For this example, I chose version 3.

index.html

<script src="https://www.google.com/recaptcha/api.js?render=YOUR_PUBLIC_KEY"></script>
<script>
	grecaptcha.ready(function() {
		grecaptcha.execute('YOUR_PUBLIC_KEY', {action: 'submit'}).then(function(token) {
			document.getElementById('g-recaptcha-response').value = token;
		});
	});
</script>

check.php : get recaptcha analysis

// reCAPTCHA v3
$url = 'https://www.google.com/recaptcha/api/siteverify';
$secretKey = 'YOUR_PRIVATE_KEY';

// Get user's reCAPTCHA v3 response and remote IP address
$recaptchaResponse = $_POST['g-recaptcha-response'];
$remoteIp = $_SERVER['REMOTE_ADDR'];

// Send a POST request to the reCAPTCHA v3 API endpoint with user's response and secret key
$data = array(
	'secret' => $secretKey,
	'response' => $recaptchaResponse,
	'remoteip' => $remoteIp
);

$options = array(
	'http' => array(
		'header' => 'Content-type: application/x-www-form-urlencoded',
		'method' => 'POST',
		'content' => http_build_query($data)
	)
);

$context = stream_context_create($options);
$response = file_get_contents($url, false, $context);

// Parse JSON response from reCAPTCHA v3 API endpoint
$jsonResponse = json_decode($response);
$score = $jsonResponse->score;

// If the reCAPTCHA v3 score is below your threshold, treat it as a failed verification
if ($score < 0.5) {
	header('HTTP/1.1 403 Forbidden');
	header('Content-Type: application/json');
	echo json_encode(array('success' => false, 'message' => 'reCAPTCHA failed'));
	exit();
}

Limit sessions

// Limit calls every 60sec.
ini_set('session.gc_maxlifetime', 60);

session_start();

if (!isset($_SESSION['submitCount'])) {
	$_SESSION['submitCount'] = 0;
}

if ($_SESSION['submitCount'] >= 2) {
		if (!isset($_SESSION['blockUntil']) || $_SESSION['blockUntil'] < time()) {
		    // Block time
		    $_SESSION['blockUntil'] = time() + 61;
		    $_SESSION['submitCount'] = 0;
		} else {
		    header('HTTP/1.1 403 Forbidden');
		    header('Content-Type: application/json');
		    echo json_encode(array('success' => false, 'message' => 'Too much sending calls. Please retry after 1 minute.'));
		    exit();
		}
} else {
		$_SESSION['submitCount']++;
}

Chek Form Fields

  //  Check form data : empty fields
	if (!isset($_POST['name']) || !isset($_POST['email']) || !isset($_POST['message'])) {
		header('HTTP/1.1 400 Bad Request');
		header('Content-Type: application/json');
		echo json_encode(array('success' => false, 'message' => 'Missing fields'));
		exit();
	}
	
	// Sanitize
	$name = filter_input(INPUT_POST, 'name', FILTER_SANITIZE_STRING);
	$email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
	$message = filter_input(INPUT_POST, 'message', FILTER_SANITIZE_STRING);

	if (!$name || !$email || !$message) {
		header('HTTP/1.1 400 Bad Request');
		header('Content-Type: application/json');
		echo json_encode(array('success' => false, 'message' => 'Invalid form data'));
		exit();
	}

Send the email and success message

 // Send the email
	$to = 'mycontact@super.com';
	$subject = 'New message from ' . $name;
	$body = "Name : $name\n\nEmail : $email\n\nMessage :\n$message";
	$headers = "From: $name <$email>";

	if (!mail($to, $subject, $body, $headers)) {
		header('HTTP/1.1 500 Internal Server Error');
		header('Content-Type: application/json');
		echo json_encode(array('success' => false, 'message' => 'An error happened while sending the message'));
		exit();
	}

	// Send the success request
	header('HTTP/1.1 200 OK');
	header('Content-Type: application/json');
	echo json_encode(array('success' => true, 'message' => 'Message sent successfully ✌️ !'));
	exit();

.HTACCESS

On Apache Web server, and on others I guess, you could add also some security directives. Like this:

# Activate mod_qos
<IfModule mod_qos.c>
    # Limit to 5 requests by second
    QS_LimitRequestBody 102400
    QS_SrvMaxConnPerIP 5
    QS_SrvMaxRequestPerSec 5
    QS_SrvMaxConnClose 10
    QS_SrvMinDataRate 150 1200
</IfModule>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Contact Form</title>
</head>
<body>
<form method="POST">
<label for="name">Name:</label>
<input type="text" name="name" id="name" required minlength="2">
<label for="email">Email:</label>
<input type="email" name="email" id="email" required minlength="4">
<label for="message">Message:</label>
<textarea name="message" id="message" required minlength="10"></textarea>
<button type="submit">Submit</button>
<input type="hidden" name="g-recaptcha-response" id="g-recaptcha-response">
</form>
<script src="https://www.google.com/recaptcha/api.js?render=YOUR_PUBLIC_KEY"></script>
<script>
grecaptcha.ready(function() {
grecaptcha.execute('YOUR_PUBLIC_KEY', {action: 'submit'}).then(function(token) {
document.getElementById('g-recaptcha-response').value = token;
});
});
</script>
</body>
</html>
<?php
// #0 - Limit calls every 60sec.
ini_set('session.gc_maxlifetime', 60);
session_start();
if (!isset($_SESSION['submitCount'])) {
$_SESSION['submitCount'] = 0;
}
if ($_SESSION['submitCount'] >= 2) {
if (!isset($_SESSION['blockUntil']) || $_SESSION['blockUntil'] < time()) {
// Block time
$_SESSION['blockUntil'] = time() + 61;
$_SESSION['submitCount'] = 0;
} else {
header('HTTP/1.1 403 Forbidden');
header('Content-Type: application/json');
echo json_encode(array('success' => false, 'message' => 'Too much sending calls. Please retry after 1 minute.'));
exit();
}
} else {
$_SESSION['submitCount']++;
}
// #1 - Check HTTP Request
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('HTTP/1.1 405 Method Not Allowed');
header('Content-Type: application/json');
echo json_encode(array('success' => false, 'message' => 'HTTP method not allowed'));
exit();
}
// #2 - reCAPTCHA v3
$url = 'https://www.google.com/recaptcha/api/siteverify';
$secretKey = 'YOUR_PRIVATE_KEY';
// Get user's reCAPTCHA v3 response and remote IP address
$recaptchaResponse = $_POST['g-recaptcha-response'];
$remoteIp = $_SERVER['REMOTE_ADDR'];
// Send a POST request to the reCAPTCHA v3 API endpoint with user's response and secret key
$data = array(
'secret' => $secretKey,
'response' => $recaptchaResponse,
'remoteip' => $remoteIp
);
$options = array(
'http' => array(
'header' => 'Content-type: application/x-www-form-urlencoded',
'method' => 'POST',
'content' => http_build_query($data)
)
);
$context = stream_context_create($options);
$response = file_get_contents($url, false, $context);
// Parse JSON response from reCAPTCHA v3 API endpoint
$jsonResponse = json_decode($response);
$score = $jsonResponse->score;
// If the reCAPTCHA v3 score is below your threshold, treat it as a failed verification
if ($score < 0.5) {
header('HTTP/1.1 403 Forbidden');
header('Content-Type: application/json');
echo json_encode(array('success' => false, 'message' => 'reCAPTCHA failed'));
exit();
}
// #3 - Check form data
if (!isset($_POST['name']) || !isset($_POST['email']) || !isset($_POST['message'])) {
header('HTTP/1.1 400 Bad Request');
header('Content-Type: application/json');
echo json_encode(array('success' => false, 'message' => 'Missing fields'));
exit();
}
// #4 - Sanitize
$name = filter_input(INPUT_POST, 'name', FILTER_SANITIZE_STRING);
$email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
$message = filter_input(INPUT_POST, 'message', FILTER_SANITIZE_STRING);
if (!$name || !$email || !$message) {
header('HTTP/1.1 400 Bad Request');
header('Content-Type: application/json');
echo json_encode(array('success' => false, 'message' => 'Invalid form data'));
exit();
}
// #5 - Send the email
$to = 'contact@test.com'; // Your Email
$subject = 'New message from ' . $name;
$body = "Name : $name\n\nEmail : $email\n\nMessage :\n$message";
$headers = "From: $name <$email>";
if (!mail($to, $subject, $body, $headers)) {
header('HTTP/1.1 500 Internal Server Error');
header('Content-Type: application/json');
echo json_encode(array('success' => false, 'message' => 'An error happened while sending the message'));
exit();
}
// #6 - Send the success request
header('HTTP/1.1 200 OK');
header('Content-Type: application/json');
echo json_encode(array('success' => true, 'message' => 'Message sent successfully ✌️ !'));
exit();
?>
# Activation de mod_qos
<IfModule mod_qos.c>
# Limite à 5 requêtes par seconde
QS_LimitRequestBody 102400
QS_SrvMaxConnPerIP 5
QS_SrvMaxRequestPerSec 5
QS_SrvMaxConnClose 10
QS_SrvMinDataRate 150 1200
</IfModule>
//Empêcher l'accès direct aux fichiers sensibles :
<FilesMatch "\.(htaccess|htpasswd|php|inc|config)$">
Order Allow,Deny
Deny from all
</FilesMatch>
//Activer la protection CSRF (Cross-Site Request Forgery).
//Vérifier que les requêtes POST proviennent du domaine autorisé,
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{REQUEST_METHOD} POST
RewriteCond %{HTTP_REFERER} !^https://(www\.)?mondomaine\.com [NC]
RewriteRule .* - [F,L]
</IfModule>
// Activer la protection XSS
//active la protection XSS (Cross-Site Scripting) en ajoutant l'en-tête X-XSS-Protection à toutes les réponses HTTP envoyées par le serveur
<IfModule mod_headers.c>
Header set X-XSS-Protection "1; mode=block"
</IfModule>
//Désactiver la liste des répertoires :
Options -Indexes
//Activer la redirection HTTPS
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
</IfModule>
# Désactiver l'affichage des erreurs PHP
php_flag display_errors off
# Désactiver l'affichage des informations sur le serveur
ServerSignature Off
ServerTokens Prod
# Empêcher le hotlinking (liens directs vers les fichiers)
RewriteEngine On
RewriteCond %{HTTP_REFERER} !^$
RewriteCond %{HTTP_REFERER} !^http(s)?://(www\.)?monsite.com [NC]
RewriteRule \.(jpg|jpeg|png|gif|pdf)$ - [NC,F,L]
# Empêcher l'exécution de scripts malveillants
AddHandler cgi-script .php .pl .py .jsp .asp .htm .shtml .sh .cgi
Options -ExecCGI

Token CSRF

index.php Generate a random token inside the form

<form>
  <?php
    session_start();
    $token = bin2hex(random_bytes(32));
    $_SESSION['csrf_token'] = $token;
  ?>
  <input type="hidden" name="csrf_token" value="<?php echo $token ?>">
</form>

check.php Check the token

  if (!isset($_POST['csrf_token']) || $_POST['csrf_token'] !== $_SESSION['csrf_token']) {
    // Jeton CSRF manquant ou invalide
    header('HTTP/1.1 400 Bad Request');
    echo 'Erreur : jeton CSRF manquant ou invalide.';
    exit();
  }
// Récupération des éléments HTML
const form = document.getElementById('contact-form');
const nameInput = document.getElementById('name');
const emailInput = document.getElementById('email');
const messageInput = document.getElementById('message');
// Validation des données à la soumission du formulaire
form.addEventListener('submit', (event) => {
// Empêcher l'envoi du formulaire par défaut
event.preventDefault();
// Validation du nom
if (nameInput.value.trim() === '') {
alert('Veuillez saisir votre nom');
nameInput.focus();
return;
}
// Validation de l'email
const email = emailInput.value.trim();
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
alert('Veuillez saisir une adresse email valide');
emailInput.focus();
return;
}
// Validation du message
if (messageInput.value.trim() === '') {
alert('Veuillez saisir un message');
messageInput.focus();
return;
}
// Soumission du formulaire si toutes les validations sont passées
form.submit();
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment