
Hugo
For the past few days, I've noticed several suspicious uses of my contact form.
Looking closer, I noticed that each contact form submission was followed by a user signup with the same email and a name that always followed the same pattern:
qSfDMiWAiLnpYYzdCeCWd
fePXzKXbAmiLAweNZ
etc... Let's just say their membership in the human species seems particularly dubious.
Anyway, it's probably time to add some controls, and one of the most famous is the captcha.
Everyone knows captchas – they're annoying, probably on par with cookie consent banners.
Nowadays we see captchas where you have to identify traffic lights, solve additions, drag a puzzle piece to the right spot, and so on.
But you may have noticed that lately we're also seeing simple forms with a checkbox: "I am not a robot".

Sometimes the captcha isn't even visible anymore, with detection happening without asking you anything.
So how does it work? And how can I add it to my application?
In the Nuxt ecosystem, the most common solution is Nuxt turnstile. The documentation is pretty clear on how to add it.
It's a great solution, but it relies on Cloudflare turnstile, and I'm trying to use only european products for Writizzy and Hakanai.
Still, the documentation helps understand a bit better how next-generation captchas work.
When the page loads, the turnstile widget performs client-side checks:
Note that fingerprinting can be frowned upon by GDPR, which may consider it as uniquely identifying a person. Personally, I find that debatable, but in the context of anti-spam protection, we're kind of chasing our tail here since it would be necessary to ask bots for their permission to try to detect them. We're at the limits of absurdity here.
But let's continue. Based on the previous info, the script sends all this to Cloudflare. Based on this info and relying on a huge database of worldwide traffic, Cloudflare calculates a percentage chance that the user is a bot.
The form will vary between:
Now, you might say, the checkbox is a bit light, isn't it? If I've gotten this far, I can easily automate a click on a checkbox. Especially since Cloudflare is everywhere, it's necessarily the same form everywhere.
Yes... But...
First, the way you check the box will be analyzed. Is the click too fast, does it seem automated, is the mouse path to reach the box natural? All this can trigger additional protection.
EDIT: Turnstile might not do this operation. reCaptcha, Google's solution, is known for doing it. Turnstile is less explicit on the subject.
But on top of that, the checkbox triggers a challenge, a small calculation requested by Cloudflare that your client must perform. The result is what we call a proof of work.
This work is slow for a computer. We're talking about 500ms, an eternity for a machine. For a human user, it's totally anecdotal. And the satisfaction of having proven their humanity makes you forget those 500 little milliseconds.
On the other hand, for a bot, this time will be a real problem if it needs to automate the creation of hundreds or thousands of accounts.
So it's not impossible to check this box, but it's costly. And it's supposed to make the economic equation uninteresting at high volumes.
Now, even though all this is nice, I still don't want to use Cloudflare, so how do I replace it?
During my research, I came across altcha. The solution is open source, requires no calls to external servers, and shares no data.
The implementation requires requesting the Proof of Work (the famous JavaScript challenge) from your server. Here we'll initiate it from the Nuxt backend, in a handler:
typescript
// server/api/altcha/challenge.get.ts
import { createChallenge } from 'altcha-lib'
export default defineEventHandler(async () => {
const hmacKey = useRuntimeConfig().altchaHmacKey as string
return createChallenge({
hmacKey,
maxnumber: 100000,
expires: new Date(Date.now() + 60000) // 1 minute
})
})
In the contact form page, we'll add a Vue component:
vue
<ClientOnly>
<Altcha
v-model:payload="altchaPayload"
/>
</ClientOnly>
This altchaPayload will be added to the post payload, for example:
typescript
await $fetch('/api/contact', {
method: 'POST',
body: {
email: loggedIn.value ? user.value?.email : event.data.email,
subject: event.data.subject,
message: event.data.message,
altcha: altchaPayload.value
}
})
The calculation result will then be verified in the /api/contact endpoint
typescript
const hmacKey = useRuntimeConfig().altchaHmacKey as string
const ok = await verifySolution(data.altcha, hmacKey)
if (!ok) {
throw createError({ statusCode: 400, message: 'Invalid challenge' })
}
The Vue component I mentioned earlier is this one:
vue
<script setup lang="ts">
import { onMounted, onUnmounted, ref, watch } from 'vue'
const altchaWidget = ref<HTMLElement | null>(null)
const props = defineProps({
payload: {
type: String,
required: false
}
})
const emit = defineEmits<{
(e: 'update:payload', value: string): void
}>()
const internalValue = ref(props.payload)
watch(internalValue, (v) => {
emit('update:payload', v || '')
})
const onStateChange = (ev: CustomEvent | Event) => {
if ('detail' in ev) {
const { payload, state } = ev.detail
if (state === 'verified' && payload) {
internalValue.value = payload
} else {
internalValue.value = ''
}
}
}
onMounted(() => {
const script = document.createElement('script')
script.src = 'https://cdn.jsdelivr.net/gh/altcha-org/altcha@main/dist/altcha.min.js'
script.type = 'module'
document.head.appendChild(script)
if (altchaWidget.value) {
altchaWidget.value.addEventListener('statechange', onStateChange)
}
})
onUnmounted(() => {
if (altchaWidget.value) {
altchaWidget.value.removeEventListener('statechange', onStateChange)
}
})
</script>
<template>
<altcha-widget
ref="altchaWidget"
challengeurl="/api/altcha/challenge"
hidelogo
hidefooter
style="--altcha-max-width:100%"
/>
</template>
And there you go, the contact page and the signup page are now protected by this altcha.
Now, does it work?
The implementation was done yesterday. And unfortunately, I'm still seeing very suspicious signups on Pulse. So clearly, Altcha didn't do its job.
However, now that we know how it works, it's easier to understand why it doesn't work.
Altcha doesn't do any of the checks that Turnstile does:
The only protection is the proof of work, which only costs the attacker time.
Now for Pulse, for reasons I don't understand, the person having fun creating accounts makes about 4 per day. The cost of the proof of work is negligible in this case.
So Altcha is not suited for this type of "slow attack".
Anyway, I'll have to find another workaround... And I'm open to your suggestions.