FAZ Cookie Manager 1.33.0 è online. Se dovessi dare un titolo a questa versione non parlerei di cookie: parlerei di codice che sembra funzionare perché esiste. Tre dei difetti che ho chiuso hanno la stessa forma — un metodo scritto, una logica corretta, perfino un test che lo chiama — e nessuno che lo esegua in produzione. Non si vedono leggendo la funzione. Si vedono solo cercando chi la chiama.
Due script di GTM4WP, uno bloccato e uno no
La prima segnalazione veniva dal forum di wordpress.org: su un sito con GTM4WP e WooCommerce, dei due script che il plugin aggiunge solo uno veniva bloccato prima del consenso. L’autore aveva anche una spiegazione, e la spiegazione era sbagliata — ma la segnalazione era giusta, e questo è il punto che conta.
Ho trovato due difetti reali, nessuno dei due quello indicato. Il primo è di ordine. Quando il consenso arriva, gli script bloccati vengono reinseriti nella pagina, e uno script creato da JavaScript parte in modo asincrono per impostazione predefinita: l’ordine del documento va perso. Lo script WooCommerce di GTM4WP chiama una funzione definita dalla sua libreria generica, quindi se parte prima trova il vuoto. Adesso il ripristino passa da una coda unica che rispetta le posizioni originali e aspetta il caricamento di ogni script esterno prima del successivo, mentre chi era dichiarato asincrono resta asincrono e un download fallito libera la coda invece di bloccarla.
Il secondo difetto è più insidioso e riguarda chi decide se un tag va bloccato. Il plugin scorre una mappa di schemi provider e si fermava al primo che corrispondeva. Se quel primo apparteneva a una categoria consentita, lo script passava — anche quando un altro schema, più avanti nella stessa mappa, lo assegnava a una categoria negata. L’ordine della mappa decideva il consenso. Ora vince sempre la categoria bloccata, in entrambi gli strati di blocco e qualunque sia l’ordine, e uno schema condiviso da più categorie conserva quella negata invece della prima consentita.
Una cosa la dico perché è vera e non perché faccia bella figura: non ho dimostrato che questi due difetti fossero la causa del problema di chi ha segnalato. Su un’installazione pulita, con il catalogo di serie, entrambi gli script vengono bloccati correttamente — quindi nella sua configurazione c’è qualcosa che non ho ancora visto. Ho corretto ciò che ho potuto provare, e gli ho chiesto i quattro dati che servono per chiudere il resto. Una diagnosi plausibile non è una diagnosi.
Un filtro registrato su un hook che WordPress non ha
Lavorando su quel percorso ne è uscito il primo dei tre difetti invisibili. Il plugin bloccava gli script inline aggiunti con wp_add_inline_script() agganciandosi a un filtro chiamato wp_inline_script_tag. Quel filtro non esiste. Non è cambiato nome, non è stato rimosso in una versione recente: cercandolo in tutto wp-includes di WordPress non compare da nessuna parte. Quello vero si chiama wp_inline_script_attributes, esiste dalla 5.7 e riceve la mappa degli attributi invece del tag completo.
add_filter() accetta qualunque stringa senza protestare — è un registro, non un contratto — quindi la funzione era lì, scritta e corretta, e WordPress non l’ha mai chiamata. Per quindici release il blocco degli script inline si è retto interamente sull’output buffer di riserva. Funzionava, nella maggior parte dei casi, ed è esattamente per questo che nessuno se n’è accorto. Il commento sopra la riga diceva «WP 5.7+ expose questo filtro»: una spiegazione sicura di sé e falsa, che ha protetto l’errore meglio di qualunque silenzio.
Il test che accompagna la correzione non verifica il comportamento della funzione: quello era già giusto. Verifica che WordPress la chiami davvero, disegnando uno script inline attraverso il core e guardando cosa esce. Chiamare direttamente un callback non può accorgersi di un nome di hook sbagliato, ed è la ragione per cui il difetto è sopravvissuto a una suite di quasi milletrecento test.
Una guardia che non poteva dire no
Il secondo difetto invisibile è arrivato da una segnalazione su GitHub, e questo era grave: errore 500 sul frontend. Su un host dove l’utente PHP non è proprietario dei file di WordPress e l’estensione FTP è caricata, ogni richiesta che doveva scrivere un nuovo file di configurazione o di stile restituiva una pagina di errore.
La catena è istruttiva. WP_Filesystem() di WordPress assegna l’oggetto globale prima di connettersi, e quando la connessione fallisce lo lascia dov’è. Il plugin scartava il valore di ritorno e restituiva quell’oggetto, che sembrava vivo e aveva il trasporto morto: la prima scrittura arrivava a ftp_fput(null). E qui la parte che trasforma un guasto in un 500: un TypeError è un errore fatale, non un ritorno falso. Il codice chiamante aveva già un piano B perfettamente funzionante — se la scrittura non riesce, gli asset vengono messi in linea nella pagina — ma quel piano B si attiva su un valore falso, e un fatale non restituisce nulla. La rete di sicurezza c’era, tesa, e il problema le passava sopra.
Verificando la segnalazione ho trovato due cose che l’autore non poteva sapere. La prima: il metodo che scrive non aveva alcun controllo, mentre il metodo che legge, dieci righe più sotto, ne ha due dal giorno in cui è stato scritto. Il difetto era il più permissivo di due metodi gemelli — la stessa forma del difetto sulla mappa provider, in un file completamente diverso. La seconda: esiste una funzione can_access_filesystem() che avrebbe dovuto impedire tutto questo, e non poteva. Legge un’opzione il cui valore predefinito è «sì, posso scrivere», e l’unica funzione che scrive quell’opzione ha, in tutto il plugin, una sola occorrenza: la propria dichiarazione. Nessuno la chiama. Una guardia il cui interruttore non è collegato a niente.
Ora un trasporto inutilizzabile viene rifiutato, la scrittura riporta un fallimento ordinario e il piano B parte. La lettura dei file JSON di serie non è mai dipesa dal trasporto e continua a funzionare com’era. Una cosa non l’ho toccata, e lo scrivo perché resta aperta: nello stesso file c’è un terzo controllo scritto male, if ($file_system->errors), dove errors è un oggetto sempre presente e quindi la condizione è sempre vera. Correggerlo cambierebbe il valore di un percorso usato altrove, e non è una cosa che si infila dentro la correzione di un fatale. Ha bisogno del suo test e del suo turno.
I titoli del banner, adesso scegli tu il tag
La novità della versione è piccola e la chiedeva chi tiene alla struttura delle proprie pagine: il titolo del banner, quello del centro preferenze e i titoli delle categorie ora hanno un tag HTML configurabile — da H1 a H6, paragrafo, div o span, con H2, H2 e H3 come valori di partenza. Un banner che dichiara un H2 dentro una pagina che ha già la sua gerarchia di intestazioni non aiuta nessuno, e chi usa un lettore di schermo se ne accorge prima di tutti. La scelta sopravvive al cambio di lingua del banner.
Il terzo difetto invisibile era nei miei test
Chiudo con quello che mi ha insegnato più degli altri, perché non era nel prodotto. Dopo aver reso seriale il ripristino degli script, un test è diventato rosso: contava gli script ancora bloccati dopo il consenso e ne trovava cinque invece di zero. Sembrava una regressione grave. Misurando, la realtà era un’altra: il conteggio scende da quattordici a zero in due secondi, e il test lo fotografava a un secondo. Il numero cambiava fra le corse — cinque nella suite, dieci in isolamento — e un guasto vero non oscilla.
Il danno però era reale, e nascosto nel posto peggiore. Quel test sta in un blocco che si esegue in ordine, quindi il suo fallimento ha impedito l’esecuzione dei nove test successivi: rifiuto totale, liste di eccezioni, regole personalizzate, controllo delle richieste di rete, consenso per singolo servizio, pagamenti. Su un plugin di consenso è il cuore del contratto. Nel riepilogo apparivano come «nove non eseguiti», una riga che si legge come rumore e invece diceva che nove garanzie erano sospese.
Cercando se lo stesso assunto stesse altrove ne ho trovato un secondo, e questo è il più insidioso dei due: un test che aspettava mezzo secondo fisso prima di verificare la stessa cosa. Mezzo secondo contro uno svuotamento che ne richiede due. Non è mai fallito, perché la pagina su cui gira ha pochi script — passava per fortuna, non per correttezza. Aveva smesso di verificare continuando a dire verde, e un test così occupa il posto di una garanzia senza fornirla. Adesso entrambi attendono che la coda si svuoti, con un tetto oltre il quale falliscono, e ho controllato che sappiano ancora fallire: su un rifiuto il conteggio resta sopra zero ben oltre quel tetto.
Un’attesa fissa è una scommessa sul tempo di esecuzione, e il numero scelto registra la macchina di chi l’ha scritta, non il contratto del prodotto. Rendere seriale il ripristino ha reso perdenti tutte quelle scommesse in un colpo. Una l’ha persa subito e si è fatta vedere. L’altra ha continuato a vincere, ed è quella che mi preoccupava.
