Vous cliquez sur un lien vers un journal local, et le titre se charge, puis l’image, puis les trois premiers paragraphes, et vous avez commencé à lire avant que quoi que ce soit d’autre ne se passe. Puis la page devient blanche. Ou elle se transforme en un tas de liens sans style, le logo étiré sur toute la largeur de l’écran. Ou une boîte glisse vers le haut et vous demande soit d’accepter le pistage, soit de payer 2,99 £ par mois, sans troisième bouton.

L’article était sur votre machine. Le serveur de l’éditeur l’avait déjà envoyé en entier, texte et feuilles de style, et votre navigateur l’avait déjà affiché. Ce qui l’a repris, c’est du code que l’éditeur a choisi d’exécuter ensuite, sur votre ordinateur, acheté à un fournisseur commercial dont le produit consiste à reprendre ce qui vient d’être livré.

Chez moi, je fais tourner un Pi-hole1, qui bloque les hôtes publicitaires et de pistage pour tous les appareils du réseau. Sur une liste croissante de sites d’actualité britanniques, cela suffisait pour que la page soit détruite. Alors le 22 septembre 2026, j’ai construit une extension Chrome pour l’empêcher, appelée Keep The Page. Le code est sur GitHub à damo2929/browserplugin, sous licence MIT, et voici ce qu’elle fait, ce que j’ai trouvé dans ces pages en la construisant, et les correctifs qui ont empiré les choses avant que le bon n’arrive.

Ce n’est pas un contournement de paywall. Si le serveur n’a jamais envoyé l’article, rien ici ne le fera apparaître. Le Times est resté fermé, et c’est normal. Ce qu’elle garde, c’est ce qu’on vous avait déjà envoyé.

L’article arrive, puis il disparaît

Vus de l’extérieur, tous fonctionnent de la même façon. Le HTML arrive complet, puis un script, généralement chargé depuis un hôte appartenant à un fournisseur plutôt qu’au journal, décide que vous ne méritez pas de le voir, et le retire.

Ce qu’il retire, et comment, dépend du fournisseur que l’éditeur a acheté. Voici les quatre que j’ai rencontrés, chacun relevé sur une vraie page pendant la construction :

ÉditeurCe qui arriveCe que la page se fait ensuite à elle-même
Newsquest (269 titres)2l’article completconstruit un mur, exécute une charge eval, ouvre une boîte de dialogue confirm()
notebookcheck.netl’article completsupprime <body> au bout d’environ 7 secondes, puis des dialogues, puis une boucle de rechargement
National World (The Scotsman, Yorkshire Post)3l’article plus environ 68KB de son propre CSSsupprime chaque <link> et <style> toutes les 100ms, pour toujours
Reach plc (Mirror, Daily Record, Manchester Evening News, Liverpool Echo et d’autres)4l’article completle recouvre de « acceptez le pistage ou payez 2,99 £/mois »

Le chiffre de 269 ne vient pas d’un communiqué de presse, qui parle de « plus de 200 marques ». C’est le nombre de domaines sur les propres certificats TLS de Newsquest, la liste dont l’extension avait besoin pour savoir où s’exécuter.

Le cas National World est celui qui devrait le plus inquiéter les éditeurs, parce qu’il casse la page pour des raisons qui n’ont rien à voir avec la publicité. Le décapeur de feuilles de style a un complice qui récupère le CSS depuis l’hôte du fournisseur. Mon Pi-hole bloque cet hôte. Donc le décapage a tourné, la restauration n’est jamais venue, et la mise en page du journal lui-même est restée dépendante, en permanence, de la joignabilité d’un fournisseur publicitaire. Ce n’est pas un mur. C’est un éditeur qui confie l’apparence de son propre site à un tiers sans s’en rendre compte.

Puis-je demander pourquoi la feuille de style d’un journal doit attendre le serveur d’une régie publicitaire avant d’avoir le droit de rester sur la page ? Il n’y a aucune raison qui serve le lecteur. Aucune.

Pourquoi j’appelle cela un malware

J’emploie le mot exprès, pour décrire ce que fait le code, et non comme une conclusion juridique sur qui que ce soit. Les charges anti-adblock répondent au sens ordinaire sur quatre points, et chacun a été observé directement :

CritèreCe que j’ai vu
S’exécute sans consentement, contre votre intérêtpersonne ne l’a demandé, et son rôle est de vous retirer un contenu que vous avez déjà
Détruit des données déjà livréesl’article et son CSS arrivent intacts, puis du code dans la page les supprime
Obscurci pour résister à la lecturetables de chaînes permutées comme o[293 * (r + 450) % e], exécutées via eval
Échappe au blocage et punit l’interférenceCNAME cloaking pour esquiver les listes de blocage DNS, contrôles anti-altération qui escaladent vers un dialogue ou une boucle de rechargement

