Skip to main content

Codificação e decodificação seguras de URLs em Java

Escrito por
Headshot of Jura Gorohovsky

Jura Gorohovsky

security champions guide

14 de agosto de 2023

0 minutos de leitura

A codificação de URLs é um método que garante que sua URL contenha apenas caracteres válidos, para que o servidor de destino possa interpretá-la corretamente. De acordo com o padrão RFC 3986, os URIs (que são um superconjunto das URLs) contêm apenas um conjunto limitado de caracteres: números, letras e alguns símbolos gráficos, todos pertencentes ao conjunto de caracteres ASCII.

Se uma URL contiver caracteres fora desse conjunto limitado, eles deverão ser codificados com porcentagem. A codificação com porcentagem converte um caractere em uma representação hexadecimal de dois dígitos de oito bits, precedida pelo caractere de escape %. O mesmo processo deve ser aplicado aos delimitadores (por exemplo, &, /, ? ou #) do ASCII quando usados fora de suas posições estruturais previstas na URL.

Já a decodificação de URLs converte uma URL codificada com porcentagem de volta à sua forma original, restaurando os caracteres não padronizados.

É importante entender que codificação não é criptografia. Criptografar significa modificar informações usando uma chave secreta, de modo que ninguém além do destinatário possa acessar as informações originais. Já o objetivo da codificação de URLs não é ocultar partes da URL de quem estiver observando, mas garantir que o servidor de destino consiga interpretá-la com facilidade e sem ambiguidades, além de impedir que o usuário do cliente que está criando e enviando a URL a manipule.

Deixar de codificar uma URL pode causar diversos problemas. Por exemplo, talvez seu aplicativo não consiga montar a URL para enviá-la ao servidor. Além disso, o servidor que recebe a URL pode não conseguir analisá-la corretamente e retornar um erro. Outro risco é a manipulação de uma URL não codificada, que pode expor seu aplicativo a ameaças de segurança.

Cada linguagem de programação oferece uma ou mais APIs para codificar e decodificar URLs. Este artigo aborda Java, explica por que a codificação e a decodificação de URLs são importantes e mostra como fazê-las corretamente.

O que é codificação e decodificação de URLs em Java?

Em Java, a codificação e a decodificação de URLs são importantes nos seguintes casos de uso:

  • Processar dados de formato livre inseridos por um visitante em um formulário HTML, como um formulário de pesquisa.

  • Montar chamadas a uma API externa no código, adicionando parâmetros de consulta a uma URL base.

  • Montar chamadas a um gateway de API usado para encaminhar solicitações a serviços internos.

Uma URL tem a seguinte estrutura:

Diagrama que identifica os componentes de esquema, host, caminho, consulta e fragmento de uma URL de exemplo.

Em geral, não é necessário codificar a URL inteira. É claro que, às vezes, o caminho pode conter espaços em nomes de arquivos enviados por usuários — e até existe o Punycode para nomes de host. Porém, na maioria dos casos, você controla as seções de host e caminho. Isso significa que basta codificar, uma a uma, as partes da URL que representam dados variáveis (ou seja, os valores dos parâmetros da string de consulta).

Um caso específico em que é preciso codificar a URL inteira é quando ela é passada como parâmetro na string de consulta de outra URL.

Como implementar a codificação e a decodificação seguras de URLs em Java

Para entender melhor a codificação e a decodificação de URLs em Java, confira duas classes comumente usadas em aplicativos Java para codificar e decodificar parâmetros de strings de consulta.

Como codificar URLs em Java

Para aplicar a codificação com porcentagem aos valores dos parâmetros de uma string de consulta em um aplicativo Java, normalmente você usa a classe java.net.URLEncoder e o método encode().

Veja exatamente o que o método encode() faz:

  • Ele garante que todos os caracteres alfanuméricos — como a a z, A a Z, 0 a 9, e os caracteres especiais ., -, * e _ — sejam mantidos.

  • Ele converte o espaço em um sinal de mais +.

  • Todos os outros caracteres são codificados com porcentagem.

Esse método foi criado para preparar dados de formulários HTML para envio, convertendo-os para o formato MIME application/x-www-form-urlencoded, adequado para codificar valores de parâmetros de consulta de URLs.

Há três versões sobrecarregadas do método encode():

1. encode(String s, String enc) permite definir explicitamente o esquema de codificação como uma string (recomenda-se UTF-8). Você pode usar essa versão, mas lembre-se de que ela lança uma exceção verificada UnsupportedEncodingException. Isso significa que seu código precisa tratá-la com uma declaração @throws ou um bloco try/catch. Também vale lembrar que usar literais de string traz o risco de erros de digitação:

 String url;
  try {
      url = "https://example.com/search?q=" +
            URLEncoder.encode(parameterValue, "UTF-8");
  } catch (UnsupportedEncodingException e) {
      throw new RuntimeException(e);
  }

2. encode(String s, Charset charset) está disponível desde o Java 10 e é a melhor versão até agora. Ela usa uma constante para UTF-8 (StandardCharsets.UTF_8), o que elimina o risco de erros de digitação ao especificar a codificação e não lança exceções verificadas. Assim, você não precisa tratá-las para compilar seu código:

String url = "https://example.com/search?q=" +
              URLEncoder.encode(parameterValue, StandardCharsets.UTF_8);

3. encode(String s) é a versão mais antiga e está marcada como obsoleta no OpenJDK 17. Não use essa versão: ela utiliza a codificação padrão da plataforma em que a máquina virtual Java (JVM) está sendo executada, que não é necessariamente UTF-8.

O método URLEncoder.encode() tem uma particularidade: ele codifica um espaço como o caractere de mais, em vez de %20, provavelmente por seguir a descrição de strings de consulta de um padrão antigo. Por isso, às vezes os desenvolvedores alteram a saída de encode() para substituir o sinal de mais por %20, que representa o espaço:

return URLEncoder.encode(parameter, StandardCharsets.UTF_8).replaceAll("\\+", "%20");

Por exemplo, digamos que você precise pesquisar repositórios usando a API REST do GitHub. O GitHub oferece diversos qualificadores de pesquisa que ajudam a filtrar resultados por linguagem de programação, tamanho do repositório e visibilidade. Por exemplo, a pesquisa por "user:defunkt forks:>100" retorna todos os repositórios do usuário defunkt com cem ou mais forks. Você pode usar esses filtros em chamadas à API, mas precisa colocá-los no parâmetro de consulta q:

String searchQuery = "language:Java stars:100..1000 pushed:>2018-01-01 is:public";
String url = "https://api.github.com/search/repositories?q=" +
           URLEncoder.encode(searchQuery, StandardCharsets.UTF_8) +
           "&per_page=10&sort=stars&order=desc";
HttpResponse<String> response = sendGetRequest(url);

Neste código, searchQuery armazena um conjunto de qualificadores de pesquisa que ajuda a encontrar todos os repositórios públicos de Java com entre 100 e 1.000 estrelas, atualizados desde 2018. O valor deste exemplo está definido diretamente no código, mas também poderia vir da leitura de um arquivo ou banco de dados, ou ser inserido diretamente por um usuário em um aplicativo web ou para dispositivos móveis.

Ao montar url, este código concatena três strings:

  1. A URL base da API, o delimitador da string de consulta ?, o parâmetro q e seu delimitador de atribuição =: "https://api.github.com/search/repositories?q=".

  2. searchQuery, que é o valor do parâmetro q, é codificado com porcentagem usando URLEncode().

  3. O restante dos parâmetros de consulta não precisa ser codificado, pois está definido diretamente no código e não contém caracteres inválidos.

Ao executar esse código, a URL resultante é https://api.github.com/search/repositories?q=language%3AJava+stars%3A100..1000+pushed%3A%3E2018-01-01+is%3Apublic&per_page=10&sort=stars&order=desc. O valor de q é codificado como la nguage%3AJava+stars%3A100..1000+pushed%3A%3E2018-01-01+is%3Apublic, em que : é substituído por %3A, os espaços são substituídos por + e > é substituído por %3E.

Como decodificar URLs em Java

A decodificação explícita de parâmetros de consulta de URLs é menos comum, pois muitos frameworks, incluindo o Spring Boot, fazem isso automaticamente.

Se você não estiver usando um framework, o processo deve depender do que pretende fazer em seguida.

É provável que você receba a URL para decidir quais ações executar, como consultar dados ou redirecionar a solicitação para outro serviço. Nesse caso, sua lógica de processamento provavelmente analisará cada parâmetro de consulta separadamente. Você pode começar analisando a URL, extraindo a string de consulta e decodificando cada valor de parâmetro individualmente.

Para decodificar caracteres codificados com porcentagem, use java.net.URLDecoder.decode():

String encodedUrl = "https://www.google.com/search?q=it%27s+my+party&newwindow=1&sxsrf=APwXEdeEqrxGIrZCgLpZFvGUSzgPweokog%3A1682563238731";
URI uri = URI.create(encodedUrl);
List<Map.Entry<String, String>> queryParamsAndValues = Arrays.stream(uri.getRawQuery().split("&"))
      .map(param -> Map.entry(param.split("=")[0], URLDecoder.decode(param.split("=")[1], StandardCharsets.UTF_8)))
      .toList();

Aqui, encodedUrl contém uma URL de pesquisa do Google com parâmetros de consulta codificados com porcentagem pelo navegador. Este código cria um novo objeto uri do tipo URI para extrair a string de consulta inteira. O objeto oferece o método getRawQuery(), que retorna apenas a string de consulta, mantendo codificados todos os valores dos parâmetros:

q=it%27s+my+party&newwindow=1&sxsrf=APwXEdeEqrxGIrZCgLpZFvGUSzgPweokog%3A1682563238731

Em seguida, o código divide a consulta bruta pelo delimitador &, criando um array de pares individuais de parâmetro e valor. Cada par é transformado: o parâmetro é mantido como está e o valor é decodificado. Por fim, todos os pares transformados são reunidos em uma lista:

q -> it's my party
newwindow -> 1
sxsrf -> APwXEdeEaxbGIrZCzLpZFvGUSzgPweokog:1682563238731

Depois de separar todos os parâmetros e associá-los aos respectivos valores, você pode aplicar a lógica necessária para validá-los e definir as próximas etapas do aplicativo.

Práticas recomendadas para lidar com URLs em Java

Ao trabalhar com URLs em Java, vale seguir algumas práticas recomendadas para garantir o tratamento correto e evitar possíveis problemas.

Não deixe de codificar URLs

Se você não codificar suas URLs, prepare-se para o inesperado — desde exceções em tempo de execução até respostas confusas dos servidores que está tentando acessar.

Por exemplo, reveja o cenário da chamada à API do GitHub e veja o que acontece quando os parâmetros de pesquisa não são codificados:

String searchQuery = "language:Java stars:100..1000 pushed:>2018-01-01 is:public";
String url = "https://api.github.com/search/repositories?q=" +
           searchQuery +
           "&per_page=10&sort=stars&order=desc";
HttpResponse<String> response = sendGetRequest(url);

Vale lembrar que, neste exemplo de código, sendGetRequest(url) encapsula a criação de uma solicitação HTTP usando a API de criação de solicitações do Java 11:

String auth = getAuthToken();
HttpRequest request = HttpRequest.newBuilder()
       .uri(new URI(url))
       .version(HttpClient.Version.HTTP_2)
       .header("Content-Type", "application/json")
       .header("Authorization", auth)
       .timeout(Duration.of(30, SECONDS))
       .GET()
       .build();

HttpClient client = HttpClient.newHttpClient();
return client.send(request, HttpResponse.BodyHandlers.ofString());

Se você executar este código sem codificar searchQuery, haverá uma falha em tempo de execução, porque o construtor de URI não consegue criar um objeto URI a partir de uma string que contém um espaço não codificado:

java.net.URISyntaxException: Illegal character in query at index 58: https://api.github.com/search/repositories?q=language:Java stars:100..1000 pushed:>2018-01-01 is:public&per_page=10&sort=stars&order=desc
   at java.base/java.net.URI$Parser.fail(URI.java:2974)
   at java.base/java.net.URI$Parser.checkChars(URI.java:3145)
   at java.base/java.net.URI$Parser.parseHierarchical(URI.java:3233)
   at java.base/java.net.URI$Parser.parse(URI.java:3175)
   at java.base/java.net.URI.<init>(URI.java:623)
   at org.example.CallGitHubAPI.sendGetRequest(CallGitHubAPI.java:67)

E se você insistir em enviar essa solicitação mesmo assim? Você pode tentar usar a API Scanner, que aceita um objeto URL (em vez de URI) e o utiliza para ler um fluxo de entrada:

String searchQuery = "language:Java stars:100..1000 pushed:>2018-01-01 is:public";
String url = "https://api.github.com/search/repositories?q=" +
           searchQuery +
           "&per_page=10&sort=stars&order=desc";

URL urlFromNonEncodedString;
Scanner inputStream = null;
try {
  urlFromNonEncodedString = new URL(url);
  inputStream = new Scanner(urlFromNonEncodedString.openConnection().getInputStream());
  System.out.println(inputStream.useDelimiter("\\A").next());
} catch (IOException e) {
  throw new RuntimeException(e);
}
finally {
  if (inputStream != null) inputStream.close();
}

Isso funciona. A solicitação à API chega ao destino, mas retorna o erro 400 Bad Request, indicando uma possível falha do lado do cliente, como uma sintaxe de solicitação malformada:

Exception in thread "main" java.lang.RuntimeException: java.io.IOException: Server returned HTTP response code: 400 for URL: https://api.github.com/search/repositories?q=language:Java stars:100..1000 pushed:>2018-01-01

Rotem Bar mostra muito bem como URLs não codificadas podem ser manipuladas na comunicação entre serviços que usam REST.

Um dos cenários descritos por Rotem em que é possível manipular URLs envolve um serviço que:

  1. Recebe parâmetros de uma URL enviada pelo cliente.

  2. Cria uma nova URL para chamar outro serviço.

Veja um exemplo de código que reproduz esse cenário:

is:public&per_page=10&sort=stars&order=desc
   at org.example.CallGitHubAPI.callWithoutEncoding(CallGitHubAPI.java:65)
   at org.example.CallGitHubAPI.callGitHubAPI(CallGitHubAPI.java:37)
   at org.example.Main.main(Main.java:16)
Caused by: java.io.IOException: Server returned HTTP response code: 400 for URL: https://api.github.com/search/repositories?q=language:Java stars:100..1000 pushed:>2018-01-01 is:public&per_page=10&sort=stars&order=desc
   at java.base/sun.net.www.protocol.http.HttpURLConnection.getInputStream0(HttpURLConnection.java:1997)
   at java.base/sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1589)
   at java.base/sun.net.www.protocol.https.HttpsURLConnectionImpl.getInputStream(HttpsURLConnectionImpl.java:224)
   at org.example.CallGitHubAPI.callWithoutEncoding(CallGitHubAPI.java:62)

