sMug@replicatorbe
~/Publications/Forensic/les-huit-endroits-ou-une-date-se-cache-dans-un-site

Les huit endroits où une date se cache dans un site

Forensic / /13 min de lecture

Une page affiche « publié le 12 mars 2019 ». C'est du texte, ça ne vaut rien. Mais la même date est écrite à huit autres endroits du site, dont deux que personne ne pense à corriger — et quatre de plus qui échappent complètement à celui qui publie.

Une page affiche « publié le 12 mars 2019 ». C'est une ligne de texte. Elle vaut ce que vaut celui qui l'a tapée, et n'importe qui la change en trois secondes.

La question intéressante n'est donc pas de la croire ou non. C'est de savoir combien d'autres endroits, dans le même site, portent la même date — et si tous racontent la même histoire.

La question m'est venue par l'autre bout : en migrant un vieux site d'un moteur à un autre, j'ai découvert que l'opération avait écrit la date du jour dans six emplacements sur huit. Des articles de 2012 se présentaient comme publiés le matin même. Corriger ça oblige à faire l'inventaire complet, et l'inventaire est plus long qu'on ne croit.

Rien de ce qui suit n'est secret : tout est dans les spécifications HTTP depuis vingt-cinq ans. C'est simplement rarement regardé ensemble.

1. L'en-tête Last-Modified

Le plus direct. Le serveur ne l'invente pas : il le dérive de la date de dernière modification du fichier sur le disque.

curl -sI https://exemple.tld/blog/article/ | grep -i last-modified

Une réserve immédiate, et elle est de taille : cet en-tête date le fichier, pas le contenu. Un générateur statique qui régénère tout le site remet chaque page à la date du build. Sur beaucoup de sites, les deux cents pages renvoient donc la même valeur — celle du dernier déploiement. Ce n'est pas une déception : c'est déjà une information, et elle ne concerne pas l'article, elle concerne l'exploitant.

2. L'ETag, qui répète la même chose sans qu'on le remarque

C'est mon préféré, parce qu'il survit à la précaution.

Sous nginx, l'ETag par défaut n'est pas un identifiant opaque : c'est la date de modification en hexadécimal, un tiret, la taille en hexadécimal.

etag: "67644684-887c"

0x67644684 = 1734624900 secondes depuis 1970, soit le 19 décembre 2024 à 16h15 UTC. 0x887c = 34940, et c'est très exactement le content-length de la réponse.

Autrement dit : un administrateur qui retire Last-Modified en pensant masquer la date du fichier n'a rien masqué du tout, il l'a juste déplacée deux lignes plus bas en base 16. Apache a sa propre recette — historiquement inode, taille et mtime — et un W/ en préfixe signale un ETag faible, souvent posé par la couche de compression.

C'est la leçon générale de tout ce billet : une même information sort par plusieurs tuyaux, et on ne pense à fermer que celui qu'on voit.

3. Le sitemap.xml

Chaque entrée porte un <lastmod>. Il est déclaratif, donc faux quand le générateur est paresseux — mais il a un avantage que les autres n'ont pas : il énumère des adresses. Y compris celles qui ne sont plus liées depuis aucune page.

4. Le flux RSS ou Atom

<pubDate>, <updated>, et le fuseau horaire déclaré à côté. Surtout, un flux se recopie : agrégateurs, lecteurs, caches. Une date retouchée sur le site reste intacte dans une copie du flux prise six mois plus tôt, chez quelqu'un d'autre.

5. Le balisage de la page

Le JSON-LD (datePublished, dateModified), l'OpenGraph (article:published_time), l'attribut datetime d'une balise <time>. Ces trois-là sont souvent plus précis que ce que la page affiche à l'écran — à la seconde et au fuseau près — parce qu'ils sortent du moteur et pas de la plume.

Et c'est le deuxième endroit qu'on oublie de corriger. Quelqu'un qui antidate un article change le titre, change le texte visible, et laisse le <script type="application/ld+json"> dire la vérité en bas de page.

