Skip to main content

Codificación y decodificación segura de URL en Java

Escrito por
Headshot of Jura Gorohovsky

Jura Gorohovsky

security champions guide

14 de agosto de 2023

0 minutos de lectura

La codificación de URL es un método que garantiza que la URL solo contenga caracteres válidos para que el servidor receptor pueda interpretarla correctamente. Según el estándar RFC 3986, los URI (que son un superconjunto de las URL) solo contienen un conjunto limitado de caracteres: dígitos, letras y algunos símbolos gráficos, todos dentro del conjunto de caracteres ASCII.

Si una URL contiene caracteres que no pertenecen a este conjunto limitado, deben codificarse con porcentajes. La codificación con porcentajes convierte un carácter en una representación hexadecimal de dos dígitos y ocho bits, precedida por el carácter de escape %. El mismo proceso debe aplicarse a los delimitadores (por ejemplo, &, /, ? o #) que forman parte de ASCII cuando se usan fuera de las posiciones estructurales previstas en la URL.

En cambio, la decodificación de URL es un método que devuelve una URL codificada con porcentajes a su forma original y restaura los caracteres no estándar.

Es importante entender que la codificación no es cifrado. El cifrado consiste en modificar información con una clave secreta para que nadie, excepto la parte destinataria, pueda acceder a la información original. En cambio, el propósito de la codificación de URL no es ocultar partes de una URL a observadores externos, sino garantizar que el servidor receptor pueda interpretarla de forma clara e inequívoca, y evitar que el usuario del cliente que crea y envía la URL la manipule.

No codificar una URL puede causar diversos problemas. Por ejemplo, es posible que tu aplicación no pueda crear la URL que debe enviar al servidor. Además, el servidor que recibe la URL quizá no pueda analizarla correctamente y responda con un error. Otro riesgo es que alguien manipule una URL sin codificar y exponga tu aplicación a posibles amenazas de seguridad.

Cada lenguaje de programación ofrece una o más API para codificar y decodificar URL. Este artículo habla de Java, de por qué es importante codificar y decodificar URL y de cómo hacerlo correctamente.

¿Qué son la codificación y la decodificación de URL en Java?

En Java, la codificación y decodificación de URL son importantes para los siguientes casos de uso:

  • Procesar datos de formato libre que un visitante ingresa en un formulario HTML, como un formulario de búsqueda.

  • Crear llamadas a una API externa desde el código al agregar parámetros de consulta a una URL base.

  • Crear llamadas a una puerta de enlace de API que se usa para enrutar solicitudes a servicios internos.

Una URL tiene la siguiente estructura:

Diagrama que identifica los componentes de esquema, host, ruta, consulta y fragmento de una URL de ejemplo.

Normalmente, no necesitas codificar la URL completa. Por supuesto, hay casos en los que la sección de ruta puede contener espacios debido a archivos subidos por usuarios; incluso existe algo llamado Punycode para los nombres de host. Sin embargo, en la mayoría de los casos, controlas el host y las secciones de ruta, así que solo necesitas codificar, una por una, las partes de una URL que representan datos variables (es decir, los valores de los parámetros en una cadena de consulta).

Un caso específico en el que sí debes codificar la URL completa es cuando se pasa como parámetro en la cadena de consulta de otra URL.

Cómo implementar una codificación y decodificación de URL segura en Java

Para entender mejor la codificación y decodificación de URL en Java, revisemos un par de clases que se usan habitualmente en aplicaciones Java para codificar y decodificar parámetros de cadenas de consulta.

Cómo codificar URL en Java

Para aplicar la codificación con porcentajes a los valores de los parámetros de una cadena de consulta en una aplicación Java, normalmente se usa la clase java.net.URLEncoder y su método encode().

Esto es exactamente lo que hace el método encode():

  • Garantiza que todos los caracteres alfanuméricos —como a a z, A a Z, 0 a 9, y los caracteres especiales ., -, * y _— permanezcan intactos.

  • Convierte el carácter de espacio en un signo más +.

  • Todos los demás caracteres se codifican con porcentajes.

Este método se creó para preparar datos de formularios HTML para su envío, convirtiéndolos al formato MIME application/x-www-form-urlencoded, que sirve para codificar los valores de los parámetros de consulta de una URL.

Hay tres sobrecargas del método encode():

1. encode(String s, String enc) te permite establecer explícitamente el esquema de codificación como una cadena (se recomienda UTF-8). Puedes usar esta sobrecarga, pero ten en cuenta que genera una UnsupportedEncodingException comprobada, lo que significa que tu código debe manejarla mediante una declaración @throws o un bloque try/catch. También es importante señalar que usar literales de cadena conlleva el riesgo de introducir errores tipográficos:

 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á disponible desde Java 10 y es, hasta ahora, la mejor sobrecarga. Usa una constante para UTF-8 (StandardCharsets.UTF_8), lo que elimina el riesgo de errores tipográficos al especificar la codificación y no genera excepciones comprobadas. Por lo tanto, no necesitas manejarlas para compilar tu código:

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

3. encode(String s) es la sobrecarga más antigua y está marcada como obsoleta en OpenJDK 17. No deberías usarla porque emplea la codificación predeterminada de la plataforma en la que se ejecuta la máquina virtual de Java (JVM), que no necesariamente es UTF-8.

El método URLEncoder.encode() tiene una particularidad: decodifica un espacio como el carácter más en lugar de %20, probablemente porque sigue una descripción de las cadenas de consulta de un estándar anterior. Por eso, los desarrolladores a veces modifican el resultado de encode() para reemplazar el signo más por %20 como representación del espacio:

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

Por ejemplo, supongamos que necesitas buscar repositorios con la API REST de GitHub. GitHub ofrece calificadores de búsqueda exhaustivos que ayudan a filtrar los resultados por lenguaje, tamaño del repositorio y visibilidad. Por ejemplo, una búsqueda de "user:defunkt forks:>100" devuelve todos los repositorios del usuario defunkt que tienen cien o más forks. Puedes usar todos estos filtros en llamadas a la API, pero debes incluirlos en el 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);

