Skip to content
KitploitKITPLOIT
FerramentasExploitsBlog
Log in
Enviar
FerramentasExploitsBlog
Enviar

Ferramentas de Hacking, PenTest e Cibersegurança para o seu Arsenal de Segurança!

Kitploit é um diretório de ferramentas de hacking, cibersegurança e pentesting. Descubra as últimas atualizações de projetos para encontrar vulnerabilidades, analisar sistemas, automatizar testes e fortalecer sua segurança.

FeedsContatoPrivacidade© 2026 Kitploit

Diretório de Ferramentas

Categorias

Ver todas as categorias
Loading categories
Instatic-Stored-XSS-CVE-2026-103931 — Prova de conceito e análise de um XSS armazenado no filtro de URL isSafeUrl() do Instatic, onde caracteres de controle C0 iniciais contornam o bloqueio do esquema javascript:. | Kitploit
Ferramentas/GitHubGitHub/overgrowncarrot1/instatic-stored-xss-cve-2026-103931
Análise EstáticaAnálise de VulnerabilidadesAnálise de CódigoExploraçãoExploração de Aplicações WebSegurança Web
GitHubovergrowncarrot1/instatic-stored-xss-cve-2026-103931

Instatic-Stored-XSS-CVE-2026-103931

Prova de conceito e análise de um XSS armazenado no filtro de URL isSafeUrl() do Instatic, onde caracteres de controle C0 iniciais contornam o bloqueio do esquema javascript:.

Ver Repositório
há 1 diaAinda não revisado

Mais Populares

Ver todos →

Descubra as ferramentas mais usadas pela nossa comunidade.

Explore todas as ferramentas

Navegue pela nossa coleção de ferramentas

Ver todas as ferramentas →
Compartilhar

Bypass do filtro de esquema de URL em isSafeUrl() via caracteres de controle C0 iniciais

Afetado: Instatic v0.0.13 / commit 63ad5d6 (e todas as revisões anteriores que contenham src/core/html-sanitize/index.ts) Componente: src/core/html-sanitize/index.ts → isSafeUrl() / safeUrl() Classe: CWE-79 (XSS Armazenado) via CWE-184 (Lista Incompleta de Entradas Não Permitidas)

Resumo

isSafeUrl() é o único ponto de estrangulamento que bloqueia URLs javascript:, vbscript: e data: em todo o publisher. Ele normaliza a entrada com .replace(/[\t\n\r]/g, '').trim() antes de testar o prefixo do esquema.

O parser de URL WHATWG remove todos os caracteres de controle C0 iniciais (U+0000–U+001F) e espaço antes de ler um esquema. O String.prototype.trim() do JavaScript remove apenas U+0009, U+000A, U+000B, U+000C, U+000D, U+0020 e espaços Unicode — ele deixa U+0000–U+0008 e U+000E–U+001F no lugar.

Assim, uma URL prefixada com, por exemplo, U+0001 é reportada como segura pela guarda, enquanto todo navegador a analisa e executa como o esquema javascript:.

Prova de conceito

Contra o src/core/html-sanitize/index.ts não modificado:

payload           : "\x01javascript:alert(document.domain)"
isSafeUrl()       : true          <-- guard reports "safe"
WHATWG URL scheme : javascript:   <-- what the browser actually runs
safeUrl() output  : "\x01javascript:alert(document.domain)"   (NOT collapsed to "#")

27 dos 32 caracteres de controle C0 contornam a verificação. U+0000 é neutralizado pela análise de atributos HTML (NUL → U+FFFD), deixando 26 prefixos exploráveis de forma confiável (U+0001–U+0008, U+000E–U+001F). O mesmo bypass derrota os filtros vbscript: e data:.

A suíte existente (src/__tests__/publisher/utils.test.ts) cobre case folding e tabs incorporadas (java\tscript:), mas nunca um caractere de controle inicial, razão pela qual isso não foi detectado.

Caminho ponta a ponta

base.link declara href: { type: 'url' } e LinkPropsSchema o tipa como um Type.String({ default: '#' }) sem restrições — não há validação de URL no momento da escrita. Portanto:

  1. escapeProps() encaminha props do tipo type: 'url' | 'image' | 'media' para isSafeUrl(value) ? value : '#' — o payload passa bruto e deliberadamente sem escape HTML.
  2. O render() de base.link emite `<a href="https://github.com/overgrowncarrot1/instatic-stored-xss-cve-2026-103931/blob/main/%24%7BsafeUrl%28props.href%29%7D" …>`. safeUrl() verifica novamente com o mesmo isSafeUrl() quebrado, depois escapeHtml() — que só escapa & < > " ' e não toca em caracteres de controle.
  3. O payload chega literalmente no atributo href: <a href="https://github.com/overgrowncarrot1/instatic-stored-xss-cve-2026-103931/blob/main/%5Cx01javascript%3Aalert%28document.domain%29" target="_self">Click me</a>

Sinks afetados

