Asegurando las aplicaciones web de Spring Boot

Este artículo se aplica a los sitios creados con el marco Spring Boot. Discutiremos los siguientes cuatro métodos para agregar capas adicionales de seguridad a las aplicaciones Spring Boot:
  • Prevención de la inyección SQL utilizando consultas parametrizadas
  • Validación de entrada de parámetros de URL
  • Validación de entrada de campo de formulario
  • Codificación de salida para prevenir ataques XSS reflejados
Utilizo estos métodos para mi sitio web, Initial Commit , que se construye con Spring Boot, el motor de plantillas de Thymeleaf, Apache Maven, y está alojado en AWS Elastic Beanstalk.
En nuestra discusión de cada consejo de seguridad, primero describiremos un vector de ataque para ilustrar cómo se puede explotar una vulnerabilidad relevante. Luego describiremos cómo proteger la vulnerabilidad y mitigar el vector de ataque. Tenga en cuenta que hay muchas formas de realizar una tarea determinada en Spring Boot; se sugieren estos ejemplos para ayudarlo a comprender mejor las posibles vulnerabilidades y los métodos de defensa.

Prevención de la inyección SQL utilizando consultas parametrizadas

La inyección SQL es un ataque común y fácil de entender. Los atacantes intentarán encontrar aberturas en la funcionalidad de su aplicación que les permitirán modificar las consultas SQL que su aplicación envía a la base de datos, o incluso enviar sus propias consultas SQL personalizadas. El objetivo del atacante es acceder a los datos confidenciales que se almacenan en la base de datos, a los que no debería accederse mediante el uso normal de la aplicación, o causar un daño irreparable al sistema bajo ataque.
Una forma común en que un atacante intentará inyectar SQL en su aplicación es a través de los parámetros de URL que se utilizan para crear consultas SQL que se envían a la base de datos. Por ejemplo, considere la siguiente URL de ejemplo:
https://fakesite.com/getTransaction?transactionId=12345  
Digamos que hay un punto final del controlador Spring Boot definido en el /getTransactionque acepta un ID de transacción en el parámetro de URL:
@GetMapping("/getTransaction")
public ModelAndView getTransaction(@RequestParam("transactionId") String transactionId) {

    ModelAndView modelAndView = new ModelAndView();

    sql = "SELECT transaction_user, transaction_amount FROM transaction WHERE transaction_id = " + transactionId;

    Transaction transaction = jdbcTemplate.query(sql, new TransactionRowMapper());

    modelAndView.addObject("transaction", transaction);
    modelAndView.setViewName("transaction");

    return modelAndView;
}
Observe que la declaración SQL en este ejemplo se construye utilizando concatenación de cadenas. El transactionIdes simplemente echa mano después de la cláusula "WHERE" usando el +operador.
Ahora imagine que un atacante usa la siguiente URL para acceder al sitio:
https://fakesite.com/getTransaction?transactionId=12345;+drop+table+transaction;  
En este caso, el transactionIdatacante manipula el parámetro de la URL (que se define como una Cadena en nuestro método de controlador) para agregar una declaración "DROP TABLE", por lo que el siguiente SQL se ejecutará contra la base de datos:
SELECT transaction_user, transaction_amount FROM transaction WHERE transaction_id = 12345; drop table transaction;  
Esto eliminaría la tabla de transacciones que conduce a una aplicación dañada y posiblemente a una pérdida de datos irreparable, debido al hecho de que la declaración SQL acepta el parámetro de URL proporcionado por el usuario y lo ejecuta como un código de SQL en vivo.
Para remediar la situación, podemos usar una función llamada consultas parametrizadas . En lugar de concatenar nuestras variables dinámicas directamente en sentencias de SQL, las consultas parametrizadas reconocen que se está pasando un valor dinámico inseguro y utiliza la lógica incorporada para garantizar que se escape todo el contenido proporcionado por el usuario. Esto significa que las variables que se pasan a través de consultas parametrizadas nunca se ejecutarán como código de SQL en vivo.
Aquí hay una versión de los fragmentos de código afectados arriba, actualizada para usar consultas parametrizadas:
sql = "SELECT transaction_user, transaction_amount FROM transaction WHERE transaction_id = ?";

Transaction transaction = jdbcTemplate.query(sql, new TransactionRowMapper(), transactionId);  
Observe el reemplazo del +operador y la transactionIdvariable directamente en la declaración SQL. Estos son reemplazados por el ?, que representa una variable que se pasará más adelante. La transactionIdvariable se pasa como un argumento al jdbcTemplate.query()método, que sabe que todos los parámetros que se pasan como argumentos deben escaparse. Esto evitará que cualquier entrada de usuario sea procesada por la base de datos como código de SQL en vivo.
Otro formato para pasar consultas parametrizadas en Java es el NamedParameterJdbcTemplate . Esto presenta una forma más clara de identificar y realizar un seguimiento de las variables pasadas a través de las consultas. En lugar de usar el ?símbolo para identificar los parámetros, NamedParameterJdbcTemplateusa dos puntos :seguidos del nombre del parámetro. Los nombres y valores de los parámetros se mantienen en un mapa o estructura de diccionario, como se ve a continuación:
Map<String, Object> params = new HashMap<>();