En este código, searchQuery contiene un conjunto de calificadores de búsqueda que te ayudan a encontrar todos los repositorios públicos de Java con entre 100 y 1,000 estrellas, actualizados desde 2018. El valor de este ejemplo está codificado directamente, pero también podría obtenerse al leer un archivo, una base de datos o datos ingresados directamente por el usuario en una aplicación web o móvil.

Al crear url, este código concatena tres cadenas:

  1. La URL base de la API, el delimitador de cadena de consulta ?, el parámetro q y su delimitador de asignación =: "https://api.github.com/search/repositories?q=".

  2. searchQuery, que es el valor del parámetro q, se codifica en formato porcentual mediante URLEncode().

  3. El conjunto restante de parámetros de consulta no necesita codificarse porque está codificado directamente y no contiene caracteres no permitidos.

Al ejecutar este código, la URL resultante es 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. El valor de q se codifica como la nguage%3AJava+stars%3A100..1000+pushed%3A%3E2018-01-01+is%3Apublic, donde : se reemplaza por %3A, los espacios por + y > por %3E.

Cómo decodificar URL en Java

La decodificación explícita de parámetros de consulta de URL se hace con menos frecuencia porque muchos frameworks, incluido Spring Boot, la manejan automáticamente.

Si no usas un framework, el proceso dependerá de lo que quieras hacer a continuación.

Es probable que recibas la URL para decidir qué acciones realizar, como consultar datos o redirigir la solicitud a otro servicio. En ese caso, la lógica de procesamiento probablemente implique analizar cada parámetro de consulta por separado. Puedes empezar por analizar la URL, extraer la cadena de consulta y decodificar individualmente los valores de los parámetros.

Para decodificar caracteres codificados con porcentajes, puedes usar 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();

Aquí, encodedUrl contiene una URL de búsqueda de Google con parámetros de consulta codificados con porcentajes por el navegador. Este código crea un nuevo objeto uri de tipo URI para extraer la cadena de consulta completa. El objeto ofrece un método llamado getRawQuery(), que devuelve solo la cadena de consulta y mantiene codificados todos los valores de los parámetros:

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

Luego, el código divide la consulta sin procesar usando el delimitador &, lo que genera un arreglo de pares individuales de parámetro y valor. Cada par se transforma de modo que el parámetro se mantiene igual y el valor se decodifica. Por último, todos los pares transformados se recopilan en una lista:

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

Una vez que tengas todos los parámetros separados y asociados con sus respectivos valores, puedes aplicar la lógica que necesites para validarlos y definir los siguientes pasos de tu aplicación.

Prácticas recomendadas para manejar URL en Java

Al trabajar con URL en Java, conviene tener presentes varias prácticas recomendadas para manejarlas correctamente y evitar posibles problemas.

No omitas la codificación de URL

Si omites la codificación de tus URL, prepárate para lo inesperado: desde excepciones en tiempo de ejecución hasta respuestas confusas de los servidores a los que intentas conectarte.

Por ejemplo, volvamos al caso de la llamada a la API de GitHub y veamos qué pasa si no codificas los parámetros de búsqueda:

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);

Ten en cuenta que, en este ejemplo de código, sendGetRequest(url) envuelve la creación de una solicitud HTTP con la API de creación de solicitudes de 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());

