DEV Community

Gabriel Chertok
Gabriel Chertok

Posted on

Submit forms without using re-captcha

If you ever had to put a form on a public page, you know bots will find it and send you "service offerings", newsletters, direct emails, and much more. That's exactly what happened the first time I deployed the Ingenious contact form.

Opening a gateway to an inbox is problematic because, like everything in security, attackers -spammers in this case- have all the time in the world, and they can afford to do it wrong a million times until they nail it.

To fix this problem, some developers use re-captcha, a tool that "[...]uses an advanced risk analysis engine and adaptive challenges to keep malicious software from engaging in abusive activities on your website" 🥱. In plain English it keeps bots away from your forms.

There are a lot of great wrappers depending on the technology you are using. At Ingenious, we use Next.js and deploy our website to Vercel. If I wanted, I could have implemented some re-captcha validation on our contact form with an already existing npm package, but the sole idea of adding a library for something that trivial didn't sound right.

Looking for alternatives, I learned about honeypots. Honeypots are additional inputs you put on a form to make bots think they are submitting correct info. The idea is to give the bot a honeypot field that looks legit and hide it with CSS from the users. On the backend, we can check if honeypot fields were submitted and discard that submission.

export default function ContactForm({ onSubmit }) {
  return (
    <div>
      <h1>Contact Us</h1>
      <form onSubmit={onSubmit}>
        {/* This is for the bot */}
        <div className="honey">
          <label htmlFor="name">Name</label>
          <input id="name" name="name" type="text" autoComplete="off" />
        </div>
        <div className="honey">
          <label htmlFor="email">Email</label>
          <input id="email" name="email" type="email" autoComplete="off" />
        </div>
        <div className="honey">
          <label htmlFor="message">Message</label>
          <textarea id="message" name="message" autoComplete="off"></textarea>
        </div>

        {/* This is for real users */}
        <div>
          <label htmlFor="name89jhbg2">Name</label>
          <input name="name89jhbg2" id="name89jhbg2" type="text" />
        </div>
        <div className="flex flex-col">
          <label htmlFor="email789miu82">Email</label>
          <input name="email789miu82" id="email789miu82" type="email" />
        </div>
        <div className="flex flex-col">
          <label htmlFor="message342cdssf3">Message</label>
          <textarea name="message342cdssf3" id="message342cdssf3"></textarea>
        </div>
        <button>Send</button>
      </form>
      <style jsx>{`
        .honey {
          display: none;
        }
      `}</style>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Another technique I've used is to delay the rendering of the form several seconds after the page itself is rendered. My thinking behind this is that bots may or may not execute JS -likely they do- but I don't think they will wait more than 3 or 4 seconds. On the other hand, users don't need to see the form until they are way down on the page -the contact form in our case is close to the bottom of the page. By the time user has scrolled to the bottom, the form will be loaded already.

When working with Next.js, you will use the next/dynamic package that's somehow similar to the React.lazy functionality. The idea is to dynamically import a module creating a new chunk. Next.js will then fetch the module at runtime.

Importing a module returns a promise that we can delay. In the case of Next.js, we need to ask for the module to be client-side only with ssr: false, otherwise it will end up on the statically generated page.

import dynamic from "next/dynamic";
import { delay } from "../utils";

const ContactForm = dynamic(
  () => import("../components/contact-form").then(delay(3000)),
  {
    ssr: false
  }
);

export default function IndexPage() {
  return (
    <>
      <ContactForm onSubmit={onSubmit} />
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Lastly, we can tell Next.js to use a placeholder component while loading the dynamically imported one.

import dynamic from "next/dynamic";
import { delay } from "../utils";

function ContactFormPlaceholder() {
  return <div>Nice Spinner</div>;
}

const ContactForm = dynamic(
  () => import("../components/contact-form").then(delay(3000)),
  {
    ssr: false,
    loading: () => <ContactFormPlaceholder />
  }
);
Enter fullscreen mode Exit fullscreen mode

This technique may hurt SEO, but how many times we need SEO for a contact form? The whole point is to allow real users to submit the form, not bots, even GoogleBot.

Here's the full example

You can reload the codesandbox, and scroll down to the bottom to se the form placeholder before the actual form loads, and click the "Show hidden fields" checkbox to try submitting the form as a bot.

Latest comments (3)

Collapse
 
moeraad profile image
moeraad

why don't you delay the submit button it self, the user need more than 10 sec to fill the form, activating the submit button after 10 seconds should keep the bots away

Collapse
 
sroehrl profile image
neoan

I would be interested in the success rate of the delayed implementation. Also: I personally use hcaptcha. It an alternative to the Google data collection that comes along with recaptcha and even let's you participate in the revenue (these Captchas are training, after all)

Collapse
 
iamcherta profile image
Gabriel Chertok

Never heard of hcaptcha, will take a look! I implemented the delayed rendering along with the honeypot and spam rate is very low but I cannot tell which one boosted the performance, I think the honeypot removed the bulk of our problems, still think delaying the rendering was a nice approach to the problem.

It can be even more specific if I show the form only when users are approaching that section on the page using the intersection observer api.