Como você pode ver, diante de um valor complexo como o usado no parâmetro q, deixar de codificar a URL vai acabar causando problemas em algum momento. Mesmo que você tenha a sorte de receber uma resposta válida do servidor, não dá para ter certeza de que ela contém o que você esperava ou se você acabou explorando, sem querer, uma vulnerabilidade do servidor.

Falando em vulnerabilidades, não codificar parâmetros de URL pode facilmente levar a elas — um tema que abordamos melhor no artigo "Desenvolvedores, codifiquem suas URLs".

private static URI forwardRequestToAnotherService(String key, String user) {
    if (!validateUser(user)) return null;
    String newUrl = new StringBuilder()
            .append("https://my.internal.api.com/info?key=")
            .append(key)
            .append("&user=")
            .append(user)
            .toString();
    return URI.create(newUrl);
}

O serviço recebe dois parâmetros, key e user, e valida corretamente o usuário. No entanto, key não é validado e é simplesmente inserido como está ao criar uma nova URL. Nenhum dos parâmetros de consulta dessa nova URL é codificado.

Quando o serviço recebe uma entrada legítima, cria uma nova URL como esperado:

forwardRequestToAnotherService("55s72502010a", "legituser")
// Output: https://my.internal.api.com/info?key=55s72502010a&user=legituser

