
Um usuário do Docmost com privilégios baixos poderia fornecer um attachmentId de vítima ao endpoint de upload genérico e sobrescrever o anexo armazenado de outra página dentro do mesmo workspace.
Um usuário do Docmost com baixos privilégios poderia fornecer um attachmentId de uma vítima para o endpoint genérico de upload e sobrescrever um anexo armazenado de outra página dentro do mesmo workspace.
Identifiquei, divulguei de forma responsável e reproduzi uma falha de autorização de Alta gravidade no Docmost, a plataforma de documentação colaborativa de código aberto.
O site oficial do Docmost apresenta a plataforma como uma wiki corporativa pronta para uso on-premise com 3M+ downloads, e afirma ser confiável por equipes de organizações como Vilnius City, Bechtle, o Governo Australiano, a Cruz Vermelha e ETS Quebec.
O bug residia no caminho genérico de upload de arquivos que o Docmost também utiliza para os fluxos de salvar/atualizar diagramas.
Eu estava revisando esse código com uma pergunta muito específica em mente:
O que acontece se o endpoint de upload comprova acesso de edição em uma página, mas o alvo da sobrescrita é selecionado com um ID de anexo separado, controlado pelo usuário?
Neste caso, essa pergunta levou diretamente a uma falha real de vinculação de objeto.
O Docmost permitia que um chamador enviasse:
pageId para uma página que ele tinha permissão de editar, eattachmentId pertencente a uma página diferente no mesmo workspaceO servidor realizava uma verificação de consistência da sobrescrita, mas a proteção usava a lógica booleana errada.
Isso significava que a requisição poderia passar pela autorização e sobrescrever o anexo da vítima de qualquer forma.
Esse problema tornou-se CVE-2026-34213.
Docmost: docmost/docmost
Advisory: GHSA-89fp-2hch-j9gp
CVE: CVE-2026-34213
Corrigido na versão: v0.71.0

