diff --git a/assets/php/form.php b/assets/php/form.php
index f5a057a..42e1c5a 100644
--- a/assets/php/form.php
+++ b/assets/php/form.php
@@ -3,16 +3,35 @@
/**
* Clase base para la gestión de formularios.
*
- * Además de la gestión básica de los formularios.
+ * Gestión de token CSRF está basada en: https://www.owasp.org/index.php/PHP_CSRF_Guard
*/
abstract class Form {
/**
- * @var string Cadena utilizada como valor del atributo "id" de la etiqueta <form> asociada al formulario y
- * como parámetro a comprobar para verificar que el usuario ha enviado el formulario.
+ * @var string Sufijo para el nombre del parámetro de la sesión del usuario donde se almacena el token CSRF.
+ */
+ const CSRF_PARAM = 'csrf';
+
+ /**
+ * @var string Identificador utilizado para construir el atributo "id" de la etiqueta <form> como $tipoFormulario.$formId
.
*/
private $formId;
+ /**
+ * @var string Valor del parámetro enctype del formulario.
+ */
+ private $enctype;
+
+ /**
+ * @var string Valor del atributo "class" de la etiqueta <form> asociada al formulario. Si este parámetro incluye la cadena "nocsrf" no se generá el token CSRF para este formulario.
+ */
+ private $classAtt;
+
+ /**
+ * @var string Parámetro de la petición utilizado para comprobar que el usuario ha enviado el formulario..
+ */
+ private $tipoFormulario;
+
/**
* @var string URL asociada al atributo "action" de la etiqueta <form> del fomrulario y que procesará el
* envío del formulario.
@@ -37,22 +56,35 @@ abstract class Form {
*
$_SERVER['PHP_SELF']
$tipoFormulario.$formId
.
*
* @param array $opciones (ver más arriba).
*/
- public function __construct($formId, $opciones = array() )
+ public function __construct($tipoFormulario, $opciones = array(), $formId = 1)
{
- $this->formId = $formId;
+ $this->tipoFormulario = $tipoFormulario;
+ $this->formId = $tipoFormulario.$formId;
- $opcionesPorDefecto = array( 'action' => null, );
+ $opcionesPorDefecto = array( 'action' => null, 'class' => null, 'enctype' => null );
$opciones = array_merge($opcionesPorDefecto, $opciones);
$this->action = $opciones['action'];
+ $this->classAtt = $opciones['class'];
+ $this->enctype = $opciones['enctype'];
if ( !$this->action ) {
$this->action = htmlentities($_SERVER['PHP_SELF']);
@@ -81,6 +113,13 @@ abstract class Form {
if ( ! $this->formularioEnviado($_POST) ) {
return $this->generaFormulario();
} else {
+ // Valida el token CSRF si es necesario (hay un token en la sesión asociada al formulario)
+ $tokenRecibido = $_POST['CSRFToken'] ?? FALSE;
+ $errores = $this->csrfguard_ValidateToken($this->tipoFormulario, $tokenRecibido);
+
+ // limpia los tokens CSRF que no han sido utilizados en esta petición
+ self::limpiaCsrfTokens();
+
$result = $this->procesaFormulario($_POST);
if ( is_array($result) ) {
return $this->generaFormulario($_POST, $result);
@@ -133,7 +172,7 @@ abstract class Form {
*/
private function formularioEnviado(&$params)
{
- return isset($params['action']) && $params['action'] == $this->formId;
+ return isset($params['action']) && $params['action'] == $this->tipoFormulario;
}
/**
@@ -149,12 +188,29 @@ abstract class Form {
{
$htmlCamposFormularios = $this->generaCamposFormulario($datos, $errores);
+ $classAtt='';
+ if ( $this->classAtt ) {
+ $classAtt = " class=\"{$this->classAtt}\"";
+ }
+
+ $enctypeAtt='';
+ if ( $this->enctype ) {
+ $enctypeAtt = " enctype=\"{$this->enctype}\"";
+ }
+
+ // Se genera el token CSRF si el usuario no solicita explícitamente lo contrario.
+ $tokenCSRF = '';
+ if ( ! $this->classAtt || strpos($this->classAtt, 'nocsrf') === false ) {
+ $tokenValue = $this->csrfguard_GenerateToken($this->tipoFormulario);
+ $tokenCSRF = "";
+ }
+
/* <<< Permite definir cadena en múltiples líneas.
* Revisa https://www.php.net/manual/en/language.types.string.php#language.types.string.syntax.heredoc
*/
- $htmlForm = "";
return $htmlForm;
}
@@ -213,4 +269,70 @@ abstract class Form {
return $html;
}
+
+ /**
+ * Método para eliminar los tokens CSRF almecenados en la petición anterior que no hayan sido utilizados en la actual.
+ */
+ public static function limpiaCsrfTokens()
+ {
+ foreach(array_keys($_SESSION) as $key) {
+ if (strpos($key, self::CSRF_PARAM) === 0) {
+ unset($_SESSION[$key]);
+ }
+ }
+ }
+
+ private function csrfguard_GenerateToken($formParameter)
+ {
+ if ( ! session_id() ) {
+ throw new \Exception('La sesión del usuario no está definida.');
+ }
+
+ $paramSession = self::CSRF_PARAM.'_'.$formParameter;
+ if (isset($_SESSION[$paramSession])) {
+ $token = $_SESSION[$paramSession];
+ } else {
+ if ( function_exists('hash_algos') && in_array('sha512', hash_algos()) ) {
+ $token = hash('sha512', mt_rand(0, mt_getrandmax()));
+ } else {
+ $token=' ';
+ for ($i=0;$i<128;++$i) {
+ $r=mt_rand(0,35);
+ if ($r<26){
+ $c=chr(ord('a')+$r);
+ } else{
+ $c=chr(ord('0')+$r-26);
+ }
+ $token.=$c;
+ }
+ }
+
+ $_SESSION[$paramSession]=$token;
+ }
+ return $token;
+ }
+
+ private function csrfguard_ValidateToken($formParameter, $tokenRecibido)
+ {
+ if ( ! session_id() ) {
+ throw new \Exception('La sesión del usuario no está definida.');
+ }
+
+ $result = TRUE;
+
+ $paramSession = self::CSRF_PARAM.'_'.$formParameter;
+ if ( isset($_SESSION[$paramSession]) ) {
+ if ( $_SESSION[$paramSession] !== $tokenRecibido ) {
+ $result = array();
+ $result[] = 'Has enviado el formulario dos veces';
+ }
+ $_SESSION[$paramSession] = ' ';
+ unset($_SESSION[$paramSession]);
+ } else {
+ $result = array();
+ $result[] = 'Formulario no válido';
+ }
+ return $result;
+ }
+
}