L’obscurcissement n’est pas de la minification. Le code minifié est petit. Ici, le code est agencé pour qu’on ne puisse pas y trouver avec grep ce qu’il fait, puis la charge passe par eval pour que rien sur le disque ne corresponde à ce qui s’exécute.

Le cloaking est une technique connue, avec sa propre littérature de recherche5. Le chargeur est récupéré depuis ce qui ressemble à un sous-domaine du journal, et ce nom pointe vers le fournisseur :

a02342.<publisher-domain>   ->  cdn-52-x.privacy-mgmt.com      (Sourcepoint)
fb.html-load.com            ->  adshield-fallback-dev-wskxz.b-cdn.net

Le sous-domaine est aléatoire pour chaque titre. Une liste de blocage qui nomme des hôtes ne peut donc pas suivre, et c’est tout l’intérêt.

Et le code traite toute interférence comme une preuve de culpabilité. Les messages d’erreur décodés dans la charge que les mainteneurs de listes de filtres attribuent à Ad-Shield6 sont littéralement Vital API blocked et Vital API blocked (eval). Le chargeur de Sourcepoint écrit un attribut, le relit aussitôt et lève une erreur si la valeur a changé :

z.call(O,'src',G), O[x]('src') !== G && throw E
… catch (W) { try { await l(W) } catch (x) { o(W) } }   // o() raises the dialog

Sourcepoint a vendu cela ouvertement. Sa propre documentation disait « on average about 30% of messaged users will turn off their adblockers »7. Je ne décris donc pas un script voyou glissé par quelqu’un. C’est un produit, acheté et déployé délibérément par l’éditeur.

Les murs consent-or-pay sont une autre catégorie, et je ne les appelle pas malware. Ils ne détruisent pas ce qui a été livré et ne se cachent pas des listes de blocage. Ils contraignent d’une autre manière, et c’est l’objet de la section suivante.

Accepter 1 467 partenaires ou payer 2,99 £

Le mur sur les titres de Reach est Quantcast Choice, désormais exploité par InMobi8 et servi depuis cmp.inmobi.com. Il offre deux choix. Accepter, ou payer 2,99 £ par mois. Il n’y a pas de « non » gratuit.

Accepter partage vos données avec 1 467 partenaires listés et écrit un cookie euconsent-v2 qui dure 13 mois. Personne ne lit une liste de 1 467 entreprises, personne ne pourrait peser ce que chacune ferait des données même en la lisant, et le chiffre à lui seul vous dit quel genre de consentement on vous demande.

Le RGPD britannique dit que le consentement doit être librement donné, et que pour en juger on regarde si le service a été subordonné à un consentement à un traitement dont il n’a pas besoin9. L’ICO a publié des lignes directrices selon lesquelles le consent or pay peut être licite10, lignes directrices qu’il dit désormais en cours de révision, et le Comité européen de la protection des données a dit que pour les grandes plateformes qui ne proposent que ces deux options, dans la plupart des cas il ne le sera pas11. Je ne prétendrai pas que le régulateur l’a interdit. Il ne l’a pas fait.

Alors voici où j’en arrive, sans détour. J’ai choisi de retirer le mur et de refuser le consentement. Cela veut dire que je lis l’article sur la branche gratuite, sans payer et sans remettre mes données à 1 467 entreprises. C’est une décision. L’extension le dit sur sa propre page de réglages, et je ne la déguise pas en quelque chose de neutre. Un consentement que vous ne pouvez pas refuser est un prix, et je ne paierai pas un prix déguisé en question.

Retirer la boîte, c’est le tiers facile. Les deux autres tiers sont dans Répondre correctement à la question du consentement, parce qu’une bannière qu’on supprime sans répondre revient à chaque fois.

Un jour, huit commits

L’ensemble a été construit en une journée. Quelques heures à fouiller des pages avant le moindre commit, puis huit commits entre 20:32 et 22:50. Je l’ai construit avec Claude Code, et une bonne partie du travail ingrat, lire du script inline obscurci ligne par ligne, a été faite par l’agent pendant que je regardais ce que les pages faisaient à l’écran. Ce partage a bien fonctionné, et là où il a déraillé, c’est plus bas, parce qu’il a déraillé d’une façon qui mérite d’être connue.

HeureCommit
20:32premier commit : Newsquest, notebookcheck, National World, Reach, Page Six
20:46le fournisseur derrière chaque mécanisme, consigné dans le README
20:5650 liens de la une de Google News, comme échantillon non choisi
21:02répondre à l’API de consentement avec chaque finalité refusée
21:11liens sociaux désamorcés
22:50protections désactivables, MSN et Bing, refus du consentement par clic, 77 tests

