loading...

Submit forms without using re-captcha

iamcherta profile image Gabriel Chertok ・3 min read

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>
  )
}

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} />
    </>
  );
}

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 />
  }
);

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.

Posted on by:

iamcherta profile

Gabriel Chertok

@iamcherta

I run technology at Ingenious, an agency specializing in building products for the healthcare industry. 💛 remote work ‧ sometimes 🔨 are the best tool for the job.

Discussion

pic
Editor guide
 

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)

 

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.