Todos estes passam pelo mesmo isSafeUrl():

SinkArquivo
Toda prop de módulo url / image / media (link href, button href, image src, video src/poster, form action, form redirectUrl)src/core/publisher/escapeProps.ts:108
Atributos HTML personalizados arbitrários definidos pelo usuário em qualquer nósrc/core/htmlAttributes/attributes.ts:66
href e src de link/imagem Markdownsrc/core/markdown/renderMarkdown.ts:75
faviconUrl do sitesrc/core/publisher/render.ts:322
Chamada safeUrl() de todo módulo basesrc/modules/base/utils/escape.ts

Impacto

Escalação de privilégios de um editor de baixo privilégio para comprometimento total de admin.

O papel Client embutido possui site.content.edit, o que é suficiente para definir um href de link ou um atributo HTML personalizado. A URL injetada então é renderizada no canvas do editor de admin, que é um iframe srcdoc — mesma origem que /admin.

server/securityHeaders.ts:69-72 define apenas frame-ancestors 'none'; base-uri 'self'; object-src 'none' em /admin, com um comentário de código explícito de que uma política script-src é deliberadamente não definida ainda. Sem script-src, nada impede que uma URL javascript: seja executada na origem do admin.

Quando um Owner ou Admin abre a página afetada no editor e ativa o elemento, o payload é executado na mesma origem que a SPA de admin. O cookie de sessão é HttpOnly, então não pode ser lido diretamente — mas o payload pode conduzir a API de admin como a vítima (criar uma conta de owner, ler segredos ou instalar um plugin, cujo entrypoint de servidor é um caminho para execução de código).

No site publicado o impacto é mais restrito: cspPlan.ts define script-src 'none' (ou 'self'), o que bloqueia URLs javascript:. No entanto server/publish/frontendInjections.ts:377 relaxa isso para 'self' 'unsafe-inline' para qualquer página que carregue um script inline, e 'unsafe-inline' volta a permitir URLs javascript: — então XSS no site publicado é alcançável nessas páginas.

Observe que a docstring de attributes.ts já identifica exatamente esse modelo de ameaça ("no site publicado E, mais seriamente, dentro do canvas do editor de admin (mesma origem que /admin)") — a guarda simplesmente não o implementa completamente.

Verificação

Confirmado em um navegador (Chromium, iframes srcdoc reproduzindo a CSP de cada origem, renderizando a saída byte a byte do próprio safeUrl() do repositório):

Origem reproduzidaCSP aplicadaResultado
Canvas do editor de admin (/admin)nenhuma — como securityHeaders.ts emitejavascript: executado
Página publicada (baseline)script-src 'none' — como cspPlan.ts emitebloqueado (script-src-elem)
Página publicada com um script inlinescript-src 'self' 'unsafe-inline' — como frontendInjections.ts:377 emitever nota

As duas primeiras linhas são resultados observados. A execução na origem de admin reportou document.domain como a origem de serviço, confirmando execução na mesma origem em vez de um contexto opaco.

Ressalva sobre fidelidade: o harness aplica cada política via <meta http-equiv>, enquanto o Instatic a envia como cabeçalho de resposta HTTP. Eles são equivalentes para a aplicação de script-src, mas uma reprodução em uma instância real de bun run dev carregaria os cabeçalhos genuínos.

Correção

Remova toda a faixa C0 + espaço em vez de confiar em trim(). Isso é precisamente o que a própria regex isJavaScriptProtocol do React faz com seu prefixo ^[\u0000-\u001F ]* — uma verificação cruzada útil de que esta é uma classe de bypass conhecida e do mundo real, e não teórica.

--- a/src/core/html-sanitize/index.ts
+++ b/src/core/html-sanitize/index.ts
@@ -30,7 +30,11 @@ export function escapeHtml(value: unknown): string {
   * normalisation browsers apply during URL parsing.
   */
  export function isSafeUrl(url: string): boolean {
-  const normalized = url.replace(/[\t\n\r]/g, '').trim().toLowerCase()
+  const normalized = String(url ?? '')
+    .replace(/[\t\n\r]/g, '')
+    .replace(/^[\u0000-\u0020]+/, '')
+    .replace(/[\u0000-\u0020]+$/, '')
+    .toLowerCase()
    return (
      !normalized.startsWith('javascript:') &&
      !normalized.startsWith('vbscript:') &&

Verificado contra o arquivo corrigido: o payload é bloqueado, safeUrl() o colapsa para #, 0 de 99 URLs perigosas prefixadas com controle/espaço permanecem aceitas, e todos os 14 casos de teste existentes de isSafeUrl se comportam de forma idêntica.

Teste de regressão sugerido

it('blocks javascript: behind a leading C0 control character', () => {
  for (let i = 0x00; i <= 0x1f; i++) {
    expect(isSafeUrl(String.fromCharCode(i) + 'javascript:alert(1)')).toBe(false)
  }
})

Defesa em profundidade

Baixar ferramenta