C’est une extension Manifest V3 avec deux permissions, declarativeNetRequest et storage, sans permission d’hôte et sans accès réseau propre, elle ne peut donc rien récupérer, réécrire aucune réponse ni parler à aucun serveur à votre place. Cette contrainte a façonné la conception plus que tout le reste, parce que le travail intéressant doit se faire à l’intérieur de la page.

Deux mondes, un attribut qui descend et un événement qui remonteLe code de la page et les réglages de l'extension vivent dans des mondes différentsStockage de l'extensionchrome.storage.localprotectionscanaux de traçageréglages sociaux50 dernières erreurslu et écrit parla page de réglagesMonde isolébridge.jssocial.jsportal.jspeut lire chrome.storagene peut pas toucher auxfonctions de la pageMonde de la page (MAIN)walls.jsguard.jsportal-early.jsenveloppe setTimeout, cookie,__tcfapi, window.adLightaucun chrome.*Descend : un attribut sur l'élément html, seulement ce qui est coupé<html data-ktp-off="cookies,dom">installation par défaut : rien d'écritRemonte : un événement ktp-reportdetail : une chaîne JSONun objet ne passe pas de façon fiable
Chrome exécute les scripts d’extension dans deux mondes. Le monde de la page atteint le JavaScript de la page mais n’a aucune API d’extension. Le monde isolé peut lire les réglages mais ne peut pas toucher aux fonctions de la page. Tout passe de l’un à l’autre sous forme d’un attribut qui descend et d’un événement qui remonte.

Le monde MAIN de Chrome partage l’environnement JavaScript de la page12. Un script qui s’y trouve peut remplacer setTimeout, envelopper document.cookie ou définir window.adLight avant la page, et c’est exactement ce qu’il faut pour combattre ces murs. En pratique, il ne peut pas appeler chrome.storage. Le monde isolé le peut, mais ne voit pas les fonctions de la page. Alors un petit pont lit les réglages et les écrit sur <html>, et les erreurs remontent sous forme d’un CustomEvent dont le détail est une chaîne JSON, parce qu’un objet ne traverse pas cette frontière de façon fiable.

L’attribut qui descend ne liste que ce que vous avez désactivé. Une installation par défaut n’écrit rien du tout dans la page. C’est important, parce qu’un marqueur permanent sur <html> est précisément le genre de chose que ces SDK recherchent, et parce qu’une panne du stockage échoue alors du côté de la défense de la page plutôt que de l’autre.

Trouver la porte, ne pas combattre le mur

Le correctif Newsquest tient en deux lignes de raisonnement, et c’est celui à l’aune duquel tout le reste a été mesuré.

Tout leur mur tient derrière un seul drapeau dans la page :

var adLight = false;        // line 1647
if (adLight !== true) { …}  // line 2020: loader, eval payload, confirm()

adLight est le drapeau abonné « peu de publicité ». S’il est vrai, le mur ne se construit jamais. Alors l’extension définit window.adLight à true à document_start, avant que le propre script de la page ne s’exécute, avec un mutateur qui ignore ce qu’on y écrit :

Object.defineProperty(window, 'adLight', {
  configurable: false,
  enumerable: true,
  get() { return true; },
  set() { /* ignore the page's "false" */ }
});

Un var en tête de script ne redéfinit pas une propriété que l’objet global possède déjà. Il ne fait que lui affecter une valeur13. Donc le var adLight = false de la page s’exécute, tombe sur le mutateur et ne fait rien. Le chargeur du mur ne démarre jamais, la charge eval n’arrive jamais, et il n’y a aucun contrôle anti-altération à déclencher, parce que rien n’a été altéré. Le drapeau a simplement dit oui.

C’est la seule propriété de toute l’extension qui n’est pas configurable. Elle doit l’être pour survivre à la déclaration. Tout le reste est configurable: true, pour qu’aucune page ne se voie jamais confisquer définitivement une de ses propres API.

Il couvre 269 titres et ne peut pas être désactivé dans les réglages, qui disent pourquoi : il s’exécute avant que chrome.storage puisse répondre, et une fois posé il ne peut pas être annulé. Une case à cocher serait de la décoration.

Chaque correctif qui a empiré les choses

C’est la section utile, parce que chaque erreur ici est la chose évidente qu’on essaie.