6. L'adresse de la page

Deux cas, et le second est le plus courant des deux.

?p=1234, /?id=57, un nom de fichier numéroté : un identifiant de ce genre est un compteur. Il ne dit pas quand, il dit dans quel ordre — et ça suffit. Si les articles 1230 et 1240 sont datés de façon crédible, l'article 1234 est encadré, et aucune retouche de la date affichée ne défait cet encadrement.

L'autre cas, c'est la date écrite en clair dans l'adresse. La plupart des moteurs de blog le font par défaut, et beaucoup de sites ont gardé ce réglage sans jamais se demander pourquoi :

/2019/03/12/mon-article/
/blog/2019/03/mon-article.html
/archives/2019/03/12/mon-article

Ce n'est plus un indice, c'est la date elle-même — publiée à l'endroit le plus visible de la page, recopiée dans chaque lien entrant, dans chaque partage, dans chaque signet. Et c'est celle qui coûte le plus cher à corriger : changer l'adresse, c'est casser les liens. Le moteur pose alors une redirection permanente de l'ancienne vers la nouvelle, et cette redirection conserve précisément ce qu'on voulait effacer.

curl -sI https://exemple.tld/2019/03/12/mon-article/ | grep -iE '^(HTTP|location):'

Une 301 qui part d'une adresse datée de 2019 est une déclaration de l'exploitant lui-même : la page a bien vécu là, sous cette date.

C'est la raison pour laquelle je préfère des adresses sans compteur et sans date. Pas par cachotterie : parce que l'adresse d'une page doit désigner son sujet, pas le jour où je l'ai écrite. Un article corrigé trois ans plus tard reste à la même adresse, et cette adresse ne ment sur rien.

7. La place dans les listes

L'index, la pagination, les archives par mois, les pages de catégorie. Ces pages sont triées par un critère que le moteur applique sans réfléchir — souvent l'ordre d'insertion en base, pas la date affichée. Un article antidaté remonte alors mal : il est daté de 2012 et se trouve coincé entre deux articles de 2024.

8. L'heure

La plus bavarde, et celle à laquelle personne ne pense.

Quarante articles à 00:00 : ce n'est pas une habitude de publication, c'est un import — la date a été saisie sans heure, et le moteur a complété par des zéros. Un lot entier à la même minute : une migration. Des heures réparties entre 7h et 23h, avec un creux l'après-midi : un humain qui écrit, dans un fuseau qu'on peut déduire.

Le coût de la retouche

Ces huit-là appartiennent tous à celui qui publie. Ce qui compte, c'est ce que chacun coûte à corriger :

EndroitCe qu'il donneCoût de la retouche
Last-Modifieddate du fichierun touch
ETagla même, en hexadécimalle même touch
sitemap.xmldate déclarée + adresses oubliéesune régénération
Flux RSSdate déclarée, fuseau, copies chez des tiershors de portée une fois diffusé
JSON-LD / OpenGraphdate à la secondeune modification manuelle, souvent oubliée
Adresse de la pagel'ordre, ou la date en clairimpossible sans casser les liens
Ordre dans les listesl'ordre réel de créationdépend du moteur
Heurele mode de saisienécessite d'y avoir pensé

Les deux lignes qui portent — l'adresse et l'ordre — sont précisément celles qui ne se corrigent pas d'un coup de commande.

Les quatre que le site ne contrôle pas

Le reste échappe entièrement à l'exploitant, et c'est là que ça devient intéressant.

Les archives du web. L'Internet Archive conserve la date de sa première capture d'une adresse, et toutes les suivantes. Une interrogation suffit :

curl -s "http://archive.org/wayback/available?url=exemple.tld/blog/article/"

Avec la nuance obligatoire : c'est la date de capture, pas de publication. Un robot ne passe pas le jour de la mise en ligne, et une page peut avoir été capturée parce que quelqu'un l'a soumise à la main. Ça donne une borne supérieure — la page existait au plus tard à cette date — et une borne supérieure, en datation, c'est déjà beaucoup.

