
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.