Mas o que acontece se um agente mal-intencionado enviar uma URL com o parâmetro key adulterado?

forwardRequestToAnotherService("55s72502010a&user=admin#", "legituser")
// Output: https://my.internal.api.com/info?key=55s72502010a&user=admin#&user=legituser

Veja o que acontece:

  1. O valor de key enviado pelo cliente é 55s72502010a&user=admin#. O serviço acrescenta a chave à URL sem codificá-la.

  2. O serviço valida o parâmetro user normalmente, e legituser continua sendo considerado legítimo porque, bem, é legítimo.

  3. Como o parâmetro key original inclui o caractere &, que funciona como separador de parâmetros na query string da URL, a URL resultante recebe dois parâmetros — key=55s72502010a e user=admin — que são duas partes da chave enviada pelo cliente. Como você pode ver, embora o serviço tenha validado o usuário legituser, a URL resultante se passa por outro usuário, que pode ter permissões elevadas, e essa identidade passou pela validação.

  4. Como o parâmetro key original termina com o caractere #, que funciona como separador de fragmento em URLs, a parte &user=legituser da URL original enviada pelo cliente é deslocada para a seção de fragmento e, na prática, ignorada.

Como resultado, você obtém uma URL que representa um usuário não relacionado e não verificado, o que caracteriza um ataque de elevação de privilégios.

