Questa versione nasce da una segnalazione che conteneva già la diagnosi. Un utente aveva notato che il registro dei consensi del suo sito non tornava: il banner funzionava, i visitatori cliccavano, e nel database finiva una frazione di quello che avrebbe dovuto finirci. Aveva fatto la cosa che quasi nessuno fa — era andato a contare. Cinquantasette consensi registrati e duecentottantasei rifiutati, in un giorno solo, con la quota di rifiuti che cresceva man mano che invecchiava l’HTML in cache e che crollava subito dopo uno svuotamento.
Un difetto che non si vedeva da nessuna parte
La causa era un gettone. Ogni richiesta che scrive un consenso porta con sé un codice che dimostra che la pagina da cui arriva è stata generata da quel sito, e quel codice veniva accettato solo per la mezza giornata corrente e per quella precedente. Dodici, al massimo ventiquattro ore. Il problema è che una pagina servita da una cache può restare in circolazione molto più a lungo: LiteSpeed Cache spedisce nei propri valori predefiniti una durata di sette giorni, e i valori di WP Rocket e W3 Total Cache si misurano in giorni. Passato quel limite, ogni consenso raccolto da quella pagina veniva rifiutato.
Quello che rende questo difetto peggiore di altri è dove non si vedeva. Il visitatore non notava niente: il banner rispondeva, la conferma appariva, la sua scelta veniva applicata davvero. Il gestore del sito non notava niente: nessun errore, nessun avviso, nessun numero rosso. L’unica cosa che mancava era il documento che l’articolo 7(1) del GDPR chiede di conservare — la prova che quel consenso c’è stato. Un difetto che tocca solo le prove è il peggior difetto possibile per un plugin di consenso, perché si manifesta esattamente il giorno in cui qualcuno le chiede.
Il gettone ora vale sette giorni, che è la durata di cache più lunga fra quelle predefinite in circolazione, e chi tiene una cache ancora più lunga può allargare la finestra con un filtro. Allungarla non indebolisce ciò che il gettone dimostra: è legato ai sali di quella singola installazione, quindi non può essere fabbricato altrove a nessuna età, e la protezione contro il riutilizzo da un altro sito non è mai stata l’età del gettone.
La metà che doveva evitare che succedesse ancora
Il difetto originale aveva una seconda metà, e per me è la parte interessante di questo rilascio. Correggere la finestra era facile. La cosa difficile era fare in modo che, se un consenso viene rifiutato di nuovo per una ragione qualunque, qualcuno se ne accorga — invece di scoprirlo mesi dopo contando le righe a mano. Quindi ho aggiunto un contatore dei rifiuti e una riga nello Stato del sistema che lo mostra.
Poi ho fatto rivedere quel contatore, e aveva quattro difetti. Tutti e quattro nella stessa direzione: rendere il rapporto più tranquillo della realtà che racconta.
Il numero veniva stampato con una conversione per interi ma gli veniva passata la stringa già formattata per la lingua. In italiano duemiladue si scrive 2.002, e la conversione si ferma al punto delle migliaia: la riga diceva 2. Duemiladue record persi, riportati come due. In ogni lingua, perché ogni lingua raggruppa le migliaia in qualche modo, e senza un solo avviso da parte di PHP.
Il secondo: la finestra che si dichiarava «negli ultimi 7 giorni» in realtà azzerava tutto quando scadeva il primo rifiuto registrato. Su un sito che continua a perdere consensi, il numero non saliva e basta: saliva e poi crollava. Simulando trecento rifiuti al giorno, la riga mostrava 2.100 al sesto giorno e 299 al settimo. Non è soltanto un numero sbagliato: è un numero che sembra un miglioramento. Chi avesse guardato quella pagina a una settimana di distanza avrebbe visto la cifra scendere dell’ottantasei per cento mentre non era cambiato niente.
Il terzo: dei cinque modi in cui un consenso può andare perso, ne veniva contato uno solo. E la riga restava nascosta finché il contatore era a zero, il che rendeva illeggibile la sua stessa assenza — un sito che perdeva tutti i consensi per una causa che nessuno contava mostrava una pagina identica a quella di un sito sano. Ora le cause sono contate tutte e cinque, separatamente, e la riga compare anche quando il numero è zero.
Il quarto era di sicurezza: il contatore veniva scritto prima del limitatore di frequenza dell’endpoint, quindi chiunque poteva ottenere una scrittura nel database per ogni richiesta, sull’unica rotta che deve restare raggiungibile senza autenticazione. La richiesta necessaria era già nel repository: era il test end-to-end scritto per un’altra verifica. Bastava metterci attorno un ciclo.
Lo stesso difetto, in una seconda copia
Mentre sistemavo il gettone dei consensi ho controllato se esistesse altro codice che facesse la stessa aritmetica. Esisteva: l’endpoint che conta le visualizzazioni di pagina aveva la propria copia, con lo stesso limite di dodici ore e lo stesso effetto — visite, aperture del banner e scelte dell’utente che arrivavano da pagine in cache più vecchie di un giorno venivano rifiutate, e il cruscotto mostrava numeri più bassi del vero senza dirlo a nessuno.
Ho corretto anche quello, nello stesso rilascio, e non per fretta. Due copie della stessa regola libere di divergere sono esattamente ciò che ha prodotto il problema iniziale: correggerne una e aprire una segnalazione per l’altra sarebbe stato commettere lo stesso errore due volte. Adesso ciascun gettone viene coniato dalla classe che poi lo accetta, così non resta una seconda copia che possa prendere una strada diversa.
Una sottostringa che decideva chi può partire prima del consenso
L’ultima correzione non viene da una segnalazione. Viene dall’aver finalmente guardato l’elenco degli avvisi dell’analisi statica, che era lì da giugno con quindici voci aperte e nessuno che lo leggesse.
Una funzione del plugin decide se uno script è un tag Google governato dal Consent Mode e, in quanto tale, se può caricarsi prima che il visitatore abbia risposto al banner. Quella decisione veniva presa cercando il nome del dominio come sottostringa dentro l’indirizzo. Il che significa che un indirizzo qualunque che contenesse quel nome da qualche parte — per esempio un tracciatore di terze parti che porta una destinazione di rinvio in un parametro — passava il controllo e si caricava prima del consenso. Non serve un attaccante per incontrarlo: basta un indirizzo scritto così.
Ora il confronto avviene sull’host, ed è un host o un suo sottodominio, non un pezzo di testo trovato per caso. Nella stessa revisione ho stretto anche il controllo degli schemi degli indirizzi, che nell’altra direzione era troppo severo: un’immagine il cui indirizzo conteneva un due punti fuori posto — una cosa banale come /foto/a.jpg?v=12:30 — restava bloccata anche dopo il consenso, senza che niente spiegasse perché.
La lezione, che è sempre la stessa
Tre dei difetti raccontati qui sopra erano coperti da test. I test erano verdi.
Il troncamento di 2.002 non veniva rilevato perché i due valori di prova erano 42 e 900, entrambi sotto il migliaio. Il controllo che doveva impedire di nascondere di nuovo la riga cercava il nome di una variabile che nella versione nuova non esiste più, quindi non poteva fallire in nessun caso. Le verifiche sullo stato a zero confrontavano una sottostringa, e «900 in the last 7 days» contiene «0 in the last 7 days»: il test che doveva dimostrare che un contatore scaduto legge zero passava mentre la pagina mostrava novecento.
Per questo, in questa versione, ogni nuova verifica l’ho prima lanciata contro il codice vecchio, a controllare che fallisse. Sette asserzioni su ventinove falliscono sul codice precedente; quella sul formato del rapporto fallisce quattro volte e stampa il difetto per esteso. Un test che non può fallire non dimostra niente, e costa esattamente quanto uno che funziona: è più pericoloso dell’assenza di test, perché dà una risposta rassicurante a una domanda che non ha mai posto.
Chiudo con un ringraziamento sincero a chi ha aperto la segnalazione. Non si è limitato a dire che qualcosa non andava: ha misurato, ha portato i numeri, e ha indicato la direzione giusta fin dalla prima riga. È il genere di segnalazione che vale dieci sospetti, e questa versione esiste per quella.