Les moteurs de recherche. Savoir si une page est indexée, et depuis quand, est un indice à part entière : une adresse absente de tous les index alors que le reste du site y est, ça se remarque. Google accepte encore d'encadrer une recherche avec before: et after: ; la commande cache:, elle, a disparu fin 2024, avec les liens vers les versions en cache — ce qui était l'outil le plus commode de la panoplie. Restent Bing, les moteurs qui maintiennent leur propre index, et les copies moins bien tenues qui gardent parfois de vieilles versions d'une page longtemps après sa modification.

Les liens entrants. C'est la mine d'or de la catégorie, et de loin. Un lien vers une page est daté par la page qui le porte — et celle-là appartient à quelqu'un d'autre. Chercher le plus ancien lien connu vers un domaine ne date donc pas une page précise : ça dit à partir de quand le site a existé pour les autres, le moment où il a commencé à être lu, cité, repris. C'est une mesure de notoriété, pas de publication.

Elle a pourtant une propriété que les autres indices n'ont pas : elle se retourne. Si un lien daté de 2017 pointe vers une page qui se présente comme publiée en 2019, il n'y a que trois explications possibles — l'adresse a été réutilisée pour un autre contenu, le lien a été modifié après coup, ou la date affichée est fausse. Aucune des trois n'est neutre, et c'est à l'exploitant du site de dire laquelle.

Et le premier gisement, ce sont les réseaux sociaux — qu'on oublie systématiquement parce qu'on cherche des « liens entrants » au sens du référencement, c'est-à-dire des pages web. Un message qui partage une adresse est horodaté à la minute, publiquement, sur une machine qui n'appartient pas à celui qui publie. Et il ne reste pas seul : il est repartagé, cité, commenté, et chaque reprise porte à son tour sa propre date. Supprimer un lien de son site est l'affaire d'une seconde ; le retirer de vingt fils de discussion ne se fait pas.

Avec un bonus que je vois rarement exploité : l'aperçu. Quand un lien est partagé, la plateforme fabrique une carte — titre, description, image — en lisant la page au moment du partage, puis elle la conserve telle quelle. C'est une capture miniature et datée de l'état de la page ce jour-là. Un titre d'aperçu qui ne correspond plus au titre actuel, c'est une modification qu'on sait désormais situer dans le temps.

À côté de ça, les sources que personne ne peut réécrire : les archives horodatées de listes de diffusion, l'historique d'édition d'un article encyclopédique — daté à la minute, avec l'adresse citée dans le différentiel —, un fil de forum, un message public. Les outils commerciaux d'analyse de liens viennent après, pas avant.

Avec une élégance qui plaît : pour dater le lien, on applique à la page qui le porte exactement la même grille que ci-dessus. Les huit endroits, à nouveau, un cran plus loin.

Les fichiers déposés à côté. C'est souvent le maillon faible, pour une raison simple : on les dépose une fois et on n'y touche plus jamais. Une refonte réécrit toutes les pages, régénère tout le HTML — et laisse les fichiers joints exactement où ils étaient, avec leur date d'origine intacte. Ils survivent à l'opération qui a effacé tout le reste.

Ça commence par le plus direct : une image est une ressource comme une autre, donc elle a ses propres en-têtes.

curl -sI https://exemple.tld/media/visuel.jpg | grep -iE '^(last-modified|etag):'

S'y ajoutent les métadonnées EXIF quand personne ne les a nettoyées, le nom du fichier lui-même (IMG_20190312_101544.jpg ne demande pas d'analyse), le chemin de dépôt quand le moteur range par année et par mois, et jusqu'au format : un visuel en WebP ne date pas de 2009.