Então, como se proteger contra esse ataque? Uma opção é validar o valor de key além de validar o user. No entanto, talvez você queira delegar essa validação a outro serviço na cadeia de requisições. Nesse caso, a solução mais simples é codificar os valores dos parâmetros ao construir uma nova URL:

private static URI forwardRequestToAnotherService(String key, String user) {
    if (!validateUser(user)) return null;
    String newUrl = new StringBuilder()
            .append("https://my.internal.api.com/info?key=")
            .append(URLEncoder.encode(key, StandardCharsets.UTF_8))
            .append("&user=")
            .append(URLEncoder.encode(user, StandardCharsets.UTF_8))
            .toString();
    return URI.create(newUrl);
}

Com essa alteração, veja o que acontece quando alguém tenta enviar um valor malicioso para key:

forwardRequestToAnotherService("55s72502010a&user=admin#", "legituser")
// Output: https://my.internal.api.com/info?key=55s72502010a%26user%3Dadmin%23&user=legituser

O valor codificado de key (key=55s72502010a%26user%3Dadmin%23) impede a tentativa de elevação de privilégios porque & é codificado como %26 e # como %23. A URL resultante segue para o próximo serviço com o nome de usuário correto. O próximo serviço pode retornar um erro ao tentar interpretar o parâmetro key adulterado, mas receber uma resposta de erro é sempre melhor do que sofrer um ataque.

