AkiraAkira.dev
4 min de lecture

Akira UI 2.6.0 est sortie en retard, et c'est grâce à la revue

L'éditeur devait refuser javascript:. Depuis la v1.1.0, une tabulation suffisait pour passer. La revue menée avant la release a mis la faille au jour.

aussi en EN PT

La documentation de l’éditeur d’Akira UI était formelle : une adresse javascript: est refusée, au collage comme dans la boîte de dialogue des liens. C’était vrai pour javascript:alert(1). Avec une tabulation entre java et script, c’était faux, et cela l’est resté de la v1.1.0 à la v2.5.0.

Aucun test ne l’a signalé. La faille a été découverte parce que la v2.6.0 a attendu une revue de sécurité avant d’être taguée.

Ce que le navigateur fait d’une tabulation

Le guard s’appelle isSafeEditorUrl, dans src/components/ui/editor/extensions.ts. Voici la version publiée en v2.5.0 :

// src/components/ui/editor/extensions.ts, v2.5.0
const SAFE_PROTOCOLS = ['http', 'https', 'mailto', 'tel'];

export function isSafeEditorUrl(url: string): boolean {
    const protocol = /^([a-z][a-z0-9+.-]*):/i.exec(url.trim())?.[1];

    return (
        protocol === undefined ||
        SAFE_PROTOCOLS.includes(protocol.toLowerCase())
    );
}

Avant de chercher un schéma, le parseur d’URL du WHATWG retire les tabulations, les sauts de ligne et les retours chariot où qu’ils soient, ainsi que les caractères de contrôle C0 et les espaces aux deux extrémités. L’expression régulière ne fait rien de tout cela. Avec java<TAB>script:alert(1), elle ne trouve aucun schéma, la fonction renvoie true, et le navigateur reconstitue javascript:alert(1) au clic. Un caractère de contrôle en tête produit le même effet, puisque trim() ne l’enlève pas.

Ce guard protège toutes les entrées de liens de l’éditeur TipTap : le collage, l’autolink, le HTML enregistré et la boîte de dialogue. Il est arrivé avec l’éditeur en v1.1.0, le 7 août, et il est resté six semaines sur npm. L’advisory GHSA-r2w8-25qp-4gw4, de sévérité moyenne, couvre les versions à partir de 1.1.0 et antérieures à 2.6.0. Toutes sont désormais marquées comme deprecated sur npm, et leur installation affiche un avertissement qui cite l’advisory. Si vous utilisez l’éditeur, passez à la 2.6.0.

Comment la revue est remontée jusque-là

Trente et un commits attendaient la v2.6.0 : des graphiques en aires, en barres, en lignes et en donut avec leur palette de séries, des actions dans le toast, des tons sur le Button, du bleed sur la Table, et la légende « Required » traduisible dont un projet qui utilise la bibliothèque avait besoin.

L’un de ces commits s’intitulait « fix(toast): hold the toast open when an action fails, and vet its link ». Il ajoutait au toast la même expression régulière, appliquée elle aussi au résultat de trim(). Son unique test passait javascript:alert(1) en toutes lettres, le seul cas que l’expression savait déjà attraper, et il serait resté vert indéfiniment. La revue du diff depuis la v2.5.0 a repéré le contournement dans le toast. En tirant le fil, elle est remontée jusqu’au guard de l’éditeur. Celui du toast n’a jamais été publié ; celui de l’éditeur l’était depuis des semaines.

Un seul guard, écrit comme le parseur

Les deux vérifications se trouvent désormais dans src/lib/safe-url.ts :

// src/lib/safe-url.ts
const STRIPPED_BY_THE_URL_PARSER = /[\t\n\r]/g;
const EDGE_C0_OR_SPACE = /^[\u0000-\u0020]+|[\u0000-\u0020]+$/g;
const SCHEME = /^([a-z][a-z0-9+.-]*):/i;

export const NAVIGABLE_SCHEMES = ['http', 'https', 'mailto', 'tel'];

export function normalizeUrl(url: string): string {
    return url
        .replace(STRIPPED_BY_THE_URL_PARSER, '')
        .replace(EDGE_C0_OR_SPACE, '');
}

export function hasNavigableScheme(url: string): boolean {
    const scheme = SCHEME.exec(normalizeUrl(url))?.[1]?.toLowerCase();

    return scheme === undefined || NAVIGABLE_SCHEMES.includes(scheme);
}

Le toast et la boîte de dialogue des liens enregistrent maintenant la valeur normalisée. Les nouveaux tests fabriquent leurs caractères avec String.fromCharCode, pour que la tabulation et le caractère de contrôle soient bien présents, et chacun échoue dès qu’on remet l’ancienne normalisation. Une mutation du code l’a confirmé.

La revue du correctif a encore relevé un problème. La première version de normalizeUrl ne nettoyait que le début de la chaîne, et la boîte de dialogue enregistrait href="https://akira-io.com ", avec une espace finale. Rien d’exploitable, mais une régression introduite par le correctif lui-même, réparée avant le merge.

Le prix de l’attente

Attendre a un coût, et on l’a payé. La légende traduisible est arrivée plus tard dans le projet qui en avait besoin.

L’attente a aussi retiré un correctif de la release. Deux séries dont les clés, une fois assainies, donnaient le même nom de custom property s’affichaient dans la même couleur. La revue du correctif y a décelé deux régressions : avec un config fourni par le projet consommateur, les deux séries reprenaient la même couleur, et le tooltip comme la légende affichaient le mauvais nom de série. Le correctif a été abandonné, le défaut reste ouvert, et la v2.6.0 le livre en connaissance de cause plutôt que de l’échanger contre deux nouveaux.

Taguer à l’heure, c’était publier un second guard avec le même contournement, à côté d’un premier déjà public depuis six semaines, avec une suite de tests qui les déclarait sûrs tous les deux.

Une documentation dit ce que le code devrait refuser. Seule une revue vérifie ce qu’il refuse vraiment.

partager