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.
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.