Skip to content

Commit 90d968c

Browse files
author
gauthier witkowski
committed
Enhance reCAPTCHA validation
1 parent 2305f36 commit 90d968c

File tree

1 file changed

+39
-22
lines changed

1 file changed

+39
-22
lines changed

AjaxForm.php

Lines changed: 39 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
*
1414
* @author Raspgot <contact@raspgot.fr>
1515
* @link https://github.com/raspgot/AjaxForm-PHPMailer-reCAPTCHA
16-
* @version 1.7.2
16+
* @version 1.7.3
1717
* @see https://github.com/PHPMailer/PHPMailer
1818
* @see https://developers.google.com/recaptcha/docs/v3
1919
*/
@@ -33,9 +33,9 @@
3333
use PHPMailer\PHPMailer\Exception;
3434

3535
// Load PHPMailer manually (no-Composer project)
36-
require_once 'PHPMailer/PHPMailer.php';
37-
require_once 'PHPMailer/SMTP.php';
38-
require_once 'PHPMailer/Exception.php';
36+
require_once __DIR__ . '/PHPMailer/PHPMailer.php';
37+
require_once __DIR__ . '/PHPMailer/SMTP.php';
38+
require_once __DIR__ . '/PHPMailer/Exception.php';
3939

4040
// Configuration constants (must be customized)
4141
const SECRET_KEY = ''; // Your reCAPTCHA secret key
@@ -48,8 +48,8 @@
4848
const FROM_NAME = 'Raspgot'; // Name displayed as sender
4949
const EMAIL_SUBJECT_DEFAULT = '[GitHub] New message received'; // Default subject if none provided
5050
const EMAIL_SUBJECT_AUTOREPLY = 'We have received your message'; // Subject of auto-reply email
51-
const MAX_ATTEMPTS = 55; // Maximum allowed submissions per session
52-
const RATE_LIMIT_DURATION = 3600; // 30 minutes
51+
const MAX_ATTEMPTS = 5; // Maximum allowed submissions per session
52+
const RATE_LIMIT_DURATION = 3600; // 1 hour rate limit (in seconds)
5353

5454
// User-facing response messages
5555
const RESPONSES = [
@@ -127,7 +127,10 @@
127127
$mail->addReplyTo($email, $name);
128128
$mail->Subject = $subject ?: EMAIL_SUBJECT_DEFAULT;
129129
$mail->Body = $emailBody;
130-
$mail->AltBody = strip_tags($emailBody);
130+
// First convert <br> tags into \n (strip_tags removes <br> without adding line breaks)
131+
$alt = preg_replace('/<br\s*\/?>/i', "\n", $emailBody);
132+
$alt = trim(strip_tags($alt));
133+
$mail->AltBody = $alt;
131134
$mail->send();
132135

133136
// Send confirmation auto-reply to user
@@ -137,9 +140,9 @@
137140
$autoReply->Subject = EMAIL_SUBJECT_AUTOREPLY . '' . $subject;
138141
$autoReply->Body = '
139142
<p>Hello ' . htmlspecialchars($name) . ',</p>
140-
<p>Thank you for reaching out. Here is a copy of your message:</p>
143+
<p>Thank you for reaching out, here is a copy of your message :</p>
141144
<hr>' . $emailBody;
142-
$autoReply->AltBody = strip_tags($emailBody);
145+
$autoReply->AltBody = $alt;
143146
$autoReply->send();
144147

145148
respond(true, RESPONSES['success']);
@@ -148,9 +151,9 @@
148151
}
149152

150153
/**
151-
* Verifies reCAPTCHA token with Google API and checks score.
154+
* Verifies reCAPTCHA token with Google API and checks score, action & hostname
152155
*
153-
* @param string $token reCAPTCHA token submitted by the form.
156+
* @param string $token reCAPTCHA token submitted by the form
154157
* @return void
155158
*/
156159
function validateRecaptcha(string $token): void
@@ -161,6 +164,7 @@ function validateRecaptcha(string $token): void
161164
$postFields = http_build_query([
162165
'secret' => SECRET_KEY,
163166
'response' => $token,
167+
'remoteip' => $_SERVER['REMOTE_ADDR'] ?? null,
164168
]);
165169

166170
curl_setopt_array($ch, [
@@ -170,34 +174,47 @@ function validateRecaptcha(string $token): void
170174
CURLOPT_TIMEOUT => 10,
171175
]);
172176

173-
$response = curl_exec($ch);
174-
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
177+
$response = curl_exec($ch);
178+
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
175179
$curlError = curl_error($ch);
176180
curl_close($ch);
177181

178-
// Handle HTTP or cURL error
182+
// If cURL execution failed (network error, timeout, etc.)
179183
if ($response === false) {
180184
respond(false, '❌ reCAPTCHA request failed : ' . ($curlError ?: 'Unknown cURL error.'));
181185
}
182186

187+
// If Google did not respond with a 200 OK
183188
if ($httpCode !== 200) {
184189
respond(false, '❌ reCAPTCHA HTTP error : ' . $httpCode);
185190
}
186191

187-
// Parse JSON response
188192
$data = json_decode($response, true);
189193

194+
// If response cannot be decoded as valid JSON
190195
if (json_last_error() !== JSON_ERROR_NONE || !is_array($data)) {
191196
respond(false, '❌ Invalid JSON response from reCAPTCHA.');
192197
}
193198

194-
// Check success flag
199+
// If Google says "success" flag is false (invalid token, wrong secret, expired, etc.)
195200
if (empty($data['success'])) {
196201
$errors = isset($data['error-codes']) ? implode(', ', $data['error-codes']) : 'Unknown error.';
197202
respond(false, '❌ reCAPTCHA verification failed : ' . $errors);
198203
}
199204

200-
// Check score (threshold configurable if needed)
205+
// If the "action" does not match the one expected (mitigates token reuse across forms)
206+
$expectedAction = 'submit';
207+
if (($data['action'] ?? '') !== $expectedAction) {
208+
respond(false, '❌ reCAPTCHA action mismatch.');
209+
}
210+
211+
// If the hostname returned by Google does not match our server's hostname (prevents token theft)
212+
$expectedHost = $_SERVER['SERVER_NAME'] ?? '';
213+
if (!empty($expectedHost) && ($data['hostname'] ?? '') !== $expectedHost) {
214+
respond(false, '❌ reCAPTCHA hostname mismatch.');
215+
}
216+
217+
// If the score is below threshold (Google thinks the request looks like a bot)
201218
$score = $data['score'] ?? 1.0;
202219
if ($score < 0.6) {
203220
respond(false, '❌ Low reCAPTCHA score (' . $score . '). You might be a robot.');
@@ -220,13 +237,13 @@ function sanitize(string $data): string
220237
}
221238

222239
/**
223-
* Sends a JSON response and terminates the script.
240+
* Sends a JSON response and terminates the script
224241
*
225-
* @param bool $success Whether the operation was successful.
226-
* @param string $message Message to be displayed to the user.
227-
* @param string|null $field Optional field name to mark as invalid.
242+
* @param bool $success Whether the operation was successful
243+
* @param string $message Message to be displayed to the user
244+
* @param string|null $field Optional field name to mark as invalid
228245
*
229-
* @return never This function does not return; it ends execution with exit().
246+
* @return never This function does not return; it ends execution with exit()
230247
*/
231248
function respond(bool $success, string $message, ?string $field = null): never
232249
{

0 commit comments

Comments
 (0)