Programação Por Equipe DevBras

Capa do artigo: Arquivo no blob storage aberto para qualquer um: container privado e link com prazo

Arquivo no blob storage aberto para qualquer um: container privado e link com prazo

Deixe o container privado, entregue anexos com URL assinada que expira depois de checar a permissão no seu sistema e teste o acesso anônimo antes que alguém liste os seus arquivos.

O sistema de RH recebe o comprovante de residência do funcionário, manda para o blob storage e grava a URL no banco: https://rhcontas.blob.core.windows.net/anexos/comprovante-joao-silva.pdf. Funciona. Funciona até demais: essa URL abre numa janela anônima, sem login, para qualquer pessoa que a receba, adivinhe ou liste o container. A resposta curta: o upload ter funcionado não diz nada sobre quem pode ler. Deixe o container privado, guarde no banco só o nome do blob, e entregue o arquivo com um link que expira em poucos minutos, gerado depois de o seu sistema conferir que aquela pessoa pode ver aquele arquivo.

Se você ainda está decidindo tirar arquivos da pasta do site e levar para o blob, comece pelo guia arquivo grande no blob, não na pasta. Este aqui trata do passo seguinte, o controle de acesso, que é onde os vazamentos acontecem.

Por que o arquivo fica público sem ninguém perceber

Blob storage não é um sistema de arquivos com permissão por usuário do seu sistema. Ele conhece outra coisa: credenciais da conta de armazenamento, identidades da nuvem e o nível de acesso do container ou do bucket. Os usuários do seu RH não existem para ele. Então só há dois jeitos de alguém ler um arquivo: ter uma credencial válida ou o container permitir leitura anônima.

Quando o tutorial manda marcar o container como público "para a imagem aparecer", a leitura anônima vira a regra para tudo que cair ali, incluindo o comprovante do João. No Azure, o nível de acesso anônimo do container pode permitir ler blobs individuais ou também listar o conteúdo inteiro. Com listagem liberada, ninguém precisa adivinhar nome nenhum: uma requisição devolve o índice de todos os arquivos.

O nome do arquivo também não protege. comprovante-joao-silva.pdf é adivinhável, e mesmo um GUID aparece em log de proxy, histórico do navegador, e-mail encaminhado e ferramenta de monitoramento. Nome difícil não é controle de acesso. Ele só atrasa quem não tem o link.

O desenho certo: privado por padrão, link curto sob demanda

O fluxo correto tem uma etapa a mais, e ela é o seu sistema decidindo:

  1. Container privado. Sem acesso anônimo. Na conta de armazenamento do Azure, existe a configuração que bloqueia acesso anônimo em todos os containers; deixe bloqueado se nenhum conteúdo precisa ser público.
  2. Nome de blob gerado pelo sistema. Algo como funcionarios/1042/6f1c2a9e.pdf. O nome original fica numa coluna do banco, para exibir na tela e no download.
  3. Banco guarda o nome do blob, não a URL. URL é coisa que se gera na hora.
  4. Rota de download autorizada. A pessoa pede /anexos/77/download; o servidor confere se ela pode ver o anexo 77.
  5. URL assinada com prazo curto. Só leitura, aquele blob, poucos minutos. O servidor responde com redirect para ela.
  6. Público de verdade em outro lugar. Logotipo, capa de produto, imagem de post: container separado, que pode ser público ou servido por CDN.

Exemplo em ASP.NET Core com Azure Blob Storage

O exemplo usa o pacote Azure.Storage.Blobs. A geração de SAS direto pelo cliente exige que ele tenha sido criado com a chave compartilhada da conta; a propriedade CanGenerateSasUri diz se é possível. Se você autentica com identidade gerenciada, o caminho é a SAS de delegação de usuário, que segue a mesma ideia com outra chamada.

