Implémenter un captcha sans tracking avec Atcha et Nuxt
Depuis quelques jours, j'ai remarqué plusieurs utilisations abusives de mon formulaire de contact.
Et puis en regardant mieux, j'ai pu noter que chaque usage du formulaire de contact, était suivi par une inscription d'un utilisateur avec le même email et un nom qui suivait toujours le même pattern :
qSfDMiWAiLnpYYzdCeCWd
fePXzKXbAmiLAweNZ
etc... Autant dire, que leur appartenance à l'espèce humaine est hautement soumise à caution.
Bref, il est sans doute temps d'ajouter quelques contrôles et l'un des plus fameux c'est, le captcha.
Les captcha nouvelle génération
Le captcha, tout le monde connaît, c'est pénible, peut-être à égalité avec les bandeaux pour accepter les cookies.
On retrouve désormais des captcha où il faut identifier des feux rouges, calculer des additions, déplacer une case de puzzle au bon endroit et j'en passe.
Sauf que, vous aurez peut-être noté, depuis quelque temps, on voit aussi apparaître des simples formulaires avec une case à cocher : "Je ne suis pas un robot".

Parfois le captcha n'est d'ailleurs même plus visible, la détection se faisant à votre insu, sans rien vous demander.
Alors ça marche comment ? Et comment l'ajouter dans mon application ?
Nuxt turnstile, la solution par défaut avec Nuxt
Dans l'écosystème Nuxt, la solution la plus commune c'est Nuxt turnstile. La doc est assez explicite sur la manière de l'ajouter. C'est une très bonne solution mais elle repose sur Cloudflare turnstile or je tente de n'utiliser aucun produit US pour Writizzy et Hakanai.
Malgré tout, la doc permet de comprendre un peu mieux le fonctionnement des captcha de nouvelle génération.
Au moment de l'apparition de la page, le widget turnstile va faire des vérifications côté client :
- proof of space
Le script demande au client de générer et stocker une quantité de données selon un algo prédéfini, et lui demande ensuite l'octet à un position donnée. Non seulement ça prend du temps, mais ca s'automatise difficilement à l'échelle. - des détections triviales sur le navigateur
L'idée étant d'essayer de détecter un bot (pas de plugin, un pilotage par webdriver etc...). Le fingerprinting va également aider dans ce cas là. Il s'agit de récupérer toutes les infos disponibles, sur le navigateur, l'OS, les apis disponibles, la résolution etc...
A noter que le fingerprinting peut être mal vu par la RGPD qui peut considérer qu'il s'agit d'identifier une personne de façon unique.
Personnellement je trouve ça discutable en soi (je ne suis pas un fingerprint !), mais alors dans le cadre de la protection anti spam on se mord un peu la queue ici puisqu'il serait alors nécessaire de demander aux bots leur autorisation pour qu'on essaie de les détecter. On est ici aux limites de l'absurde.
Mais reprenons. En fonction des infos précédentes, le script va envoyer tout cela à Cloudflare. Sur la base de ces infos et se reposant aussi sur une énorme base de données de l'ensemble du traffic mondial, Cloudflare va calculer un pourcentage de chance que l'utilisateur soit un bot. Le formulaire va varier entre :
- rien à faire, cloudflare est convaincu que c'est un humain
- une case à cocher "je ne suis pas un robot"
- un captcha plus élaboré si vraiment la suspicion est forte
- une page de blocage quand la suspicion ne laisse aucun doute
Alors, vous pourriez me dire, la case à cocher, c'est quand même un peu léger non ? Si je suis arrivé jusque-là, je sais facilement automatiser un click supplémentaire. D'autant plus que cloudflare étant partout, c'est forcément le même formulaire partout.
Oui... Mais...
La première chose, c'est que la façon de cocher la case va être analysé. Est-ce que le click est trop rapide, est ce qu'il semble automatisé, est ce que le parcours de la souris pour atteindre la case est naturelle.
Tout ça peut déclencher une protection supplémentaire.
EDIT : turnstile ne fait peut-être pas cette opération. reCaptcha, la solution de Google, est connu pour le faire. Turnstile est moins explicite sur le sujet.
Mais en plus de ça la case à cocher déclenche un challenge, un petit calcul demandé par Cloudflare que votre client doit réaliser. Le résultat est ce qu'on appelle, une preuve de travail (proof of work).
Ce travail est lent, pour un ordinateur. On parle de 500ms, ce qui est une éternité pour une machine.
Mais, pour un utilisateur humain, c'est totalement anecdotique. Et, franchement, la satisfaction d'avoir fièrement démontré sa supériorité face à la machine permet d'oublier ces 500 petites millisecondes.
Par contre pour un bot, ce temps va poser un vrai souci s'il faut automatiser la création de centaines ou milliers de comptes.
Donc c'est pas impossible de cocher cette case, mais c'est couteux. Et c'est censé rendre l'équation économique pas intéressante sur de gros volumes.
Maintenant, même si c'est bien beau tout ça, je ne souhaite toujours pas utiliser Cloudflare, donc, comment le remplacer ?
Altcha, alternative open source
En faisant mes recherches je suis tombé sur altcha. La solution est opensource, ne nécessite aucun appel à des serveurs externes, ne partage aucune donnée.
L'implémentation nécessite de faire la demande de Proof of work (le fameux challenge javascript) depuis votre serveur. Ici, on va l'initier depuis le backend nuxt, dans un handler :
// 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
})
})
Dans la page du formulaire de contact, on va ajouter un Composant vue :
<ClientOnly>
<Altcha
v-model:payload="altchaPayload"
/>
</ClientOnly>
Ce altchaPayload sera ajouté du payload du post, par exemple :
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
}
})
Le résultat du calcul sera ensuite vérifié dans le endpoint /api/contact
const hmacKey = useRuntimeConfig().altchaHmacKey as string
const ok = await verifySolution(data.altcha, hmacKey)
if (!ok) {
throw createError({ statusCode: 400, message: 'Invalid challenge' })
}
Le composant vue dont je parlais plus haut, c'est celui-ci :
<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>
Et voilà, la page contact et la page signup est désormais protégé par ce altcha.
Maintenant, est-ce que ça marche ?
Les limitations de altcha
La mise en place s'est faite hier. Et malheureusement, je constate toujours des inscriptions très suspectes sur Pulse. Donc manifestement, Altcha n'a pas rempli son rôle.
Cependant, maintenant qu'on sait comment ça marche, c'est plus simple de comprendre pourquoi ça ne marche pas.
Altcha ne fait aucune des vérifications faites par turnstile :
- pas de proof of space
- pas de fingerprinting
- pas de vérification du fingerprinting auprès de cloudflare
- aucune vérification comportementale du clic souris sur la case à cocher.
La seule protection c'est le proof of work, qui ne coute "que" du temps à l'attaquant.
Or pour pulse, pour une raison qui m'échappe complètement, la personne qui s'amuse à créer des comptes en fait environ 4 par jours. Le cout du Proof of work est dérisoire dans ce cas-là. Altcha n'est donc pas adapté à ce type de "slow attack".
Bref, va falloir que je trouve une autre parade... Et je suis preneur de vos suggestions.