Si ejecutas este código sin codificar searchQuery, fallará en tiempo de ejecución porque el constructor de URI no puede crear un objeto URI a partir de una cadena que contiene un espacio sin codificar:

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)

Pero ¿qué pasa si insistes en enviar esta solicitud? Podrías probar la API Scanner, que acepta un objeto URL (en lugar de URI) y lo usa para leer un flujo 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();
}

Esto sí funciona. La solicitud a la API llega a su destino, pero devuelve el error 400 Bad Request, que indica un posible error del lado del cliente, como una sintaxis de solicitud mal formada:

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 muestra muy bien cómo se pueden manipular URL sin codificar en la comunicación entre servicios mediante REST.

Uno de los escenarios que describe Rotem en los que es posible manipular URL se da cuando tienes un servicio que hace lo siguiente:

  1. Recibe parámetros de una URL que proviene del cliente.

  2. Crea una nueva URL para llamar a otro servicio.

Aquí tienes un ejemplo de código que reproduce este escenario:

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 puedes ver, si usas un valor complejo como el del parámetro q, omitir la codificación de URL terminará causándote problemas tarde o temprano. Incluso si tienes la suerte de recibir una respuesta correcta del servidor, no puedes saber con certeza si contiene lo que esperabas o si acabas de aprovechar accidentalmente una vulnerabilidad del servidor.

A propósito de vulnerabilidades, no codificar los parámetros de URL puede causarlas fácilmente. Hablamos más sobre esto en el artículo "Desarrolladores, codifiquen sus 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(key)
            .append("&user=")
            .append(user)
            .toString();
    return URI.create(newUrl);
}

El servicio recibe dos parámetros, key y user, y valida correctamente al usuario. Sin embargo, no valida key y lo pasa tal cual al crear una nueva URL. Ninguno de los parámetros de consulta de esta nueva URL está codificado.

Cuando el servicio recibe datos legítimos, crea la nueva URL como se espera:

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

Pero ¿qué pasa si un actor malicioso envía una URL con el parámetro key manipulado?

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

Esto es lo que ocurre:

  1. El valor de key que envía el cliente es 55s72502010a&user=admin#. El servicio agrega la clave a la URL sin codificarla.

  2. El servicio valida el parámetro user como de costumbre, y legituser sigue considerándose legítimo porque, bueno, lo es.

  3. Como el parámetro key original incluye un carácter & que actúa como separador de parámetros en la cadena de consulta de la URL, la URL resultante recibe dos parámetros: key=55s72502010a y user=admin, que son dos subcadenas de la clave enviada por el cliente. Como puedes ver, aunque el servicio validó al usuario legituser, la URL resultante suplanta a otro usuario con permisos potencialmente elevados, que logró evadir la validación.

  4. Como el parámetro key original termina con el carácter #, que funciona como separador de fragmentos en las URL, la parte &user=legituser de la URL original enviada por el cliente pasa a la sección del fragmento y, en la práctica, se ignora.

Como resultado, obtienes una URL que representa a un usuario no relacionado y sin verificar, lo que constituye un ataque de escalada de privilegios.

Entonces, ¿cómo puedes defenderte de este ataque? Una opción sería validar el valor de key, además de validar el user. Sin embargo, quizá quieras delegar esa validación a otro servicio en la cadena de solicitudes. En ese caso, lo más fácil sería codificar los valores de los parámetros al construir una nueva 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);
}

Con esta modificación, ve qué sucede si alguien intenta enviar el valor malicioso de key:

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

El valor codificado de key (key=55s72502010a%26user%3Dadmin%23) impide el intento de escalada de privilegios porque & se codifica como %26 y # como %23. La URL resultante se envía al siguiente servicio con el nombre de usuario correcto. Puede que el siguiente servicio devuelva un error al intentar interpretar el parámetro key manipulado, pero una respuesta de error siempre es mejor que un ataque.

Usa bibliotecas estándar para codificar y decodificar URL

En Java, específicamente, para codificar con porcentajes los parámetros de consulta, probablemente uses casi siempre URLEncoder.encode() y URLDecoder.decode(), ya que son herramientas confiables. Están disponibles de forma predeterminada en cualquier proyecto Java.

Otra opción es usar la biblioteca Open Web Application Security Project (OWASP) Java Encoder. Aunque su propósito principal es validar entradas en distintos contextos, también incluye el método Encode.forUriComponent() para codificar valores de parámetros de URL y rutas REST. Esta biblioteca es una buena opción si tu aplicación Java necesita mostrar de forma segura en distintos controles de interfaz las URL enviadas por clientes y sus componentes.