Use bibliotecas padrão para codificar e decodificar URLs

Especificamente em Java, para codificar parâmetros de query usando codificação percentual, provavelmente você usa na maioria das vezes URLEncoder.encode() e URLDecoder.decode(), ferramentas confiáveis e disponíveis por padrão em qualquer projeto Java.

Outra opção é usar a biblioteca Open Web Application Security Project (OWASP) Java Encoder. Embora seu objetivo principal seja validar entradas em diferentes contextos, ela também inclui o método Encode.forUriComponent() para codificar valores de parâmetros de URL e caminhos REST. Essa biblioteca é uma boa escolha se seu aplicativo Java precisa exibir com segurança URLs enviadas por clientes e suas partes em diferentes controles de interface.

Dito isso, dependendo do framework que você usa, talvez não seja necessário decodificar explicitamente ou seja preciso fazer isso de outra maneira:

  • No Spring ou Spring Boot, a anotação @RequestParam aplicada a um parâmetro do controlador decodifica automaticamente o valor desse parâmetro.

  • Em um aplicativo GWT, provavelmente você usa os métodos definidos na própria classe `com.google.gwt.http.client.URL` do GWT.

  • O Android tem uma API separada para construir URLs, que inclui a codificação de parâmetros de query.

Se você usa bibliotecas de terceiros para codificar e decodificar URLs, é recomendável verificar se elas têm vulnerabilidades conhecidas e, caso tenham, atualizá-las assim que as correções de segurança estiverem disponíveis.

Valide e higienize URLs antes de interagir com elas no seu aplicativo