pageId controlado pelo atacante com acesso de edição -> attachmentId da vítima controlado pelo atacante -> proteção de sobrescrita falha trata a sobrescrita entre páginas como válida -> caminho de armazenamento reconstruído a partir do attachmentId da vítima -> bytes do atacante substituem o arquivo da vítima -> página da vítima continua servindo o anexo modificado
O Docmost armazena anexos de páginas enviados como registros no banco de dados, além de arquivos de suporte no armazenamento.
Para uploads normais, o servidor cria um novo ID de anexo e escreve um novo arquivo.
Para fluxos de salvar/atualizar diagramas, no entanto, o cliente intencionalmente reutiliza um attachmentId existente para que o mesmo arquivo de diagrama possa ser atualizado no lugar, em vez de gerar um novo registro de anexo a cada vez.
Esse comportamento é legítimo por si só.
O problema é que ele cria um caminho de alto risco:
Sempre que um endpoint combina essas duas responsabilidades, a implementação deve vinculá-las exatamente.
O Docmost não o fez.
Endpoints mistos de criação/atualização são locais comuns para bugs de autorização.
O motivo é simples:
Esse é exatamente o padrão aqui.
POST /api/files/upload validava que o chamador podia editar a página nomeada por pageId.
Mas se attachmentId também fosse fornecido, o servidor entrava em um caminho de sobrescrita e selecionava um registro de anexo existente separadamente.
Isso gerou a questão crítica de segurança:
o caminho de sobrescrita comprova que o anexo selecionado realmente pertence à página autorizada?
A resposta nas versões vulneráveis era não.
A causa raiz foi um desvio de autorização através de uma chave controlada pelo usuário, combinado com um bug de lógica booleana na proteção de sobrescrita.
O fluxo vulnerável era assim:
AttachmentController.uploadFile() lia pageId dos dados do formulário multipart.validateCanEdit(page, user).attachmentId opcional da mesma requisição.AttachmentService.uploadFile() carregava o anexo existente por esse ID fornecido pelo atacante.&& em vez de rejeitar em qualquer discrepância.A proteção vulnerável era:
if (
existingAttachment.pageId !== pageId &&
existingAttachment.fileExt !== preparedFile.fileExtension &&
existingAttachment.workspaceId !== workspaceId
) {
throw new BadRequestException("File attachment does not match");
}
Essa condição só rejeitava a requisição se:
tudo ao mesmo tempo.
Isso é o oposto do que uma proteção de sobrescrita deveria fazer.
Para o caso real de ataque, o atacante intencionalmente permanecia dentro do mesmo workspace.
Então:
existingAttachment.workspaceId !== workspaceId era falseAssim que esse operando se tornava false, toda a condição && era avaliada como false, mesmo que o anexo pertencesse a uma página diferente.
Então o servidor tratava uma sobrescrita entre páginas como válida.
Essa foi a primeira metade do bug.
A segunda metade é o que tornou o impacto real.
Após a verificação, o serviço reconstruía o caminho de armazenamento de destino usando o attachmentId e o nome do arquivo fornecidos pelo atacante:
const filePath =
`${getAttachmentFolderPath(AttachmentType.File, workspaceId)}/` +
`${attachmentId}/${preparedFile.fileName}`;
Em seguida, no caminho de atualização, o Docmost apenas atualizava metadados mutáveis como:
fileSizeupdatedAtEle não reassociava a propriedade à página do atacante.
Assim, a página da vítima continuava apontando para o mesmo registro de anexo e para o mesmo ID de anexo. Apenas os bytes do arquivo subjacente mudavam.
É por isso que isso não era uma incompatibilidade inofensiva.
Era uma primitiva persistente de sobrescrita não autorizada.
Isso não era um bug cosmético nem um problema de colisão de nomes de arquivo.
O atacante não precisava de uma condição de corrida. O atacante não precisava adivinhar um caminho aleatório. O atacante não precisava de acesso de escrita à página da vítima.
Ele precisava apenas de:
A partir daí, ele podia substituir os bytes do arquivo armazenado para o anexo de outra página, enquanto a página da vítima continuava a referenciar e servir esse anexo como se nada tivesse mudado.
Isso é uma falha direta de integridade.
Em termos práticos, o atacante podia:
O ponto importante é este:
o servidor aceitou um alvo de sobrescrita escolhido pelo atacante, sem vinculá-lo à página cuja permissão de edição havia sido realmente verificada.
Isso é uma falha de controle de acesso, não apenas uma má higiene booleana.
A exploração era especialmente prática para anexos de diagramas.
O cliente do Docmost intencionalmente reutiliza attachmentId para salvamentos de diagramas e usa nomes de arquivo determinísticos:
diagram.excalidraw.svgdiagram.drawio.svgIsso é importante porque reduz os requisitos do atacante.
Para anexos genéricos, o atacante precisa de ambos:
Para diagramas, o nome do arquivo já é previsível.
Então, se o atacante pode ler o conteúdo da página da vítima, ele frequentemente pode recuperar a única peça que falta:
attachmentId da vítimaNa minha configuração de validação, usei exatamente esse caminho:
Isso foi suficiente.
O exploit atravessava limites de página e limites de espaço dentro do mesmo workspace, enquanto ainda satisfazia a verificação falha de workspace.
Validei o problema ao vivo contra o Docmost v0.70.3 usando um laboratório descartável construído a partir de docmost/docmost:0.70.3, Postgres e Redis.
O fluxo do PoC foi:
attachmentId da vítima.POST /api/files/upload com:
pageId = ID da página do atacanteattachmentId = ID do anexo da vítimafile = arquivo de substituição controlado pelo atacante usando o nome do arquivo da vítimaA forma mínima da requisição era:
POST /api/files/upload
Content-Type: multipart/form-data
pageId=<attackerPageId>
attachmentId=<victimAttachmentId>
[email protected];filename=diagram.excalidraw.svg
O resultado ao vivo observado foi:
019d18ae-b176-751c-8525-b5f3cede131d019d18ae-b15b-70e9-ac67-64948e87cc5e019d18ae-b12f-75ec-8c1c-5aff3ba6be9c200 OK686a0a0ede90ece1cbb975bb29304a6c3a90373a9c3ab2496345cf7ca59cc8fa
e0168298846cdaf75c4d880f4b721d7c0ef0ef310f75617bf2b833af34cdbeba
e0168298846cdaf75c4d880f4b721d7c0ef0ef310f75617bf2b833af34cdbeba
Substituto do atacante de outra página
Isso é uma prova completa de sobrescrita de ponta a ponta, não apenas uma revisão teórica de código-fonte.
Usei dois estilos de prova durante a triagem:
O harness autônomo foi útil para isolar a falha de lógica booleana.
O PoC HTTP ao vivo foi o artefato mais forte porque provou a história completa de segurança:
attachmentId da vítima foi aceitoEssa distinção importa em bugs de controle de acesso.
"a condição está errada" não é suficiente por si só.
"a condição está errada, e a aplicação pode ser conduzida de ponta a ponta a uma sobrescrita não autorizada persistente" é o caso completo.
A correção foi lançada na v0.71.0 e mudou a proteção de sobrescrita de && para ||:
if (
existingAttachment.pageId !== pageId ||
existingAttachment.fileExt !== preparedFile.fileExtension ||
existingAttachment.workspaceId !== workspaceId
) {
throw new BadRequestException("File attachment does not match");
}
Esse patch é mínimo, direto e correto para o bug que foi reportado.
Ele restaura a regra certa:
a sobrescrita é permitida apenas quando o anexo existente corresponde exatamente às suposições de página/workspace/tipo autorizadas.
Uma vez que a proteção rejeita qualquer incompatibilidade:
Essa foi o tipo certo de correção:
Apenas uma vinculação estrita entre a página autorizada e o alvo da sobrescrita.
Ainda há uma lição de engenharia mais ampla aqui:
endpoints genéricos de upload que também servem a fluxos de atualização in-loco devem ser tratados como superfícies de API de alto risco.
Mesmo quando o bug imediato é corrigido, projetos mais fortes a longo prazo são:
Mas para a vulnerabilidade em si, o patch publicado fechou o problema central de forma limpa.
Independentemente de o projeto ter adicionado seus próprios testes privados em torno da correção, estes são os casos que importam para a cobertura de longo prazo:
O objetivo desses testes não é apenas a correção.
É travar a vinculação de autorização para que futuros "úteis" refactors de upload não reabram a mesma classe de bug.
O advisory publicado classificou este problema como:
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:L
Isso resulta em 7.1 / Alto, que é a conclusão correta.
A métrica importante aqui é Integridade.
Isso não era um bug de metadados de baixo nível. O atacante controlava completamente os bytes de substituição escritos no caminho do anexo de outra página, e a página da vítima continuava a servir o objeto modificado depois.
Esse é exatamente o tipo de adulteração entre registros armazenados que merece Integridade Alta.
Disponibilidade permanecendo Baixa também faz sentido, pois corromper um diagrama ou documento anexado pode tornar o conteúdo da vítima inutilizável, mas o impacto principal ainda é a modificação não autorizada, não a interrupção completa do serviço.
Relatei o problema de forma privada através do GitHub Security Advisories com:
O problema foi aceito pelo mantenedor, recebeu o CVE-2026-34213 e foi publicado em 14 de abril de 2026.
O advisory público lista:
>= v0.3.0v0.71.0Esse histórico também coincidiu com minha revisão local do código-fonte: a lógica vulnerável de sobrescrita estava presente na primeira versão marcada que verifiquei na linha vulnerável.
A lição interessante aqui não é meramente "use || em vez de &&."
Isso é o sintoma.
A lição mais profunda é:
se um campo controlado pelo usuário comprova autorização e outro campo controlado pelo usuário seleciona o objeto sendo atualizado, esses dois campos devem ser vinculados explícita e exatamente.
Essa regra aparece em todos os lugares:
No momento em que um sistema diz:
ele criou um limite de segurança que deve ser imposto com invariantes de correspondência exata.
Qualquer coisa mais suave do que isso se transforma em um bug de chave controlada pelo usuário mais cedo ou mais tarde.
Este problema também reforça um segundo ponto que é fácil de subestimar:
pequenos erros booleanos em código de proteção podem ter consequências de segurança de primeira ordem.
Uma condição de três cláusulas que "parece razoável" à primeira vista foi suficiente para inverter o modelo de proteção para o caminho de sobrescrita.
É por isso que essas superfícies merecem revisão deliberada, não confiança casual.
pageId fornecido pelo chamador, mas a seleção do alvo de sobrescrita usava um attachmentId separado fornecido pelo chamador.v0.71.0 alterou corretamente a proteção para rejeitar em qualquer incompatibilidade.Esta vulnerabilidade não era sobre comportamento exótico de armazenamento.
Era sobre um caminho de atualização que confiava em um identificador de objeto selecionado pelo atacante mais do que deveria.
O Docmost comprovou acesso de edição em uma página, aceitou um ID de anexo existente de outra página e depois deixou uma verificação de sobrescrita defeituosa transformar essa incompatibilidade em uma substituição bem-sucedida de arquivo entre páginas.
É por isso que se tornou CVE-2026-34213.
O patch na v0.71.0 corrigiu o problema imediato de forma limpa, mas a lição mais ampla permanece valiosa:
quando a autorização e a seleção de objeto são divididas em campos separados controlados pelo usuário, a vinculação exata é a propriedade de segurança.