sql = "SELECT transaction_user, transaction_amount FROM transaction WHERE transaction_id = :transactionId";

params.put("transactionId", transactionId);

Transaction transaction = jdbcTemplate.query(sql, params, new TransactionRowMapper());  
Este ejemplo se comporta de manera idéntica al anterior, pero es más popular debido a la claridad que proporciona al identificar los parámetros en una declaración SQL. Esto es especialmente cierto en las sentencias de SQL más complejas que tendrían ?que controlarse para asegurarse de que están en el orden correcto.

Validación de entrada de parámetros de URL

Al pensar en la seguridad de la aplicación, una consideración primordial es enumerar todos los puntos en los que la aplicación acepta comentarios de los usuarios. Cada punto de entrada puede ser vulnerable si no se asegura adecuadamente y, como desarrolladores, debemos esperar que los atacantes intenten explotar todas las fuentes de entrada.
Una forma común en que las aplicaciones reciben datos de entrada de los usuarios es directamente de la cadena de URL en forma de parámetros de URL. La URL de muestra que usamos en la sección anterior es un ejemplo de pasar transactionIdun parámetro como una URL:
https://fakesite.com/getTransaction?transactionId=12345  
Supongamos que queremos asegurarnos de que la ID de transacción sea un número y que se encuentre dentro del rango de 1 y 100,000. Este es un proceso simple de dos pasos:
Agregue la @Validatedanotación en la clase de controlador en la que vive el método.
Use las anotaciones de validación en línea directamente en el @RequestParamargumento del método, de la siguiente manera:
@GetMapping("/getTransaction")
public ModelAndView getTransaction(@RequestParam("transactionId") @min(1) @max(100000) Integer transactionId) {  
    // Method content
}
Tenga en cuenta que hemos cambiado el tipo de la transactionIdque Integera partir de String, y añadimos el @min@maxanotaciones en línea con el transactionIdargumento de hacer cumplir el rango numérico especificado.
Si el usuario proporciona un parámetro no válido que no cumple con estos criterios, javax.validation.ContractViolationExceptionse emite un mensaje que se puede manejar para presentar al usuario un error que describe lo que hizo mal.
Aquí hay algunas otras anotaciones de restricción utilizadas comúnmente para la validación de parámetros de URL:
  • @Size: el tamaño del elemento debe estar entre los límites especificados.
  • @NotBlank: el elemento no debe ser NULL o vacío.
  • @NotNull: el elemento no debe ser NULL.
  • @AssertTrue: el elemento debe ser verdadero.
  • @AssertFalse: el elemento debe ser falso.
  • @Past: el elemento debe ser una fecha en el pasado.
  • @Future: el elemento debe ser una fecha en el futuro.
  • @Pattern: el elemento debe coincidir con una expresión regular especificada.

Validación de entrada de campo de formulario

Otro tipo más obvio de información del usuario proviene de los campos de formulario presentados a los usuarios finales con el propósito específico de recopilar información para ser guardada en la base de datos o procesada por la aplicación de alguna manera. Algunos ejemplos de campos de formulario son cuadros de texto, casillas de verificación, botones de opción y menús desplegables.
Normalmente, la entrada del campo de formulario se transmite del cliente al servidor a través de una solicitud POST. Dado que los datos del formulario generalmente incluyen información arbitraria del usuario, todos los datos del campo de entrada deben validarse para asegurarse de que no contengan valores maliciosos que puedan dañar la aplicación o exponer información confidencial.
Supongamos que estamos trabajando con una aplicación web veterinaria que tiene un formulario web que permite a los usuarios finales registrar a su mascota. Nuestro código Java incluiría una clase de dominio que representa una mascota, de la siguiente manera:
@Entity
public class Pet {

    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Integer id;

    @NotBlank(message="Name must not be empty")
    @Size(min=2, max=40)
    @Pattern(regexp="^$|[a-zA-Z ]+$", message="Name must not include special characters.")
    private String name;

    @NotBlank(message="Kind must not be empty")
    @Size(min=2, max=30)
    @Pattern(regexp="^$|[a-zA-Z ]+$", message="Kind must not include special characters.")
    private String kind;

    @NotBlank(message="Age must not be empty")
    @Min(0)
    @Max(40)
    private Integer age;

