Table of contents
Your contact page or your newsletter form is all set, ready for new visitors.
You launch your website, and after a few days here comes the drama: 1000 form submissions, all by the same bot activity.
I know I’m not that famous to reach those numbers 🫠 Let’s check the my dashboard one second…
What a funny guy that RobertMow and his 990 SUBMISSIONS
You know where this is going: let’s secure our endpoint!
In this tutorial we’ll go over 7 tips and their implementation to prevent bot form submissions.
Flag bots IP addresses
When an activity can only be a bot, we can flag IP addresses by storing them as malicious. The storage mechanism depends on your stack:
On Cloudflare Workers, use KV (key-value datastore)
On any other backend, use server-side session
We’ll see how we can notice a bot activity in the next steps.
Once the ip address if flagged, ignore any form sumbit:
<span <span class="hljs-keyword">class</span>=<span class="hljs-string">"hljs-comment"</span>><span class="hljs-comment">// prevent bots from re-submitting a form</span></span>
<span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-keyword"</span>></span>const<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span> <span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-variable constant_"</span>></span>KV<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span> = context.<span <span class="hljs-keyword">class</span>=<span class="hljs-string">"hljs-property"</span>>cloudflare<<span class="hljs-regexp">/span>.<span class="hljs-property">env</</span>span>.<span <span class="hljs-keyword">class</span>=<span class="hljs-string">"hljs-property"</span>><span class="hljs-variable constant_">KV</span></span>;
<span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-keyword"</span>></span>const<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span> ipKey = <span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-string"</span>></span>`contact_ip_<span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-subst"</span>></span>${clientIP}<span class="hljs-tag"></<span class="hljs-name">span</span>></span>`<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span>;
<span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-keyword"</span>></span>if<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span> (<span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-keyword"</span>></span>await<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span> <span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-variable constant_"</span>></span>KV<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span>.<span <span class="hljs-keyword">class</span>=<span class="hljs-string">"hljs-title function_"</span>>get</span>(ipKey)) {
<span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-variable language_"</span>></span>console<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span>.<span <span class="hljs-keyword">class</span>=<span class="hljs-string">"hljs-title function_"</span>>warn<<span class="hljs-regexp">/span>(<span class="hljs-string">`IP <span class="hljs-subst">${clientIP}</</span>span> has been flagged <span class="hljs-keyword">as</span> a bot<span class="hljs-string">`</span>);
<span class="hljs-keyword">return</span> <span class="hljs-title class_">Response</span>.<span class="hljs-title function_">json</span>({ <span class="hljs-attr">success</span>: <span class="hljs-literal">true</span> });
}</span>Prevent bypassing client-side validation
If not alrejady done, please ensure your form is validated on client-side.
In addition, if the value sent to the server isn’t validated, that means client-side validation has been bypassed and should be flagged as malicious.
<span <span class="hljs-keyword">class</span>=<span class="hljs-string">"hljs-keyword"</span>><span class="hljs-keyword">if</span><<span class="hljs-regexp">/span> (!(<span class="hljs-keyword">await</</span>span> schema.<span <span class="hljs-keyword">class</span>=<span class="hljs-string">"hljs-title function_"</span>>validate</span>(data))) {
<span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-variable language_"</span>></span>console<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span>.<span <span class="hljs-keyword">class</span>=<span class="hljs-string">"hljs-title function_"</span>>warn<<span class="hljs-regexp">/span>(<span class="hljs-string">&quot;Client-side form validation bypassed&quot;</</span>span>);
<span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-keyword"</span>></span>await<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span> <span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-variable constant_"</span>></span>KV<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span>.<span <span class="hljs-keyword">class</span>=<span class="hljs-string">"hljs-title function_"</span>>put<<span class="hljs-regexp">/span>(ipKey, <span class="hljs-string">&quot;bot&quot;</</span>span>, { <span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-attr"</span>></span>expirationTtl<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span>: <span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-number"</span>></span>86400<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span> }); <span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-comment"</span>></span>// 24 hours cooldown<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span>
<span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-keyword"</span>></span>return<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span> <span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-title class_"</span>></span>Response<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span>.<span <span class="hljs-keyword">class</span>=<span class="hljs-string">"hljs-title function_"</span>>json<<span class="hljs-regexp">/span>({ <span class="hljs-attr">success</</span>span>: <span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-literal"</span>></span>true<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span> });
}Use a client nonce
To prevent replay attacks, we generate a unique nonce for each form submission. This nonce is stored on backend's side and compared against the nonce provided by the client.
<span <span class="hljs-keyword">class</span>=<span class="hljs-string">"hljs-comment"</span>><span class="hljs-comment">// generate nonce on contact form page render</span></span>
<span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-keyword"</span>></span>const<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span> timestamp = <span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-title class_"</span>></span>Date<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span>.<span <span class="hljs-keyword">class</span>=<span class="hljs-string">"hljs-title function_"</span>>now</span>();
<span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-keyword"</span>></span>const<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span> randomPart = <span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-title class_"</span>></span>Math<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span>.<span <span class="hljs-keyword">class</span>=<span class="hljs-string">"hljs-title function_"</span>>random<<span class="hljs-regexp">/span>().<span class="hljs-title function_">toString</</span>span>(<span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-number"</span>></span>36<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span>).<span <span class="hljs-keyword">class</span>=<span class="hljs-string">"hljs-title function_"</span>>substring<<span class="hljs-regexp">/span>(<span class="hljs-number">2</</span>span>, <span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-number"</span>></span>15<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span>);
<span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-keyword"</span>></span>const<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span> nonce = <span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-string"</span>></span>`<span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-subst"</span>></span>${timestamp}<span class="hljs-tag"></<span class="hljs-name">span</span>></span>.<span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-subst"</span>></span>${randomPart}<span class="hljs-tag"></<span class="hljs-name">span</span>></span>`<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span>;
<span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-keyword"</span>></span>await<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span> <span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-variable constant_"</span>></span>KV<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span>.<span <span class="hljs-keyword">class</span>=<span class="hljs-string">"hljs-title function_"</span>>put<<span class="hljs-regexp">/span>(nonceKey, <span class="hljs-string">&quot;unused&quot;</</span>span>)<span <span class="hljs-keyword">class</span>=<span class="hljs-string">"hljs-comment"</span>><span class="hljs-comment">/**
* Validates if a nonce is valid and not expired
* <span class="hljs-doctag"><span class="hljs-doctag">@param</span></span> nonce The nonce to validate
* <span class="hljs-doctag"><span class="hljs-doctag">@param</span></span> maxAge Maximum age of the nonce in milliseconds (default: 1 hour)
* <span class="hljs-doctag"><span class="hljs-doctag">@returns</span></span> Boolean indicating if the nonce is still valid
*/</span></span>
<span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-keyword"</span>></span>export<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span> <span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-keyword"</span>></span>function<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span> <span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-title function_"</span>></span>isNonceValid<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span>(<span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-params"</span>></span><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-attr"</span>></span>nonce<span class="hljs-tag"></<span class="hljs-name">span</span>></span>: <span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-built_in"</span>></span>string<span class="hljs-tag"></<span class="hljs-name">span</span>></span>, <span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-attr"</span>></span>maxAge<span class="hljs-tag"></<span class="hljs-name">span</span>></span>: <span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-built_in"</span>></span>number<span class="hljs-tag"></<span class="hljs-name">span</span>></span> = <span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-number"</span>></span>3600000<span class="hljs-tag"></<span class="hljs-name">span</span>></span><span class="hljs-tag"></<span class="hljs-name">span</span>></span></span>): <span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-built_in"</span>></span>boolean<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span> {
<span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-keyword"</span>></span>try<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span> {
<span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-keyword"</span>></span>const<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span> parts = nonce.<span <span class="hljs-keyword">class</span>=<span class="hljs-string">"hljs-title function_"</span>>split<<span class="hljs-regexp">/span>(<span class="hljs-string">&quot;.&quot;</</span>span>);
<span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-keyword"</span>></span>if<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span> (parts.<span <span class="hljs-keyword">class</span>=<span class="hljs-string">"hljs-property"</span>>length<<span class="hljs-regexp">/span> !== <span class="hljs-number">2</</span>span>) <span <span class="hljs-keyword">class</span>=<span class="hljs-string">"hljs-keyword"</span>><span class="hljs-keyword">return</span><<span class="hljs-regexp">/span> <span class="hljs-literal">false</</span>span>;
<span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-keyword"</span>></span>const<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span> timestamp = <span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-built_in"</span>></span>parseInt<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span>(parts[<span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-number"</span>></span>0<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span>], <span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-number"</span>></span>10<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span>);
<span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-keyword"</span>></span>const<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span> now = <span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-title class_"</span>></span>Date<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span>.<span <span class="hljs-keyword">class</span>=<span class="hljs-string">"hljs-title function_"</span>>now</span>();
<span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-comment"</span>></span>// Check if nonce has expired<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span>
<span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-keyword"</span>></span>return<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span> now - timestamp &lt;= maxAge;
} <span <span class="hljs-keyword">class</span>=<span class="hljs-string">"hljs-keyword"</span>><span class="hljs-keyword">catch</span></span> (error) {
<span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-keyword"</span>></span>return<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span> <span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-literal"</span>></span>false<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span>;
}
}
...
<span <span class="hljs-keyword">class</span>=<span class="hljs-string">"hljs-keyword"</span>><span class="hljs-keyword">if</span><<span class="hljs-regexp">/span> (!<span class="hljs-title function_">isNonceValid</</span>span>(nonce)) {
<span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-variable language_"</span>></span>console<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span>.<span <span class="hljs-keyword">class</span>=<span class="hljs-string">"hljs-title function_"</span>>warn<<span class="hljs-regexp">/span>(<span class="hljs-string">&quot;Expired nonce detected&quot;</</span>span>);
<span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-keyword"</span>></span>return<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span> <span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-title class_"</span>></span>Response<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span>.<span <span class="hljs-keyword">class</span>=<span class="hljs-string">"hljs-title function_"</span>>json</span>({
<span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-attr"</span>></span>success<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span>: <span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-literal"</span>></span>false<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span>,
<span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-attr"</span>></span>error<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span>: <span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-title function_"</span>></span>t<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span>(<span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-string"</span>></span><span class="hljs-symbol">&quot;</span>contact.response.error.sessionExpired<span class="hljs-symbol">&quot;</span><span class="hljs-tag"></<span class="hljs-name">span</span>></span></span>),
});
}
<span <span class="hljs-keyword">class</span>=<span class="hljs-string">"hljs-keyword"</span>><span class="hljs-keyword">const</span><<span class="hljs-regexp">/span> nonceKey = <span class="hljs-string">`contact_nonce_<span class="hljs-subst">${nonce}</</span>span><span class="hljs-string">`</span>;
<span class="hljs-keyword">const</span> nonceState = <span class="hljs-keyword">await</span> <span class="hljs-variable constant_">KV</span>.<span class="hljs-title function_">get</span>(nonceKey);
<span class="hljs-keyword">if</span> (nonceState === <span class="hljs-string">&quot;unused&quot;</span>) {
<span class="hljs-keyword">await</span> <span class="hljs-variable constant_">KV</span>.<span class="hljs-title function_">put</span>(nonceKey, <span class="hljs-string">&quot;used&quot;</span>);
} <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (nonceState === <span class="hljs-string">&quot;used&quot;</span>) {
<span class="hljs-variable language_">console</span>.<span class="hljs-title function_">warn</span>(<span class="hljs-string">&quot;Duplicate form submission&quot;</span>);
<span class="hljs-keyword">return</span> <span class="hljs-title class_">Response</span>.<span class="hljs-title function_">json</span>(
{
<span class="hljs-attr">success</span>: <span class="hljs-literal">false</span>,
<span class="hljs-attr">error</span>: <span class="hljs-string">&quot;The form has already been submitted&quot;</span>
},
{ <span class="hljs-attr">status</span>: <span class="hljs-number">409</span>, <span class="hljs-attr">headers</span>: { <span class="hljs-string">&quot;Content-Type&quot;</span>: <span class="hljs-string">&quot;application/json&quot;</span> } }
);
} <span class="hljs-keyword">else</span> {
<span class="hljs-variable language_">console</span>.<span class="hljs-title function_">warn</span>(<span class="hljs-string">&quot;Invalid nonce&quot;</span>);
<span class="hljs-keyword">return</span> <span class="hljs-title class_">Response</span>.<span class="hljs-title function_">json</span>(
{
<span class="hljs-attr">success</span>: <span class="hljs-literal">false</span>,
<span class="hljs-attr">error</span>: <span class="hljs-string">&quot;Invalid nonce submitted&quot;</span>
},
{ <span class="hljs-attr">status</span>: <span class="hljs-number">409</span>, <span class="hljs-attr">headers</span>: { <span class="hljs-string">&quot;Content-Type&quot;</span>: <span class="hljs-string">&quot;application/json&quot;</span> } }
);
}</span>Cloudflare Rate Limiting
We use Cloudflare's Rate Limiting API to restrict the number of form submissions from a single IP address. Note 1: This is not perfect as users on mobile networks often have the same IP address. Note 2: The rate limiting API is still in open beta (August 2025).
<span <span class="hljs-keyword">class</span>=<span class="hljs-string">"hljs-comment"</span>><span class="hljs-comment">// Implementation in index.tsx</span></span>
<span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-keyword"</span>></span>const<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span> rateLimiter = context.<span <span class="hljs-keyword">class</span>=<span class="hljs-string">"hljs-property"</span>>cloudflare<<span class="hljs-regexp">/span>.<span class="hljs-property">env</</span>span>.<span <span class="hljs-keyword">class</span>=<span class="hljs-string">"hljs-property"</span>><span class="hljs-variable constant_">RATE_LIMITER_CONTACT</span></span>;
<span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-keyword"</span>></span>const<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span> clientIP = request.<span <span class="hljs-keyword">class</span>=<span class="hljs-string">"hljs-property"</span>>headers<<span class="hljs-regexp">/span>.<span class="hljs-title function_">get</</span>span>(<span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-string"</span>></span><span class="hljs-symbol">&quot;</span>CF-Connecting-IP<span class="hljs-symbol">&quot;</span><span class="hljs-tag"></<span class="hljs-name">span</span>></span></span>) || <span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-string"</span>></span><span class="hljs-symbol">&quot;</span><span class="hljs-symbol">&quot;</span><span class="hljs-tag"></<span class="hljs-name">span</span>></span></span>;
<span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-keyword"</span>></span>const<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span> rateLimitResult = <span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-keyword"</span>></span>await<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span> rateLimiter.<span <span class="hljs-keyword">class</span>=<span class="hljs-string">"hljs-title function_"</span>>limit<<span class="hljs-regexp">/span>({ <span class="hljs-attr">key</</span>span>: <span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-string"</span>></span>`contact_form_<span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-subst"</span>></span>${clientIP}<span class="hljs-tag"></<span class="hljs-name">span</span>></span>`<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span>});
<span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-keyword"</span>></span>if<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span> (!rateLimitResult.<span <span class="hljs-keyword">class</span>=<span class="hljs-string">"hljs-property"</span>>success</span>) {
<span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-comment"</span>></span>// Return 429 Too Many Requests<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span>
}The Rate Limiter needs to be bound to your Cloudflare Worker environment. This is typically done in your wrangler.toml file:
<span <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-section"</span>>[env.production]</span>
<span <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-attr"</span>>kv_namespaces</span> = [
{ binding = <span class=<span class="hljs-string">"hljs-string"</span>>&quot<span class="hljs-comment">;RATE_LIMITER_CONTACT&quot;</span>, id = <span class="hljs-string">&quot;unique-namespace-id&quot;</span> }</span>
]Honeypot Field
A hidden field called "website" is included in the form. This field is invisible to human users but will likely be filled out by bots. If the field contains any data, the submission is silently rejected.
<span <span class="hljs-keyword">class</span>=<span class="hljs-string">"hljs-comment"</span>><span class="hljs-comment">// Check honeypot field - if it contains data, it&#x27;s likely a bot</span></span>
<span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-keyword"</span>></span>const<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span> honeypotField = data.<span <span class="hljs-keyword">class</span>=<span class="hljs-string">"hljs-property"</span>>website<<span class="hljs-regexp">/span>?.<span class="hljs-title function_">toString</</span>span>();
<span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-keyword"</span>></span>if<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span> (honeypotField) {
<span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-variable language_"</span>></span>console<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span>.<span <span class="hljs-keyword">class</span>=<span class="hljs-string">"hljs-title function_"</span>>warn<<span class="hljs-regexp">/span>(<span class="hljs-string">&quot;Honeypot field triggered&quot;</</span>span>);
<span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-keyword"</span>></span>await<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span> <span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-variable constant_"</span>></span>KV<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span>.<span <span class="hljs-keyword">class</span>=<span class="hljs-string">"hljs-title function_"</span>>put<<span class="hljs-regexp">/span>(ipKey, <span class="hljs-string">&quot;bot&quot;</</span>span>, { <span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-attr"</span>></span>expirationTtl<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span>: <span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-number"</span>></span>86400<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span> }); <span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-comment"</span>></span>// 24 hours cooldown <span class="hljs-tag"></<span class="hljs-name">span</span>></span></span>
<span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-comment"</span>></span>// Return success to avoid giving bots feedback, but don<span class="hljs-symbol">&#x27;</span>t process the form<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span>
<span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-keyword"</span>></span>return<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span> <span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-title class_"</span>></span>Response<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span>.<span <span class="hljs-keyword">class</span>=<span class="hljs-string">"hljs-title function_"</span>>json<<span class="hljs-regexp">/span>({ <span class="hljs-attr">success</</span>span>: <span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-literal"</span>></span>true<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span> });
}CAPTCHA Integration (hCaptcha)
The form includes an hCaptcha challenge to verify that the user is human. We use the official React component from @hcaptcha/react-hcaptcha.
<span <span class="hljs-keyword">class</span>=<span class="hljs-string">"hljs-comment"</span>><span class="hljs-comment">// In the form component</span></span>
&lt;<span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-title class_"</span>></span>HCaptcha<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span>
ref={captchaRef}
sitekey={process.<span <span class="hljs-keyword">class</span>=<span class="hljs-string">"hljs-property"</span>>env<<span class="hljs-regexp">/span>.<span class="hljs-property">HCAPTCHA_SITE_KEY</</span>span> || <span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-string"</span>></span><span class="hljs-symbol">&quot;</span>10000000-ffff-ffff-ffff-000000000001<span class="hljs-symbol">&quot;</span><span class="hljs-tag"></<span class="hljs-name">span</span>></span></span>}
onVerify={<span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-function"</span>></span>(<span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-params"</span>></span>token<span class="hljs-tag"></<span class="hljs-name">span</span>></span>) =<span class="hljs-symbol">&gt;</span><span class="hljs-tag"></<span class="hljs-name">span</span>></span></span> <span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-title function_"</span>></span>setCaptchaToken<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span>(token)}
/&gt;
<span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-comment"</span>></span>// In the submission handling<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span>
<span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-keyword"</span>></span>const<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span> captchaResult = <span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-keyword"</span>></span>await<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span> <span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-title function_"</span>></span>validateCaptcha<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span>(data.<span <span class="hljs-keyword">class</span>=<span class="hljs-string">"hljs-property"</span>>hCaptchaToken<<span class="hljs-regexp">/span>.<span class="hljs-title function_">toString</</span>span>());
<span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-keyword"</span>></span>if<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span> (!captchaResult.<span <span class="hljs-keyword">class</span>=<span class="hljs-string">"hljs-property"</span>>success</span>) {
<span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-keyword"</span>></span>return<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span> <span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-title class_"</span>></span>Response<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span>.<span <span class="hljs-keyword">class</span>=<span class="hljs-string">"hljs-title function_"</span>>json</span>({
<span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-attr"</span>></span>success<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span>: <span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-literal"</span>></span>false<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span>,
<span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-attr"</span>></span>error<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span>: <span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-string"</span>></span><span class="hljs-symbol">&quot;</span>CAPTCHA verification failed<span class="hljs-symbol">&quot;</span><span class="hljs-tag"></<span class="hljs-name">span</span>></span></span>,
});
}hCaptcha requires a site key and secret key to be set as environment variables in the .env file (locally) and in your wrangler.jsonc file (on Cloudflare Workers).
HCAPTCHA_SITE_KEY- Your hCaptcha site key for frontend integrationHCAPTCHA_SECRET_KEY- Your hCaptcha secret key for verification
Email Validation with Mailchecker
We use the mailchecker library to validate email addresses and detect disposable email services. It is backed by a database of over 55,000 throwable email domains.
<span <span class="hljs-keyword">class</span>=<span class="hljs-string">"hljs-comment"</span>><span class="hljs-comment">// Validate email with mailchecker</span></span>
<span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-keyword"</span>></span>const<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span> email = data.<span <span class="hljs-keyword">class</span>=<span class="hljs-string">"hljs-property"</span>>email<<span class="hljs-regexp">/span>?.<span class="hljs-title function_">toString</</span>span>() || <span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-string"</span>></span><span class="hljs-symbol">&quot;</span><span class="hljs-symbol">&quot;</span><span class="hljs-tag"></<span class="hljs-name">span</span>></span></span>;
<span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-keyword"</span>></span>if<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span> (email &amp;&amp; !mailchecker.<span <span class="hljs-keyword">class</span>=<span class="hljs-string">"hljs-title function_"</span>>isValid</span>(email)) {
<span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-keyword"</span>></span>return<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span> <span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-title class_"</span>></span>Response<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span>.<span <span class="hljs-keyword">class</span>=<span class="hljs-string">"hljs-title function_"</span>>json</span>({
<span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-attr"</span>></span>success<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span>: <span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-literal"</span>></span>false<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span>,
<span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-attr"</span>></span>error<span class="hljs-tag"></<span class="hljs-name">span</span>></span></span>: <span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-string"</span>></span><span class="hljs-symbol">&quot;</span>Please use a valid email address<span class="hljs-symbol">&quot;</span><span class="hljs-tag"></<span class="hljs-name">span</span>></span></span>,
});
}Additionally, email validation is also included in the Yup schema:
<span <span class="hljs-keyword">class</span>=<span class="hljs-string">"hljs-attr"</span>>email</span>: yup
.<span <span class="hljs-keyword">class</span>=<span class="hljs-string">"hljs-title function_"</span>><span class="hljs-built_in">string</span></span>()
.<span <span class="hljs-keyword">class</span>=<span class="hljs-string">"hljs-title function_"</span>>email<<span class="hljs-regexp">/span>(messages.<span class="hljs-property">email</</span>span>.<span <span class="hljs-keyword">class</span>=<span class="hljs-string">"hljs-property"</span>>invalid</span>)
.<span <span class="hljs-keyword">class</span>=<span class="hljs-string">"hljs-title function_"</span>>required<<span class="hljs-regexp">/span>(messages.<span class="hljs-property">email</</span>span>.<span <span class="hljs-keyword">class</span>=<span class="hljs-string">"hljs-property"</span>>required</span>)
.<span <span class="hljs-keyword">class</span>=<span class="hljs-string">"hljs-title function_"</span>>test</span>(
<span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-string"</span>></span><span class="hljs-symbol">&quot;</span>is-valid-email<span class="hljs-symbol">&quot;</span><span class="hljs-tag"></<span class="hljs-name">span</span>></span></span>,
<span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-string"</span>></span><span class="hljs-symbol">&quot;</span>Email appears to be invalid or disposable<span class="hljs-symbol">&quot;</span><span class="hljs-tag"></<span class="hljs-name">span</span>></span></span>,
<span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-function"</span>></span>(<span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-params"</span>></span>value<span class="hljs-tag"></<span class="hljs-name">span</span>></span>) =<span class="hljs-symbol">&gt;</span><span class="hljs-tag"></<span class="hljs-name">span</span>></span></span> (value ? mailchecker.<span <span class="hljs-keyword">class</span>=<span class="hljs-string">"hljs-title function_"</span>>isValid<<span class="hljs-regexp">/span>(value) : <span class="hljs-literal">true</</span>span>)
),Avoid false positive
I do not suggest using content filtering, for example on keywords (“nigerian”, “prince”) as this may result in false positives.
Quick summary
If you’re in a hurry, captchas can still be a decent option — most bots don’t handle them well.
Just make sure you check the captcha value on the server so it can’t be skipped.
That’s what’s worked for me against spam, hopefully it’s useful for you too.
Have you run into malicious activity, DDoS attacks, or spammed forms?
I’m curious how you solved it — and I wouldn’t mind hearing the weird or funny stories that came with it.
More Articles

Create your own MCP servers
Discover how to design and build secure MCP servers using Typescript or Python.

MCP servers - Introduction & Guide
In this article, I explain how MCPs (Model Context Protocols) work and how to integrate them into your IDEs to connect external tools to your agent, providing it with context and levers for action to reduce redundant tasks -> Your IDE becomes a true all-in-one tool.

Cloud Email Microservices: A Guide to Using AWS Lambda and Cloudflare Workers
Deploy an email microservice on Lambda and handle queues — invoke from Cloudflare Workers or any Node.js backend
remix.run.png)
Best Practices for an Optimized Contact Page Design
Build a Contact Page That Connects — and Blocks Spam
.png)
Send and Receive Custom Domain Emails for Free
Set up free professional email addresses like you@yourdomain.com without hosting a mail server or paying for Google Workspace. This guide shows how to receive emails using Forward Email and send as your custom domain via Gmail — fast, reliable, and 100% free.
.png)
CI/CD deep-dive: Deploy a scalable Multi-Environment React App to AWS S3 + CloudFront with GitHub Actions
Learn how to build and deploy a scalable multi-environment React + Vite app using Domain-Driven Design, GitHub Actions for CI/CD, and AWS S3 + CloudFront for fast, cost-effective static hosting. Includes environment-specific configs, branch-based workflows, and secure AWS deployment setup.
.png)

Comments
Be the first to comment!