Mais il n'y a aucune raison de s'arrêter aux images — tout ce qui est téléchargeable depuis le site relève du même traitement, et le reste est souvent plus bavard :

  • un PDF transporte une date de création et une date de modification dans ses métadonnées, plus le nom et parfois la version du logiciel qui l'a produit — ce qui date la chaîne d'outils autant que le document ;
  • un document bureautique moderne est une archive compressée : les propriétés déclarent l'auteur, la date de création, celle de dernière modification et le temps d'édition cumulé, et chaque fichier à l'intérieur de l'archive porte en plus son propre horodatage ;
  • une archive (.zip, .tar.gz) conserve la date de chaque fichier qu'elle contient, c'est-à-dire un instantané du disque de quelqu'un au moment où il l'a fabriquée ;
  • un simple fichier texte ou un export .csv ne contient rien, lui — et retombe donc entièrement sur le Last-Modified et l'ETag du serveur, c'est-à-dire sur les deux premiers points de ce billet.

On récupère, on lit les métadonnées, et on tente le rapprochement avec la page qui les présente. L'intérêt n'est pas la date isolée du fichier : c'est l'écart. Une note de service jointe à un article, créée deux ans après la date affichée de l'article, pose une question à laquelle il faut bien répondre quelque chose.

Ce que tout ça vaut

Il faut dire la limite avant de conclure, sinon la conclusion ne vaut rien non plus.

Aucun de ces douze indices ne prouve quoi que ce soit. Chacun est falsifiable, et plusieurs le sont d'une seule commande. Un site statique correctement tenu aligne spontanément les huit premiers, sans la moindre intention de tromper : c'est même le signe d'un bon outil, et celui qui remet en ligne ses propres archives fait exactement ça.

Donc la cohérence ne démontre rien de plus qu'elle-même. Ce qui parle, c'est la contradiction. Un JSON-LD à 2019 sous un ETag à 2025. Un sitemap qui ignore une page que le flux RSS liste encore. Un document joint dont les métadonnées sont postérieures de deux ans à l'article qui le propose. Un lien entrant plus vieux que la page vers laquelle il pointe. Là, on ne tient toujours pas une date — on tient un écart, et un écart demande une explication.

C'est la même méthode que pour reconnaître quelqu'un derrière une adresse : on ne cherche pas l'indice décisif, on cherche la constellation. Un point isolé se retourne d'une phrase. Un faisceau d'indices indépendants, obtenus par des chemins qui ne communiquent pas entre eux, ça ne se retourne plus aussi facilement — et ça, c'est déjà un résultat.

Un dernier détail, que je tiens de mes installations de vidéosurveillance et que j'ai raconté ailleurs : l'en-tête date: d'une réponse HTTP, c'est l'horloge de la machine qui a fabriqué cette réponse. Si elle dérive, tout ce qu'elle horodate dérive avec elle, silencieusement.

Encore faut-il savoir de quelle machine on parle. Ce qui précède vaut pour le cas simple — un serveur qui sert ses propres fichiers, et c'est celui que j'ai pris tout du long. Dès qu'un réseau de diffusion ou un cache s'intercale, le date: qu'on lit est celui du nœud qui a répondu, pas celui de l'origine. Et si la réponse sort du cache, la spécification veut que le date: de l'origine soit conservé et qu'un en-tête age: annonce depuis combien de temps elle y dort : date: + age: doit retomber sur l'heure courante. Quand l'addition ne tombe pas juste, ce n'est pas une preuve de dérive d'horloge — c'est d'abord le signe qu'on ne parle pas à la machine qu'on croit.

Les autres en-têtes le disent aussi, quand on les regarde : un via:, un x-cache, un server qui ne correspond à rien, un etag devenu faible (W/) parce qu'un intermédiaire a recompressé le contenu en chemin. Dans ce cas de figure, last-modified et etag peuvent parfaitement décrire la copie détenue par le cache plutôt que le fichier d'origine. Ce n'est pas un obstacle, c'est une précaution à prendre avant de conclure quoi que ce soit.

Avant de dater une page, ça vaut donc la peine de savoir à quelle machine on est en train de parler — et si cette machine-là sait elle-même quelle heure il est.