Ce que j’ai essayéCe qui s’est passé
Bloquer l’hôte du chargeurl’hôte est un CNAME first-party aléatoire par titre, et une requête échouée est elle-même le signal de détection
Surveiller setAttribute sur les scripts injectésa déclenché le contrôle de relecture de Sourcepoint, qui a ouvert le dialogue qu’on voulait empêcher
Répondre au confirm() par Annulerdans ce SDK, Annuler veut dire recharger, d’où une boucle de rechargement infinie
Sauvegarder <body> à DOMContentLoaded pour le restaurer plus tardle mur vide la page pendant l’analyse, la sauvegarde était donc celle d’un body vide
Surveiller remove() et removeChild()le mur efface la page d’un seul innerHTML = '', pas nœud par nœud
Tout surveiller à la fois sur notebookcheckle mur a escaladé vers un dialogue, puis une boucle de rechargement, nettement pire que ne rien faire
Rediriger l’hôte du fournisseur vers un bouchon localune règle redirect avec seulement declarativeNetRequest a invalidé tout le jeu de règles, tuant en silence la règle Newsquest sur 269 sites
Faire tourner les gardes de page sur tous les sitesa patché des prototypes globaux sur chaque page visitée, y compris celle de ma banque

La redirection mérite un second regard. Chrome donne à une règle block un accès implicite et exige une permission d’hôte pour tout le reste14. Sa documentation dit que les règles statiques invalides sont ignorées15. Ce que j’ai vu était pire : tout le fichier est parti, sans aucune erreur sur la page, et la règle 1 a simplement cessé d’exister sur 269 sites. Les deux règles pour MSN vivent donc désormais dans un jeu de règles à part, pour qu’une mauvaise modification de l’un n’emporte pas l’autre.

La boucle de rechargement a enseigné l’autre règle dure. location.reload ne peut pas être intercepté. L’objet Location est infalsifiable dans le standard HTML16, ses méthodes ne sont donc ni modifiables ni configurables, et l’essayer donne :

Object.defineProperty(location,'reload',...)
-> TypeError: Cannot redefine property: reload

Un mur dont le chemin d’échec est « recharger la page » ne peut plus être arrêté une fois engagé sur ce chemin. Par rien. Le seul correctif est de faire en sorte qu’il n’y arrive jamais. C’est la même leçon qu’avec adLight, apprise à la dure : chaque tentative de combattre un mur déjà lancé a empiré les choses, et chaque correctif qui a marché a empêché le mur de démarrer.

Une seule minuterie a fait les dégâts

notebookcheck n’a pas d’adLight. Le mur tourne toujours. Avec le traçage activé, l’extension a montré ce qu’il planifiait :

dropped setTimeout(7005ms) scheduled from eval    <- the body.remove()
dropped setTimeout(1251ms) / 105ms / 0ms x8
dropped setInterval(15000ms)

Une de ces minuteries fait tous les dégâts : celle à 7 secondes qui retire <body>. Tout ce qui suit est une réaction, parce que la page vide lève une erreur, l’exception ouvre le dialogue, le dialogue recharge la page, et tout recommence depuis le début avec une nouvelle série de minuteries.

Le correctif est donc une seule règle. Abandonner une minuterie si elle a été planifiée depuis du code passé par eval, si la page porte la signature de ce SDK et si le gestionnaire est une fonction. La pile dit d’où vient un appel, et eval y laisse sa marque.

before: 4 page loads, 3 confirms, reload loop
after:  1 page load,  0 confirms, alive 40,378ms, content intact
Une minuterie, et tout ce qui en découlenotebookcheck : une minuterie fait les dégâts, le reste est réactionTel que serviarticle et CSSarrivent, affichéschargeur depuishtml-load.comcharge evalplanifie des minuteries7,0s : la minuterie partbody.remove()la page vide lève,confirm() ouvertla page rechargeet tout recommence4 chargements, 3 dialogues, boucle de rechargementAvec l'extensionarticle et CSSarrivent, affichéschargeur depuishtml-load.comcharge evalplanifie des minuterieschaque minuterie evalabandonnée d'emblée1 chargement, 0 dialogue,vivante à 40 secondes
notebookcheck sans et avec l’extension. Rien en aval de la minuterie des 7 secondes n’a besoin d’être corrigé, parce que rien de tout cela n’arrive une fois cette minuterie abandonnée.

Il y avait à ce moment deux autres mécanismes dans le build, une redirection du chargeur vers un bouchon et un élément leurre pour absorber les écritures du mur. Les deux marchaient, et les deux soignaient des symptômes de cette seule minuterie, ce qui n’est devenu évident qu’en testant la règle des minuteries seule, qui s’est révélée suffisante. Les deux sont donc partis, et avec eux une permission et toutes les permissions d’hôte. Testez toujours si le dernier changement suffit à lui seul avant de garder l’échafaudage autour.

La signature est l’attribut data-sdk sur la balise du chargeur, et elle correspond à une forme, l/<n>.<n>, plutôt qu’à une version. Trois versions sont apparues en une journée. Elle s’enclenche et ne met jamais un négatif en cache, parce que la balise du chargeur n’est peut-être pas encore analysée quand les premières minuteries sont planifiées, et un « non » mémorisé la désarmerait pour de bon sur une page qui porte bien le mur.

La mesure disait que tout allait bien. L’écran, non.

