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:
- 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.
- 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. - Banco guarda o nome do blob, não a URL. URL é coisa que se gera na hora.
- Rota de download autorizada. A pessoa pede
/anexos/77/download; o servidor confere se ela pode ver o anexo 77. - URL assinada com prazo curto. Só leitura, aquele blob, poucos minutos. O servidor responde com redirect para ela.
- 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?