WebView2 e CefSharp no app desktop: limite a navegação e planeje a atualização
Embutir um navegador no programa traz o Chromium inteiro. Restrinja os endereços, isole a ponte com o código nativo, sirva arquivos locais e saiba quem atualiza o motor.
O Estoque Fácil era um programa Windows de 40 MB que abria em dois segundos. Para ganhar um painel com gráficos, a equipe embutiu um navegador e carregou a página do painel. Ficou bonito. Seis meses depois, o instalador tinha passado de 200 MB, o Gerenciador de Tarefas mostrava seis processos do programa, e um cliente descobriu que, colando um endereço qualquer na caixa de busca do painel, abria o site que quisesse dentro do sistema — com acesso à ponte que gravava arquivos no disco.
A resposta curta: um controle de navegador embutido, como WebView2 ou CefSharp, não é um componente leve, é o Chromium inteiro. Trate a página como conteúdo não confiável: liste os endereços permitidos, bloqueie o resto, exponha ao JavaScript só as mensagens que você precisa e sirva suas telas a partir de arquivos locais. E decida desde o começo quem mantém o motor atualizado.
O que você herda ao embutir um navegador
O navegador embutido traz o motor de renderização, o motor de JavaScript, a pilha de rede e o modelo de vários processos do Chromium: um processo principal, processos de renderização, GPU e utilitários. É por isso que o Estoque Fácil passou a ter seis processos e algumas centenas de megabytes a mais de memória com o painel aberto.
Herda também a superfície de ataque. Navegadores recebem correções de segurança com frequência, e um navegador embutido desatualizado é um navegador vulnerável rodando com as permissões do seu programa. Se a página carregada vem da internet ou de um endereço que o usuário digita, qualquer falha naquele site ou no motor alcança o seu processo.
E herda a ponte. O valor de embutir um navegador costuma ser deixar a página conversar com o programa: abrir um arquivo, imprimir, ler o banco local. Essa ponte é exatamente o que transforma "abrir um site qualquer" de incômodo em incidente.
WebView2 ou CefSharp: quem atualiza o motor
O WebView2 da Microsoft usa o motor do Edge. No modo Evergreen, o runtime é compartilhado pelo sistema e atualizado automaticamente pela Microsoft; no Windows 11 ele já vem instalado. Seu instalador fica pequeno e as correções chegam sem você publicar versão nova. No modo Fixed Version, você distribui uma versão específica junto com o programa, e atualizar passa a ser trabalho seu.
O CefSharp, baseado no Chromium Embedded Framework, embute o Chromium no pacote do seu programa. O número de versão do pacote acompanha a versão do Chromium. Isso dá controle total sobre o motor, mas cada correção de segurança do Chromium exige que você atualize o pacote, teste e distribua uma nova versão para todos os clientes. Programa com CefSharp que não é atualizado há um ano carrega um ano de falhas conhecidas.
Nenhuma das duas escolhas é errada. A errada é escolher sem saber de quem é a obrigação. O mesmo raciocínio vale para Electron, discutido em separar a janela Electron da interface.
Como embutir com o mínimo de risco
- Prefira servir suas telas de arquivos locais mapeados para um nome de host virtual, em vez de um servidor remoto.
- Liste os hosts permitidos e cancele qualquer navegação fora da lista.
- Intercepte novas janelas e abra links externos no navegador padrão do sistema.
- Use troca de mensagens em vez de expor objetos nativos inteiros ao JavaScript.
- Valide cada mensagem recebida: origem, ação permitida e parâmetros.
- Desligue DevTools e menus padrão na versão distribuída.
- Defina a política de atualização do motor e registre a versão em uso.
- Meça memória e processos com a tela aberta.
Exemplo com WebView2 no WinForms
using System.Diagnostics;
using Microsoft.Web.WebView2.Core;
public partial class FormPainel : Form
{
private static readonly HashSet<string> HostsPermitidos = new() { "app.example" };
private static bool Permitido(string endereco) =>
Uri.TryCreate(endereco, UriKind.Absolute, out var u)
&& u.Scheme == Uri.UriSchemeHttps
&& HostsPermitidos.Contains(u.Host);
private async void FormPainel_Load(object sender, EventArgs e)
{
await webView.EnsureCoreWebView2Async();
var core = webView.CoreWebView2;
core.SetVirtualHostNameToFolderMapping(
"app.example",
Path.Combine(AppContext.BaseDirectory, "painel"),
CoreWebView2HostResourceAccessKind.Deny);
core.Settings.AreDevToolsEnabled = false;
core.Settings.AreDefaultContextMenusEnabled = false;
core.Settings.AreHostObjectsAllowed = false;
core.NavigationStarting += (s, a) =>
{
if (!Permitido(a.Uri)) a.Cancel = true;
};
core.NewWindowRequested += (s, a) =>
{
a.Handled = true;
if (Uri.TryCreate(a.Uri, UriKind.Absolute, out var u) && u.Scheme == Uri.UriSchemeHttps)
Process.Start(new ProcessStartInfo(u.AbsoluteUri) { UseShellExecute = true });
};
core.WebMessageReceived += AoReceberMensagem;
webView.Source = new Uri("https://app.example/index.html");
}
private void AoReceberMensagem(object? sender, CoreWebView2WebMessageReceivedEventArgs a)
{
if (!Permitido(a.Source)) return;
var acao = a.TryGetWebMessageAsString();
switch (acao)
{
case "exportar-estoque":
ExportarEstoqueParaPastaDoUsuario();
break;
default:
return;
}
}
}
O SetVirtualHostNameToFolderMapping faz https://app.example/ servir os arquivos da pasta painel do programa, sem servidor e sem rede. O domínio .example é reservado para exemplos e não colide com nenhum site real. O NavigationStarting cancela qualquer endereço fora da lista, inclusive o colado na busca. Links externos vão para o navegador padrão, que o usuário mantém atualizado e onde a sessão dele já está.
A ponte é uma lista fechada de ações. A página manda window.chrome.webview.postMessage('exportar-estoque') e o programa decide o caminho do arquivo; a página nunca envia um caminho de disco. Com AreHostObjectsAllowed desligado, nenhum objeto .NET fica acessível ao JavaScript. No CefSharp, os equivalentes são o tratamento de navegação no request handler e o registro de objetos no repositório de objetos JavaScript, que merece a mesma lista fechada.
Login não é tarefa do navegador embutido
Se o painel precisa de login com Google, Microsoft ou outro provedor, não faça esse login dentro do controle embutido. Provedores bloqueiam ou desencorajam isso, e o usuário não tem como saber se está digitando a senha na página real. O caminho é abrir o navegador do sistema, como mostram login OAuth dentro do app não é a aba do Chrome e login no app MAUI.
Erros comuns
- Carregar qualquer endereço que o usuário digita. O programa vira um navegador com acesso à sua ponte nativa.
- Expor um objeto nativo inteiro ao JavaScript. Qualquer script na página chama qualquer método, inclusive os que gravam no disco.
- Aceitar caminho de arquivo vindo da página. Uma página comprometida lê ou sobrescreve o que quiser.
- Distribuir CefSharp sem plano de atualização. Meses de falhas conhecidas do Chromium rodando nos clientes.
- Deixar DevTools ativo na versão final. Qualquer usuário inspeciona e chama a ponte à mão.
- Ignorar memória e processos. O programa leve passa a disputar recursos em máquinas modestas.
Checklist
- As telas próprias são servidas de arquivos locais por host virtual.
- Há uma lista de hosts permitidos e a navegação fora dela é cancelada.
- Novas janelas e links externos abrem no navegador do sistema.
- A ponte aceita só uma lista fechada de mensagens, conferindo a origem.
- Nenhum objeto nativo é exposto inteiro ao JavaScript.
- DevTools e menus padrão estão desligados na versão distribuída.
- Está definido quem atualiza o motor e a versão em uso é registrada.
- Memória e número de processos foram medidos com a tela aberta.
Abra o seu programa, vá até a tela com o navegador embutido e tente fazê-lo carregar um endereço externo qualquer: ele abre? Se abre, que métodos nativos aquela página consegue chamar?
Referência: https://cefsharp.github.io/