The Scotsman et le Yorkshire Post ont été déclarés réparés sur la foi d’une mesure qui comptait le texte. La page en avait 8 808 caractères, 22 éléments sous <body>, stables à 2, 8 et 16 secondes. Selon cette mesure, elle était intacte.

Elle ne l’était pas. Je la regardais, et chaque feuille de style avait disparu, les liens formaient une liste brute, le logo SVG remplissait l’écran et il y avait une barre de défilement horizontale. Tous les mots étaient là, et c’est tout ce que cette mesure pouvait voir. J’ai dû montrer l’écran du doigt et le dire.

Ce que la mesure a vu, et ce qu'il y avait à l'écranYorkshire Post avant le correctif : la même page, mesurée de deux façonsCe que la mesure a comptéinnerText.length8 808enfants de body22à 2s, 8s, 16sinchangéVerdict : intacteCe qu'il y avait à l'écrandocument.styleSheets.length0liens en liste brute, logo pleine largeur,une barre de défilement horizontaleVerdict : casséeDécapeur abandonné dès sa planification : 2 feuilles de style, 532 règles, la page s'affiche
La même page, mesurée de deux façons. La longueur du texte disait que la page allait bien. Le nombre de feuilles de style disait qu’elle était cassée, et c’est le nombre de feuilles de style qui avait raison.

La cause était le décapeur du premier tableau, et il ne correspondait pas à la règle des minuteries, parce que c’est un script inline ordinaire et non de l’eval. Sa propre source est donc la signature : une tâche répétée dont le corps appelle querySelectorAll('link,style') puis remove(). Rien de légitime ne fait cela. Il est abandonné dès sa planification, rien n’est jamais décapé et rien n’a besoin d’être restauré. Le Yorkshire Post est passé de 0 feuille de style à 2, avec 532 règles, et la page s’est affichée.

Pour le trouver, il a fallu une trace de pile, pas une supposition. Patchez Element.prototype.remove pour journaliser la pile chaque fois qu’il retire un STYLE ou un LINK, et il a nommé le script inline et le forEach du premier coup.

Ensuite, document.styleSheets.length est entré dans chaque vérification. Une page sans feuille de style est cassée, quelle que soit la quantité de texte. Mesurer est plus difficile qu’on ne le croit, et voici les pièges dans lesquels le build est tombé ce jour-là :

PiègeCe qu’il disaitCe qui était vrai
longueur du texte comme contrôle de rendu« intacte »balisage sans style
un seul échantillon après le chargementmurs Reach « absents » sur huit titresle mur vit environ 600ms et avait déjà été balayé
console vide après la navigation« la garde ne se déclenche pas »les messages de chargement ne survivent pas à la navigation
échantillons à 1s et 4snotebookcheck en bonne santéla page se vide entre 5 et 8 secondes
cssRules compté entre originesESPN avait 5 règlesles feuilles d’autres origines lèvent une erreur, donc le compte est bas

Le correctif pour le cas des 600ms est d’interroger toutes les 100ms dès le chargement de la page. Sur Wales Online, le mur est apparu à 425ms et avait disparu à 1 129ms.

Puis les vérifications ont été faites pour de bon. 52 articles sur 26 domaines, deux par site, choisis pour couvrir chaque mécanisme : 52 sur 52 avec feuilles de style, aucun mur resté à l’écran, aucune boucle de rechargement. Puis 50 liens pris directement sur la une britannique de Google News, pas choisis par moi : 45 affichés normalement, 2 étaient des hôtes que mon Pi-hole bloque exprès, 1 était le vrai paywall du Times, et 2 étaient le même article atterri deux fois parce que Google reconstruit l’ordre des liens à chaque chargement. Aucun à zéro feuille de style. Trois titres de National World que je n’avais jamais testés sont apparus dans ce passage avec le même SDK, et tous les trois se sont affichés, et c’est à cela que sert de reconnaître une forme plutôt qu’une liste de sites.

Il y a 77 tests unitaires, dans le dépôt avec tout le reste. Il n’y a pas de Node sur cette machine, ils tournent donc sur gjs, et ils chargent le vrai walls.js contre un DOM de substitution, pour que ce soient les motifs de production qui soient testés. Chacun a été vérifié en cassant ce qu’il protège et en le regardant passer au rouge. Ils ne testent que des décisions. Les passer ne veut pas dire qu’une page s’affiche, et le Scotsman est la raison pour laquelle cette phrase figure dans le README.

Répondre correctement à la question du consentement

Supprimer une bannière de consentement laisse la question sans réponse. Cela a deux conséquences, et la première est que la bannière est reconstruite à chaque chargement de page. Et un éditeur qui retient son contenu jusqu’à ce que l’API de consentement réponde restera simplement bloqué, tandis qu’un fournisseur qui n’obtient aucune réponse peut traiter la question comme jamais posée. Le silence n’est pas un refus.

