AkiraAkira.dev
4 min de leitura

A Akira UI 2.6.0 saiu com atraso por causa da review

Com um tab no meio, javascript: passava pelo guard e o teste ficava verde. A review antes da tag encontrou a falha e a mesma no editor, publicada desde a 1.1.0.

também em EN FR

java<TAB>script:alert(1), com um tab verdadeiro entre java e script, passava pelo guard que devia recusar links perigosos nas ações do toast da Akira UI. O browser tirava o tab, ficava com javascript:alert(1) e executava o script ao primeiro clique.

A mensagem do commit que trouxe esse guard era “fix(toast): hold the toast open when an action fails, and vet its link”. Tinha teste, e o teste passava.

A 2.6.0 tinha 31 commits à espera: gráficos de área, de barras, de linhas e donut com paleta de séries, ações no toast, tons no Button, bleed na Table e a legenda “Required” traduzível de que um consumidor precisava. Antes de criar a tag, fez-se uma review de segurança sobre o diff desde a v2.5.0. Foi a melhor decisão da release.

Porque é que o teste não via nada

// src/components/ui/toast.tsx, tal como o commit do toast o deixou
const SCHEME = /^([a-z][a-z0-9+.-]*):/i;
const NAVIGABLE_SCHEMES = new Set(['http', 'https', 'mailto', 'tel']);

function safeToastHref(href: string): string {
    const scheme = SCHEME.exec(href.trim());

    if (!scheme) {
        return href;
    }

    return NAVIGABLE_SCHEMES.has(scheme[1].toLowerCase()) ? href : '#';
}

O parser de URL do WHATWG limpa o endereço antes de procurar o esquema: retira tab, LF e CR de qualquer posição, e caracteres de controlo C0 das duas extremidades. A expressão regular não faz nada disso. Com o tab no meio, não encontra esquema e o guard devolve o valor intacto. Com um carácter de controlo no início, acontece o mesmo, porque trim() não o apaga.

O único teste usava javascript:alert(1) tal e qual, precisamente o caso que a regex já apanhava. Com este código, o teste continuaria verde para sempre.

Este guard nunca chegou ao npm. Não existe na v2.5.0.

O guard do editor chegou ao npm

Ao puxar o fio à meada, a review chegou a isSafeEditorUrl em src/components/ui/editor/extensions.ts, com a mesma regex e o mesmo trim(). É ele que decide que links entram no editor TipTap: no paste, no autolink, no HTML guardado e na caixa de diálogo de links. A documentação do editor garantia que um endereço javascript: era recusado no paste e no diálogo.

Este esteve publicado. Entrou com o editor na v1.1.0, a 7 de agosto, e ficou em todas as versões até à v2.5.0. Foram seis semanas no npm. O advisory é o GHSA-r2w8-25qp-4gw4, de severidade média, e abrange as versões desde a 1.1.0 até à 2.6.0, exclusive. Estão todas marcadas como deprecated no npm, e quem instala uma delas recebe um aviso que remete para o advisory. Uma aplicação que use o editor tem de passar para a 2.6.0.

Um só guard e mais uma regressão

O mesmo erro em dois sítios pedia uma só implementação. As duas verificações passaram para 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);
}

O toast e o diálogo de link guardam agora o valor normalizado, e não o que receberam. Os testes novos constroem os caracteres com String.fromCharCode, para o tab e o carácter de controlo existirem de facto na string, e cada um falha quando se repõe a normalização antiga. Confirmou-se por mutação do código.

A review do próprio fix ainda encontrou um problema. A primeira versão de normalizeUrl só cortava o início, e o diálogo passou a guardar href="https://akira-io.com ", com um espaço no fim. Nada explorável, mas uma regressão criada pela correção. Ficou resolvida antes do merge.

O que a espera custou

Custou tempo a quem esperava pela legenda traduzível. É um custo real, e foi pago.

Custou também uma correção. Duas séries cujas chaves, depois de sanitizadas, davam o mesmo nome de custom property apareciam com a mesma cor. A review do fix encontrou duas regressões: com um config do consumidor, as duas séries voltavam a ter a mesma cor, e o tooltip e a legenda mostravam o nome da série errada. O fix ficou de fora e o defeito continua aberto. A 2.6.0 sai com ele assumido, em vez de o trocar por dois novos.

A alternativa era criar a tag no dia previsto e publicar um segundo guard com o mesmo bypass, ao lado do primeiro, já público há seis semanas, com uma bateria de testes a dar os dois como seguros.

Os testes respondem ao que alguém se lembrou de perguntar. A review antes da tag existe para perguntar o resto.

partilhar