Dicho esto, según el framework que uses, quizá no necesites decodificar explícitamente o tengas que hacerlo de otra manera:

  • En Spring o Spring Boot, la anotación @RequestParam aplicada a un parámetro del controlador permite decodificar automáticamente su valor.

  • En una aplicación GWT, probablemente uses los métodos definidos en la clase `com.google.gwt.http.client.URL` propia de GWT.

  • Android ofrece una API independiente para construir URL que incluye la codificación de parámetros de consulta.

Si usas bibliotecas de terceros para codificar y decodificar URL, conviene buscar vulnerabilidades conocidas y, si encuentras alguna, actualizar las bibliotecas en cuanto haya disponibles correcciones de seguridad.

Valida y depura las URL antes de interactuar con ellas en tu aplicación

La codificación de URL es solo una parte de la solución. Si tu aplicación web recibe datos de usuarios mediante parámetros de URL y luego los devuelve a la aplicación web, debes asegurarte de que esos datos sean inofensivos antes de volver a ponerlos en el contexto del navegador.

Esto significa que debes validar los datos de usuario, incluidos los que llegan mediante URL en el servidor, tanto en cuanto a su sintaxis como a su semántica. Además, usa listas de permitidos, ya que las listas de denegación son más propensas a errores y omisiones.

Además, si tu aplicación permite que los usuarios envíen código HTML, debes implementar la depuración de HTML. Una biblioteca de Java que se usa comúnmente para este fin es OWASP Java HTML Sanitizer.

Detecta URL inseguras con Snyk Security Extension para IntelliJ IDEA

La codificación y decodificación de URL deben formar parte de un conjunto más amplio de prácticas de seguridad, como la codificación y el escape de datos y la validación de entradas. Es fundamental escribir código seguro, y revelar las fallas de seguridad lo antes posible en el ciclo de desarrollo reduce tanto el costo de corregirlas como el impacto de los incidentes de seguridad.

Si eres desarrollador de Java y trabajas en IntelliJ IDEA, te puede convenir instalar la extensión Snyk Security.

Snyk Security detecta problemas, los destaca en el editor de código y te ayuda a corregir vulnerabilidades de seguridad conocidas en lo siguiente:

  • Tu propio código

  • Las dependencias de código abierto directas y transitivas que incorporas a tu proyecto

  • Tus imágenes de Docker

  • Tus plantillas de infraestructura como código

La extensión Snyk Security te ayuda a identificar problemas como entradas no depuradas provenientes de parámetros de URL, que pueden provocar vulnerabilidades de cross-site scripting (XSS), inyección de comandos, falsificación de solicitudes del lado del servidor (SSRF) o redirección abierta. Para cada vulnerabilidad detectada, muestra cómo proyectos Java de código abierto resolvieron problemas similares en el pasado:

IntelliJ IDEA muestra código Java marcado por Snyk por una vulnerabilidad de cross-site scripting (XSS), con el flujo de datos y ejemplos de correcciones.

Aquí está la lista completa de inspecciones de seguridad que Snyk Security ejecuta en tu código Java.

Conclusión

No codificar los parámetros de URL puede parecer una omisión menor, pero, como viste en este artículo, puede afectar gravemente tanto la confiabilidad como la seguridad de tus aplicaciones Java. Codificar con porcentajes los parámetros de URL ayuda a ofrecer los resultados esperados a los usuarios legítimos y evita que actores maliciosos evadan los controles de acceso y ejecuten ataques.

Aunque la codificación y decodificación de URL son solo parte de un conjunto más amplio de buenas prácticas de seguridad, usar API estables y herramientas de desarrollo inteligentes como la extensión Snyk Security para IntelliJ IDEA te ayuda a hacerlo bien, distribuir código seguro de forma constante y evitar pasar horas resolviendo incidentes de seguridad en producción y dando seguimiento.

Comienza con Capture the Flag

Aprende a resolver desafíos de Capture the Flag con nuestro taller virtual 101 a pedido.

Leer más

blog feature snyk container custom base recommendations
Blog

Presentamos Evo Agentic AppSec: seguridad que sigue el ritmo del desarrollo con IA

El desarrollo impulsado por IA avanza más rápido que los flujos de trabajo de seguridad tradicionales. Agentic AppSec de Snyk usa agentes de seguridad para encontrar, corregir y verificar vulnerabilidades de forma continua, mientras las personas establecen el nivel.

feature insights context
Blog

Los ataques autónomos ya están aquí. La defensa debe estar a su altura.

Los atacantes autónomos están reduciendo el tiempo disponible para defenderse. Descubre cómo el descubrimiento, la corrección, la validación y la prevención continuos pueden ayudar a los equipos de seguridad a seguirles el ritmo.

illustration hero ai
Blog

El huracán de la IA ha llegado

La IA está acelerando por igual la creación de software y los ciberataques. Los líderes deben proteger los agentes y el código desde el inicio, aplicar controles en tiempo de ejecución y validar las defensas de forma independiente.