L’extension y répond donc, dans cet ordre :

Refuser, répondre non, ne rien stocker, et seulement ensuite retirerUne bannière de consentement reçoit une réponse, pas seulement une suppressionLe conteneur d'un fournisseur est à l'écran#qc-cmp2-container #onetrust-consent-sdk sp_message_container1. Presser leur refusReject all, Decline, Only essential,Continue without acceptinglibellé entier seulement, 40 caractères maxcorrespond à Accept : le build échoue2. Répondre à l'API : non__tcfapichaque finalité, fonctionnalité etfournisseur refuséstcString "" tcloaded3. Ne jamais stocker l'enregistrementeuconsent-v2 addtl_consentOptanonConsent didomi_tokenécritures cookie et localStorageécartéesEncore à l'écran au tic suivant ?oui : le retirer, débloquer le défilementnon : le refus tient
Trois réponses et un repli. Le bouton de refus est pressé s’il existe, on répond non à tout à l’API de consentement, l’enregistrement n’est jamais stocké, et seule une bannière encore debout au tic suivant est retirée.

Elle appuie d’abord sur leur bouton de refus. Uniquement les boutons dont tout le libellé est un refus : « Reject all », « Decline », « Only essential », « Continue without accepting » et quelques autres, chacun ancré, tout ce qui dépasse 40 caractères ignoré. Un test unitaire lui donne « I Accept », « Accept All », « Agree and close », « Allow all », « Got it », « Subscribe » et « Pay £2.99/mo » et fait échouer le build si l’un d’eux correspond. Cliquer sur le mauvais bouton consentirait en votre nom, et c’est la seule chose que ce projet ne doit jamais faire. Sur msn.com, appuyer sur « Reject All » a fait disparaître la bannière de Microsoft, qui n’est pas revenue au rechargement, parce que Microsoft conserve le refus sur ses propres serveurs.

Elle répond non à l’API de consentement. Le cadre de l’IAB donne à chaque page qui recueille un consentement une fonction appelée __tcfapi pour demander ce que vous avez accepté17. Là où il y en a une, l’extension y répond avec chaque finalité, chaque fonctionnalité spéciale et chaque fournisseur refusés, une chaîne de consentement vide et eventStatus: 'tcloaded', ce qui veut dire que la réponse est définitive. Elle n’apparaît que là où un cadre de consentement est déjà présent, un site qui n’a jamais demandé ne la voit donc pas.

Elle ne stocke jamais l’enregistrement. document.cookie et localStorage écartent en silence euconsent-v2, addtl_consent, OptanonConsent, didomi_token et le reste, pour qu’un consentement que vous n’avez jamais donné ne soit jamais écrit ni rejoué auprès de 1 467 partenaires à la page suivante. Le droit britannique exige déjà un consentement avant que quoi que ce soit ne soit stocké sur votre appareil à cette fin18. L’extension fait respecter le non.

Chaque nom de cette liste est ancré aux deux bouts, et il y a une histoire derrière. Les cookies de Sourcepoint commencent par _sp_. Un motif paresseux sp_ attrape aussi sp_dc et sp_t de Spotify, qui sont la session de connexion, et m’aurait déconnecté de Spotify sur chaque page. Un test verrouille cela aussi.

Le retrait est le repli. Seulement pour une bannière encore à l’écran après le clic, et seulement une bannière réellement affichée. Plusieurs fournisseurs laissent une enveloppe permanente dans la page, qu’une bannière soit affichée ou non. euronews garde un hôte Didomi de hauteur nulle, et l’arracher casse la page sans rien gagner. Celle de Reach se trouvait trois niveaux sous <body>, dans deux enveloppes qui ne sont pas elles-mêmes fixes, la vérification doit donc descendre jusqu’à la boîte qui l’est.

Boutons de partage, et un portail qui ignore ses propres réglages

Deux autres choses sur ces pages existent pour servir quelqu’un d’autre que le lecteur, et elles demandaient un traitement différent de celui des murs.

Boutons de partage et de suivi. Chaque lien vers Facebook, Instagram, X, TikTok ou LinkedIn est réécrit en http://localhost/removeme, l’original rangé dans un attribut pour que rien ne se perde. Puis un second passage supprime ce qui est clairement du mobilier (une icône sans texte, « Share on X », tout ce qui se trouve dans un conteneur qui se dit share ou social) et masque le reste. Le marqueur est là pour qu’un seul sélecteur montre tout ce qui va partir, et un mode marquage seul s’arrête après le premier passage pour que vous puissiez regarder avant de lui faire confiance sur un site.

La version naïve casse les pages de trois façons, et chacune est désormais un test :

