Validaciones de parámetros de entrada
- 1 Introducción
- 2 ¿Por que validar los parámetros si son parámetros tipados?
- 3 Filtrado de HTML malicioso
- 4 Validación en Servidor
- 5 ¿Cómo validar correctamente los parámetros de entrada mediante Expresiones regulares?
- 6 Crear una expresión regular
- 7 Ejemplo de una insuficiente validación de datos de entrada
- 8 Validación de parámetros en ProtoBuf
- 9 Referencias
Introducción
Una gran cantidad de vulnerabilidades (fácilmente sobre 70-80%) se explotan debido a la falta de validación de parámetros (o datos) de entrada, como por ejemplo un formulario de login, una casilla de búsqueda... Pero también puede suceder sobre parámetros aparentemente inofensivos pero que no se validan correctamente. Incluso se pueden realizar directamente sobre las APIs, por lo que la validación en el lado de cliente es insuficiente.
Los riesgos de estas vulnerabilidades son multiples (normalmente graves). Por ejemplo, un caso crítico sería la inyección de comandos (véase https://owasp.org/www-community/attacks/Command_Injection ) lo que permitiría abrir una linea de comandos con el promt del usuario que ejecuta la aplicación.
Por ello recomendamos:
Filtrado de HTML malicioso.
Validar en el cliente el parámetro para no dejar fluir peticiones al servidor, como medida de mejorar el rendimiento del servidor. Adicionalmente se recomienda utilizar un anti-CSRF token.
Validar SIEMPRE en el lado del servidor los parámetros de entrada en el servidor.
¿Por que validar los parámetros si son parámetros tipados?
Es un error común pensar en que un dato tipado es un dato correctamente validado, como veremos, no es así.
Una cadena de caracteres sin validar puede conllevar multiples riesgos, como por ejemplos inyecciones de código, ejecución de comandos remotos, etc...
Por este motivo SIEMPRE se debe validar los datos de entrada y en casos necesarios escapar los carácteres conflictivos en su presentación.
Si lo deseas puede ver el siguiente video con un ejemplo de inyección SQL:
SQL Injection | Owasp Top 10 Explainer Video | Secure Code Warrior
Filtrado de HTML malicioso
Se recomienda en general que cualquier entrada se valide y se "limpie" para evitar la inyección de HTML en la capa vista, evitando así los Cross-site Scripting.
Se recomienda siempre
Las anotaciones de Spring u otros método de validación de parámetros no garantizan absolutamente que se valide correctamente el parámetro de entrada, por tanto, debemos aun así escapar las posibles inyecciones de HTML que hayan evadido este sistema.
Se recomienda hacerlo siempre.
A continuación dejamos las siguiente referencias:
https://docs.jboss.org/hibernate/validator/6.0/api/org/hibernate/validator/constraints/SafeHtml.html
https://jsoup.org/cookbook/cleaning-html/whitelist-sanitizer
Validación en Servidor
La validación en el servidor se debe realizar bajo las siguientes recomendaciones:
Si se esta utilizando un framework que incluye librerías o métodos de validación, se recomienda utilizar estos. Para ello, se debe revisar la documentación del mismo para garantizar que se utiliza correctamente.
Mediante librerías de validación de parámetros como ESAPI de OWASP.
Mediante código en el momento de uso, para ello se puede validar mediante expresiones regulares.
Verifique que...
Entendemos que los 2 primeros métodos son delegados en un tercero, por ello debemos verificar que la fuente es confiable y que hacemos un uso correcto del mismo
Uso de Framework
Los frameworks actuales suelen llevar incorporados métodos de validación, para ello debemos revisar la documentación y hacer un uso correcto de la misma.
Normalmente el funcionamiento es similar a validar nosotros el datos, pero utilizando el sistema de validación interno del framework.
Veamos un ejemplo simplificado:
En Java sin framework podríamos validar de esta manera
Clase "Employee " donde guardamos la contraseña
package com.javatpoint;
import javax.validation.constraints.Pattern;
public class Employee {
private String name;
private String pass; // No existe validación en la clase
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPass() {
return pass;
}
public void setPass(String pass) {
this.pass = pass;
}
} |
Clase "EmployeeController " es el controlador de la clase "Employee "
package com.javatpoint;
import javax.validation.Valid;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class EmployeeController {
private static final Pattern passwordPattern = Pattern.compile("^[a-zA-Z0-9]{3}");
@RequestMapping("/hello")
public String display(Model m)
{
m.addAttribute("emp", new Employee());
return "viewpage";
}
@RequestMapping("/helloagain")
public String submitForm(@ModelAttribute("emp") Employee employee , BindingResult br)
{
// Validamos del objeto empleado la contraseña
try {
if ( !passwordPattern .matcher(employee.getPassword()).matches() {
throw new YourValidationException( "Improper password format." ); //Si es incorrecta la validación continua el flujo mediante excepción
}
// Si es correcta la validación continua el flujo normal de ejecución
if(br.hasErrors())
{
return "viewpage";
}
else
{
return "final";
}
} catch(YourValidationException e ) {
response.sendError( response.SC_BAD_REQUEST, e.getMessage() );
}
}
} |
En java mediante el sistema de anotaciones (el sistema interno de validación)
Clase "Employee " donde guardamos la contraseña
package com.javatpoint;
import javax.validation.constraints.Pattern;
public class Employee {
private String name;
@Pattern(regexp="^[a-zA-Z0-9]{3}",message="length must be 3") // Configuramos la expresión regular mediante anotación en la clase
private String pass;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPass() {
return pass;
}
public void setPass(String pass) {
this.pass = pass;
}
} |
Clase "EmployeeController " es el controlador de la clase "Employee "
package com.javatpoint;
import javax.validation.Valid;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class EmployeeController {
@RequestMapping("/hello")
public String display(Model m)
{
m.addAttribute("emp", new Employee());
return "viewpage";
}
@RequestMapping("/helloagain")
public String submitForm(@Valid @ModelAttribute("emp") Employee e, BindingResult br) // No olvidar añadir la anotación @Valid para indicar que debe ser validado
{
if(br.hasErrors())
{
return "viewpage";
}
else
{
return "final";
}
}
} |
Uso de librerías externas
En este caso, si no utilizamos un framework podemos utilizar librerías especificas como ESAPI para la validación de parámetros de entrada.
Use librerías recomendadas en este Marco
Tenga especial atención en utilizar librerías recomendadas en el Marco de Seguridad, ya que las librerías de validación de fuente no confiables pueden contener vulnerabilidades o no validar de la manera adecuada. Esto implica añadir un riesgo al Producto pensando además que estamos mitigando otros problemas.
Ejemplo
public static String getValidFilePath(String filePath) throws ValidationException {
if (filePath == null || filePath.trim().equals("")) {
throw new FacesException("Path can not be the empty string or null");
}
try {
SafeFile file = new SafeFile(filePath);
File parentFile = file.getParentFile();
if (!file.exists()) {
throw new ValidationException("Invalid directory", "Invalid directory, \"" + file + "\" does not exist.");
}
if (!parentFile.exists()) {
throw new ValidationException("Invalid directory", "Invalid directory, specified parent does not exist.");
}
if (!parentFile.isDirectory()) {
throw new ValidationException("Invalid directory", "Invalid directory, specified parent is not a directory.");
}
if (!file.getCanonicalPath().startsWith(parentFile.getCanonicalPath())) {
throw new ValidationException("Invalid directory", "Invalid directory, \"" + file + "\" does not inside specified parent.");
}
if (!file.getCanonicalPath().equals(filePath)) {
throw new ValidationException("Invalid directory", "Invalid directory name does not match the canonical path");
}
}
catch (IOException ex) {
throw new ValidationException("Invalid directory", "Failure to validate directory path");
}
return filePath;
} |
Validación sin frameworks ni librerías
Tenga en cuenta que:
Aviso!
Este método no se recomienda debido a que añade trabajo y puede generar un error humano en la codificación de la validación. Por tanto, excepto si no existe framework ni librería de validación, no implemente este método.
Ejemplo
private static final Pattern zipPattern = Pattern.compile("^\d{5}(-\d{4})?$");
public void doPost( HttpServletRequest request, HttpServletResponse response) {
try {
String zipCode = request.getParameter( "zip" );
if ( !zipPattern.matcher( zipCode ).matches() {
throw new YourValidationException( "Improper zipcode format." );
}
// Aquí se añadiría el código en caso de ser correcta la validación
} catch(YourValidationException e ) {
response.sendError( response.SC_BAD_REQUEST, e.getMessage() );
}
} |
¿Cómo validar correctamente los parámetros de entrada mediante Expresiones regulares?
Se debe revisar:
El tipo de dato. Por ejemplo: Integer, Long, String, etc...
Rango de datos. Por ejemplo: si es un entero que debe contener el precio de un producto, este NO debe ser negativo, por tanto el valor mínimo es 0.
Formato del dato. Por ejemplo, en caso de esperar un email, la cadena de caracteres debe cumplir un formato de expresión regular del tipo:
^[a-zA-Z0-9_+&*-]+(?:\.[a-zA-Z0-9_+&*-]+)*@(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,7}$
En casos especiales como por ejemplo las consultas a base de datos, se deben utilizar las consultas parametrizadas.
Crear una expresión regular
Si necesitas crear una expresión regular para tu desarrollo, primero verificar que no esta en este enlace. Si no estuviera y tienes que crearla de cero, te recomendamos estos enlaces:
Tenga cuidado!
En caso de crear una expresón regular, debe garantizarse que haga de una manera incorrecta ya que además del riesgo de no validar correctamente si no esta bien desarrollada, podemos insertar una vulnerabilidad por regular expression denial of service (ReDoS) en el código.
Ejemplo de una insuficiente validación de datos de entrada
En la siguientes imagenes podemos ver la explotación de un Cross-Site Scripting reflejado (XSS reflejado)
En este caso se introduce:
<script>alert(document.cookie)</script> |
Envíamos la petición
Y vemos como se ha explotado con éxito el ataque.
En este caso no se esta validando correctamente el parámetro de entrada en el servidor y adicionalmente, tampoco se esta escapando en la presentación el código inyectado.
Validación de parámetros en ProtoBuf
Este protocolo de comunicación nos permite crear tipos de mensajes de una manera eficiente y rápida. Normalmente, desde el punto de vista funcional se definen los mensajes con el tipo deseado sin tener en cuenta que este protocolo tiene incorporado un sistema de validación.
Por ejemplo, podemos definir el siguiente mensaje:
syntax = "proto3";
package validator.examples;
import "github.com/mwitkow/go-proto-validators/validator.proto";
message InnerMessage {
// some_integer can only be in range (0, 100).
int32 some_integer = 1 [(validator.field) = {int_gt: 0, int_lt: 100}]; // Validación de los rangos numericos
// some_float can only be in range (0;1).
double some_float = 2 [(validator.field) = {float_gte: 0, float_lte: 1}]; // Validación de los rangos numericos
}
message OuterMessage {
// important_string must be a lowercase alpha-numeric of 5 to 30 characters (RE2 syntax).
string important_string = 1 [(validator.field) = {regex: "^[a-z0-9]{5,30}$"}]; // Validación mediante expresión regular
// proto3 doesn't have `required`, the `msg_exist` enforces presence of InnerMessage.
InnerMessage inner = 2 [(validator.field) = {msg_exists : true}];
} |
La compilación del mismo:
func (this *InnerMessage) Validate() error {
if !(this.SomeInteger > 0) {
return fmt.Errorf("validation error: InnerMessage.SomeInteger must be greater than '0'")
}
if !(this.SomeInteger < 100) {
return fmt.Errorf("validation error: InnerMessage.SomeInteger must be less than '100'")
}
if !(this.SomeFloat >= 0) {
return fmt.Errorf("validation error: InnerMessage.SomeFloat must be greater than or equal to '0'")
}
if !(this.SomeFloat <= 1) {
return fmt.Errorf("validation error: InnerMessage.SomeFloat must be less than or equal to '1'")
}
return nil
}
var _regex_OuterMessage_ImportantString = regexp.MustCompile("^[a-z0-9]{5,30}$")
func (this *OuterMessage) Validate() error {
if !_regex_OuterMessage_ImportantString.MatchString(this.ImportantString) {
return fmt.Errorf("validation error: OuterMessage.ImportantString must conform to regex '^[a-z0-9]{5,30}$'")
}
if nil == this.Inner {
return fmt.Errorf("validation error: OuterMessage.Inner message must exist")
}
if this.Inner != nil {
if err := validators.CallValidatorIfExists(this.Inner); err != nil {
return err
}
}
return nil
} |
Ejemplo extraído de la siguiente fuente: GitHub - mwitkow/go-proto-validators: Generate message validators from .proto annotations.
Conclusión
Como hemos visto anteriormente, es necesario que SIEMPRE, se validen los parámetros de entrada. Para ello solo debemos verificar la existencia de algún sistema propio del lenguaje, framework, etc...
Referencias
https://owasp.org/www-community/OWASP_Validation_Regex_Repository
https://cheatsheetseries.owasp.org/cheatsheets/Input_Validation_Cheat_Sheet.html