    // standard getter and setter methods...
}
Tenga en cuenta las anotaciones de restricción que se han incluido en cada campo. Estos funcionan de la misma manera que se describe en la sección anterior, excepto que hemos especificado una messagepara algunos de ellos, que anulará los mensajes de error predeterminados que se muestran al usuario cuando se viola la restricción correspondiente.
Tenga en cuenta que cada campo tiene anotaciones que especifican el rango en el que debe encajar el campo. Además, los Stringcampos (nombre y tipo) tienen una @Patternanotación, que implementa una restricción de expresiones regulares que solo acepta letras y espacios. Esto evita que los atacantes intenten incluir caracteres y símbolos especiales, que pueden tener importancia en contextos de código como la base de datos o el navegador.
El formulario HTML contiene los Petcampos de la clase correspondiente , incluido el nombre de la mascota, el tipo de animal, la edad y puede verse como a continuación:
Tenga en cuenta que este código HTML incluido incluye etiquetas de plantilla Thymeleaf para marcar el HTML.
<form id="petForm" th:action="@{/submitNewPet}" th:object="${pet}" method="POST">  
    <input type="text" th:field="*{name}" placeholder="Enter pet name…" />

    <select th:field="*{kind}">
        <option value="cat">Cat</option>
        <option value="dog">Dog</option>
        <option value="hedgehog">Hedgehog</option>
    </select>

    <input type="number" th:field="*{age}" />

    <input type="submit" value="Submit Form" />
</form>  
Cuando se completan los campos del formulario y se hace clic en el botón "Enviar", el navegador enviará una solicitud POST al servidor en el punto final "/ submitNewPet". Esto será recibido por un @RequestMappingmétodo, definido de la siguiente manera:
@PostMapping("/submitNewPet")
public ModelAndView submitNewPet(@Valid @ModelAttribute("pet") Pet pet, BindingResult bindingResult) {

    ModelAndView modelAndView = new ModelAndView();

    if (bindingResult.hasErrors()) {
        modelAndView.addObject("pet", pet);
        modelAndView.setViewName("submitPet");
    } else {
        modelAndView.setViewName("submitPetConfirmation");
    }

    return modelAndView;
}
La @Validanotación en el argumento del método impondrá las validaciones definidas en el Petobjeto de dominio. El bindingResultargumento es manejado automáticamente por la primavera y contendrá errores si alguno de los atributos tienen validaciones modelo de restricción. En este caso, incorporamos un inicio de sesión simple para volver a cargar la submitPetpágina si se violan las restricciones y mostrar una página de confirmación si los campos del formulario son válidos.

Codificación de salida para prevenir ataques XSS reflejados

El último tema de seguridad que vamos a discutir es la codificación de salida de los datos proporcionados por el usuario y los datos recuperados de la base de datos.
Imagine un escenario en el que un atacante puede pasar un valor como entrada a través de un parámetro de URL, campo de formulario o llamada a API. En algunos casos, esta entrada suministrada por el usuario podría pasarse como una variable directamente a la plantilla de vista que se devuelve al usuario, o podría guardarse en la base de datos.
Por ejemplo, el atacante pasa una cadena que es un código Javascript válido, como por ejemplo:
alert('This app has totally been hacked, bro');  
Consideremos los escenarios donde la cadena anterior se guarda en un campo de la base de datos como un comentario, que luego se recuperará en la plantilla de vista y se mostrará al usuario en su navegador de Internet. Si la variable no se escapa correctamente, la alert()declaración se ejecutará como un código en vivo tan pronto como la página sea recibida por el navegador del usuario, verán la alerta emergente. Si bien es molesto, en un ataque real este código no sería una alerta, sería un script malicioso que podría engañar al usuario para que haga algo desagradable.
De hecho, el contenido malicioso proporcionado por el usuario no necesariamente tiene que ser guardado en la base de datos para causar daño. En muchos casos, la entrada proporcionada por el usuario, como los nombres de usuario, esencialmente se repite al usuario para que aparezca en la página que está visitando. Estos se denominan ataques "reflejados" por este motivo, ya que la entrada maliciosa se refleja de nuevo en el navegador, donde puede hacer daño.
En ambos casos, el contenido dinámico debe estar correctamente codificado en salida (o escaparse) para asegurarse de que el navegador no lo procese como código JavaScript, HTML o XML en vivo.
Esto se puede lograr fácilmente usando un motor de plantillas maduro, como Thymeleaf. Thymeleaf se puede integrar fácilmente en una aplicación Spring Boot agregando las dependencias de archivos POM requeridas y realizando algunos pasos de configuración menores que no veremos aquí. El th:textatributo en Thymeleaf tiene una lógica incorporada que manejará la codificación de cualquier variable que se transmita de la siguiente manera:
<h1>Welcome to the Site! Your username is: <span th:text="${username}"></span></h1>  
En este caso, incluso si la usernamevariable contenía código malicioso, por ejemplo alert('You have been hacked');, el texto solo se mostraría en la página en lugar de ejecutarse como código Javascript en vivo por el navegador. Esto se debe a la lógica de codificación incorporada de Thymeleaf.

Acerca de: Programator

Somos Instinto Programador

0 comentarios:

Publicar un comentario

Dejanos tu comentario para seguir mejorando!

Con tecnología de Blogger.