Version naïveCe qu’elle casseCe qui est fait à la place
a[href*="x.com"]attrape aussi netflix.com, linux.com, phoenix.comanalyser l’hôte et comparer des libellés entiers
retirer chaque lien social« Continue with Facebook » disparaît et des gens se retrouvent bloqués dehorsliens de connexion, OAuth, juridiques et développeurs laissés tranquilles
retirer un lien dans une phraseles mots partent avecmasquer par défaut, une option le déballe et garde les mots

Ce dernier est un compromis que j’ai fait en connaissance de cause. Une signature de la BBC se lit alors « follow BBC Manchester on , , and . ». J’ai regardé cela et choisi de masquer quand même. L’option qui garde les mots est là pour qui n’est pas d’accord.

MSN et Bing. Les deux tournent sur les mêmes web components, environ 160 shadow roots sur la page d’accueil. Une simple requête sur le document a trouvé 1 lien. Parcourir les shadow roots en a trouvé 73, dont les tuiles Facebook et X. Ce qui ne les parcourt pas n’en voit presque rien.

MSN a bien ses propres réglages de contenu. Ils sont conservés sur les serveurs de Microsoft, rattachés à un identifiant anonyme, pas dans un cookie, ils disparaissent donc après un effacement des cookies, dans un nouveau profil et en navigation privée. Ils n’atteignent pas du tout Bing. Et avec chacun d’eux désactivé et 11 700 pixels défilés, voici ce qu’il restait dans le flux :

Interrupteur MSN, désactivéToujours sur la page
Casual Gamesla tuile Jeux
Shoppingdes tuiles publicitaires pour Booking, Temu et eBay
Comments501 liens de commentaires
Weather, Finance, Sportspartis, jusqu’au prochain effacement des cookies

Un interrupteur nommé Comments qui laisse 501 liens de commentaires sur la page est une affirmation faite à l’utilisateur, et une affirmation fausse. L’extension les retire donc elle-même, en ciblant les noms de composants stables de MSN plutôt que ses noms de classe, qui changent à chaque build.

Une chose apprise là, qui vaut bien au-delà de MSN : retirer une image de la page ne l’empêche pas de se charger. Chrome lance la récupération dès que src est défini, même pour une image jamais insérée dans la page19. Le temps qu’un content script voie une carte, sa vignette est déjà sur le fil. Les données du flux sont donc bloquées aux deux points d’accès qui les servent, et jamais les hôtes d’images, parce que th.bing.com sert aussi la recherche d’images de Bing.

Et le flux MSN clignote encore un instant à l’écran avant de disparaître. Quatre tentatives pour le masquer plus tôt ont toutes été mesurées comme efficaces, et aucune n’a arrêté le clignotement, ce qui veut dire que ce qui est peint n’est pas ce que l’extension masque. La page de réglages le dit clairement. Je préfère qu’elle le dise plutôt que de laisser entendre un chargement propre qu’elle ne livre pas.

Ce qu’elle ne fait pas

Elle ne va pasParce que
ouvrir un vrai paywallsi le serveur retient l’article, il reste retenu
deviner pour un siteun domaine n’entre qu’après que son mur y a été vu ; deux sites de News Corp supposés identiques à Page Six ne l’étaient pas, et sont ressortis
toucher un site sans signaturesur la BBC, chaque crochet est installé et rien n’est journalisé, aucune minuterie abandonnée, aucun nœud touché
téléphoner à la maisonaucune permission d’hôte, aucun accès réseau, aucune télémétrie ; le journal d’erreurs reste dans votre navigateur
faire semblantle clignotement MSN n’est pas résolu, et une variante d’Ad-Shield, wp-ls/…, ne correspond pas encore ; sa page s’affichait bien, c’est donc consigné plutôt que corrigé au jugé

Une page qu’on vous a envoyée est à vous

Une fois qu’un serveur a envoyé une page à votre navigateur, cette copie est sur votre machine. Exécuter du code dessus ensuite pour la reprendre n’est pas un modèle économique que je reconnais. C’est le vieux tour qui consiste à vous vendre quelque chose et à garder la main dessus.

Les journaux étaient autrefois une chose qu’on achetait et qu’on possédait ensuite : quelqu’un au coin de la rue avait une pile, vous tendiez l’argent, et le journal rentrait avec vous et était à vous, pour le lire, le plier, le prêter ou allumer le feu avec. Personne ne passait une heure plus tard à la maison découper la deuxième page parce que vous aviez sauté les publicités. La version web a donné le journal pour rien, puis a vendu le lecteur. D’abord aux annonceurs, puis aux 1 467 partenaires, et maintenant à un fournisseur dont tout le produit consiste à décider si vous vous êtes assez bien conduit pour garder ce qu’on vous a donné.