app.MapGet("/anexos/{id:int}/download", async (
    int id, ClaimsPrincipal user, RhDbContext db, BlobContainerClient container) =>
{
    var anexo = await db.Anexos.FindAsync(id);
    if (anexo is null) return Results.NotFound();

    var usuarioId = int.Parse(user.FindFirstValue(ClaimTypes.NameIdentifier)!);
    var podeVer = anexo.FuncionarioId == usuarioId || user.IsInRole("RH");
    if (!podeVer) return Results.NotFound();

    var blob = container.GetBlobClient(anexo.NomeBlob);
    if (!blob.CanGenerateSasUri) return Results.Problem("Cliente sem permissao para gerar SAS");

    var sas = new BlobSasBuilder(BlobSasPermissions.Read, DateTimeOffset.UtcNow.AddMinutes(5))
    {
        BlobContainerName = blob.BlobContainerName,
        BlobName = blob.Name,
        Resource = "b",
        ContentDisposition = "attachment; filename=\"" + anexo.NomeOriginalSeguro + "\""
    };

    return Results.Redirect(blob.GenerateSasUri(sas).ToString());
}).RequireAuthorization();

Três detalhes importam. A checagem devolve 404 em vez de 403, para não confirmar que o anexo existe. O prazo é de cinco minutos, suficiente para o navegador começar o download. E o ContentDisposition força o download com o nome original já higienizado, em vez de o PDF abrir dentro do domínio de armazenamento.

Em S3 e serviços compatíveis, o equivalente é a URL pré-assinada (presigned URL) para GetObject, com tempo de expiração. O princípio não muda: bucket privado, bloqueio de acesso público ligado, URL gerada depois da autorização.

Como testar se o seu container está exposto

Não confie no painel; teste como um estranho testaria. Primeiro, cole a URL permanente de um anexo numa janela anônima. Depois, tente listar o container sem credencial:

curl -i "https://rhcontas.blob.core.windows.net/anexos/comprovante-joao-silva.pdf"

curl -i "https://rhcontas.blob.core.windows.net/anexos?restype=container&comp=list"

O resultado esperado para um container privado é um erro de autorização ou de recurso não encontrado nas duas chamadas. Se a primeira devolve 200 com o PDF, o blob está público. Se a segunda devolve um XML com a lista de arquivos, qualquer pessoa consegue baixar tudo.

Depois, teste a URL assinada: gere uma, abra, espere passar o prazo e abra de novo. Ela precisa falhar depois de vencida.

Cuidados com a URL assinada

A SAS é uma credencial temporária embutida na URL. Quem tiver a URL inteira, dentro do prazo, lê o arquivo. Por isso o prazo curto é a defesa principal. Não grave a URL assinada no banco, não mande por e-mail com validade de dias e não deixe o log de acesso da aplicação registrar a query string completa, onde fica a assinatura. O guia log sem senha vale aqui também.

Se o anexo é um documento pessoal, ele também é dado pessoal sob a LGPD. Saber onde ele está, quem acessa e quando apagar faz parte da adequação; o roteiro está em adequação à LGPD em sistema pequeno.

Erros comuns

  • Container público "para a imagem aparecer". Tudo que cair nele depois, inclusive documento pessoal, fica legível por qualquer um.
  • Gravar a URL permanente no banco. Você fica preso a um acesso que não expira e não consegue revogar sem mexer no container.
  • Usar o nome original como nome do blob. O arquivo vira adivinhável e nomes com acento ou espaço quebram a URL.
  • Gerar a SAS antes de checar a permissão. Qualquer pessoa logada troca o id na rota e baixa o anexo de outra.
  • SAS com prazo de dias ou com permissão de escrita. Um link encaminhado vira acesso duradouro, ou permite sobrescrever o arquivo.
  • Misturar público e privado no mesmo container. Alguém muda o nível de acesso para resolver a capa e expõe os anexos juntos.

Checklist

  • A URL permanente de um anexo devolve erro numa janela anônima.
  • A listagem do container sem credencial devolve erro, não um XML com arquivos.
  • O banco guarda o nome do blob e o nome original, nunca a URL assinada.
  • A rota de download confere a permissão antes de gerar o link.
  • A URL assinada é só leitura, de um blob, e expira em minutos.
  • Conteúdo público mora em container separado do privado.
  • O log da aplicação não grava a query string da SAS.

Pegue agora o último anexo enviado ao seu sistema, copie a URL dele e abra numa janela anônima: ela abre o arquivo sem pedir login?

Avalie este artigo

Referência: https://medium.com/volosoft/file-upload-download-with-blob-storage-system-in-asp-net-core-abp-framework-eeb532a1aa23