A codificação de URLs é apenas parte da solução. Se seu aplicativo web recebe dados de usuários por meio de parâmetros de URL e depois os repassa ao aplicativo, você deve garantir que esses dados sejam inofensivos antes de inseri-los novamente no contexto do navegador.

Isso significa que você deve validar os dados do usuário, inclusive os recebidos por URLs, no servidor, verificando sua sintaxe e semântica. Use listas de permissões, pois listas de bloqueio estão mais sujeitas a erros e omissões.

Além disso, se seu aplicativo permite que usuários enviem código HTML, você deve implementar a higienização de HTML. Uma biblioteca bastante usada para esse fim em Java é a OWASP Java HTML Sanitizer.

Encontre URLs inseguras com a Snyk Security Extension para IntelliJ IDEA

A codificação e a decodificação de URLs devem fazer parte de um conjunto mais amplo de práticas de segurança, como codificar e escapar dados e validar entradas. Escrever código seguro é essencial, e identificar falhas de segurança o mais cedo possível no ciclo de desenvolvimento reduz tanto o custo de corrigi-las quanto o impacto de incidentes de segurança.

Se você desenvolve em Java e trabalha com o IntelliJ IDEA, pode ser útil instalar a extensão Snyk Security.

A Snyk Security encontra problemas, destaca-os no editor de código e ajuda você a corrigir vulnerabilidades de segurança conhecidas em:

  • Seu próprio código

  • Dependências de código aberto diretas e transitivas que você adiciona ao projeto

  • Suas imagens Docker

  • Seus templates de infraestrutura como código

A extensão Snyk Security ajuda você a identificar problemas como entradas não higienizadas provenientes de parâmetros de URL, que podem levar a cross-site scripting (XSS), injeção de comandos, falsificação de requisições do lado do servidor (SSRF) ou vulnerabilidades de redirecionamento aberto. Para cada vulnerabilidade detectada, ela mostra como diferentes projetos Java de código aberto corrigiram problemas semelhantes:

O IntelliJ IDEA exibe código Java sinalizado pelo Snyk por uma vulnerabilidade de cross-site scripting (XSS), com o fluxo de dados e exemplos de correção.

Confira a lista completa das verificações de segurança que a Snyk Security executa no seu código Java.

Conclusão

Deixar de codificar parâmetros de URL pode parecer uma omissão pequena, mas, como você viu neste artigo, isso pode afetar seriamente a confiabilidade e a segurança dos seus aplicativos Java. A codificação percentual dos parâmetros de URL ajuda a garantir os resultados esperados para usuários legítimos e impede que agentes maliciosos burlem controles de acesso e realizem ataques.

Embora a codificação e a decodificação de URLs sejam apenas parte de um conjunto mais amplo de boas práticas de segurança, usar APIs estáveis e ferramentas inteligentes para desenvolvedores, como a extensão Snyk Security para IntelliJ IDEA, ajuda você a acertar sempre, entregar código seguro de forma consistente e evitar horas exaustivas resolvendo incidentes de segurança em produção e acompanhando seus desdobramentos.

Comece a jogar Capture the Flag

Aprenda a resolver desafios de Capture the Flag assistindo à gravação sob demanda do nosso workshop virtual introdutório.

Leia mais

blog feature snyk container custom base recommendations
Blog

Conheça o Evo Agentic AppSec: segurança que acompanha o ritmo do desenvolvimento com IA

O desenvolvimento impulsionado por IA está ultrapassando os fluxos de trabalho tradicionais de segurança. O Agentic AppSec da Snyk usa agentes de segurança para encontrar, corrigir e verificar vulnerabilidades continuamente, enquanto as pessoas definem os critérios.

feature insights context
Blog

Os ataques autônomos já chegaram. A defesa precisa acompanhar o ritmo.

Os atacantes autônomos estão reduzindo o tempo disponível para a defesa. Saiba como a descoberta, a correção, a validação e a prevenção contínuas ajudam as equipes de segurança a acompanhar esse ritmo.

illustration hero ai
Blog

O furacão da IA chegou

A IA está acelerando tanto a criação de software quanto os ataques cibernéticos. As lideranças devem proteger agentes e código desde o início, aplicar controles em tempo de execução e validar as defesas de forma independente.