Ce que je n’arrive pas à digérer, c’est la feuille de style. Un journal qui laisse le serveur d’une régie décider si sa propre mise en page survit a cédé sa propre une. Il ne l’aura pas su, parce que personne à l’intérieur ne bloquait l’hôte, si bien que le premier à s’en apercevoir a été un lecteur avec un Pi-hole, devant une page pleine de liens bruts, à qui une boîte de dialogue disait que la faute était la sienne. Elle ne l’était pas.

Le journalisme a un prix, et je n’ai aucun problème à le payer. Ce que je ne ferai pas, c’est laisser un script décider de ce que j’ai déjà. Cette ligne se trace dans mon navigateur, pas dans le leur.


  1. Pi-hole documentation — « The Pi-hole® is a DNS sinkhole that protects your devices from unwanted content, without installing any client-side software. » ↩︎

  2. Newsquest — About us — « We are the leading local news publisher in the UK with a portfolio of more than 200 brands. » Des marques ne sont pas des domaines, d’où le chiffre plus élevé tiré des certificats. ↩︎

  3. Daily Business, 18 décembre 2024 — « National World, owner of The Scotsman and Yorkshire Post, has reached agreement on a £65.1 million takeover by Irish publisher Media Concierge. » ↩︎

  4. Reach plc — About us, archivé le 5 septembre 2026 — « 120+ brands, from household names like the Mirror, Express, Daily Record and Daily Star, to local titles like MyLondon, BelfastLive and the Manchester Evening News ». ↩︎

  5. Dimova et al., « The CNAME of the Game: Large-scale Analysis of DNS-based Tracking Evasion », PETS 2021 — le CNAME cloaking « effectively bypasses antitracking measures that rely on fixed hostname-based block lists. » ↩︎

  6. uBlockOrigin/uAssets, ticket #30988, classé par les mainteneurs sous l’étiquette « Ad-Shield », avec le message servi depuis error-report.com : « Failed to load website properly since html-load.com is blocked. » Voir aussi Jacob Desforges, « Ad-Shield ad reinsertion », 12 avril 2026. L’attribution vient de la communauté des listes de filtres ; le fournisseur ne divulgue rien. ↩︎

  7. Sourcepoint — Anti-adblock FAQs, archivé le 25 mai 2022 — « Historical experience shows on average about 30% of messaged users will turn off their adblockers. » La même page demande si « a CNAME applied to a 1st-party subdomain » empêcherait le blocage du script de détection. ↩︎

  8. AdExchanger, 16 août 2023 — « InMobi acquired Quantcast’s consent management platform, called Quantcast Choice ». ↩︎

  9. UK GDPR, Article 7 — « utmost account shall be taken of whether, inter alia, the performance of a contract, including the provision of a service, is conditional on consent ». Considérant 42 : le consentement n’est pas librement donné « if the data subject has no genuine or free choice ». ↩︎

  10. ICO — Consent or pay, publié le 23 janvier 2025 — « “Consent or pay” models can be compliant with data protection law if you can demonstrate that people can freely give their consent ». La page indique désormais que ces lignes directrices sont en révision à la suite du Data (Use and Access) Act. ↩︎

  11. EDPB Opinion 08/2024, adopté le 17 avril 2024 — « In most cases, it will not be possible for large online platforms to comply with the requirements for valid consent if they confront users only with a binary choice ». Il vise les grandes plateformes en ligne, pas les journaux régionaux. ↩︎

  12. Chrome for Developers — content_scripts manifest key — « Choosing the “MAIN” world means the script will share the execution environment with the host page’s JavaScript. » ↩︎

  13. ECMAScript — CreateGlobalVarBinding — « If a binding already exists, it is reused and assumed to be initialized. » ↩︎

  14. Chrome for Developers — declarativeNetRequest — « provides implicit access to allow , allowAllRequests and block rules », et sinon « you must request host permissions before you can perform any action on a host. » ↩︎

  15. Chrome for Developers — declarativeNetRequest — « Errors and warnings about invalid static rules are only displayed for unpacked extensions. Invalid static rules in packed extensions are ignored. » ↩︎

  16. HTML Standard — the Location interface marque ses membres [LegacyUnforgeable], ce qui en Web IDL signifie « the property will be non-configurable and will exist as an own property on the object itself ». ↩︎

  17. IAB Tech Lab — TCF v2 CMP API — « Every consent manager MUST provide the following API function: __tcfapi(command, version, callback, parameter) ». ↩︎

  18. PECR, regulation 6 — « a person must not store information, or gain access to information stored, in the terminal equipment of a subscriber or user », avec le consentement comme condition dans Schedule A1, modifié par le Data (Use and Access) Act 2025. ↩︎

  19. HTML Standard — update the image data — s’exécute « whenever that element is created or has experienced relevant mutations », y compris quand son src est défini ; être dans le document n’est pas une condition. ↩︎