Hasta ahora, te has familiarizado con los conceptos básicos del desarrollo de plugins y temas para WordPress. Ahora sería un buen momento para considerar también las implicaciones de seguridad de escribir código.
En esta lección, aprenderás a desarrollar teniendo en cuenta la seguridad.
Aprenderás los beneficios de garantizar que tu código sea seguro, qué pasos seguir para protegerlo y dónde encontrar más información sobre el desarrollo teniendo en cuenta la seguridad.
Aviso legal
Esta lección se creó como una introducción a la importancia de la seguridad al desarrollar plugins y temas. El código utilizado en esta lección está muy simplificado. No debes utilizar ninguno de los fragmentos de código de este tutorial en tus plugins o temas.
Asegúrate de leer la documentación completa sobre Seguridad en el manual para desarrolladores de WordPress en https://developer.wordpress.org/apis/security/ para garantizar que sigues los métodos y procedimientos correctos.
¿Qué significa desarrollar teniendo en cuenta la seguridad?
Desarrollar teniendo en cuenta la seguridad es el proceso de garantizar que tu código no solo funcione, sino que tampoco introduzca vulnerabilidades de seguridad.
Si el código de tu plugin o tema tiene vulnerabilidades de seguridad, podría hacer que cualquier sitio de WordPress que tenga tu producto instalado quede expuesto a posibles ataques y provocar que dicho sitio se vea comprometido.
Al escribir código, es importante desarrollar una mentalidad orientada a la seguridad y pensar en cómo podría utilizarse tu código de forma maliciosa.
- No confíes en ningún dato, ya sea la entrada del usuario, los datos de una API de terceros o incluso los datos de tu base de datos. Comprueba siempre que sea válido y seguro de utilizar.
- WordPress cuenta con varias API que pueden ayudarte con tareas habituales, como sanear la entrada del usuario, validar datos y escapar la salida. Utiliza estas API para ayudar a validar y sanear tus datos en lugar de escribir tus propias funciones.
- Mantente al día con las vulnerabilidades comunes y mantén tu código actualizado para prevenirlas.
¿En qué parte del proceso de desarrollo encaja la seguridad?
La seguridad debe considerarse en cada etapa del proceso de desarrollo.
Por lo general, la mayor cantidad de vulnerabilidades de seguridad detectadas se encuentran en la capa de PHP, que se ejecuta en el servidor.
Por lo tanto, esta lección se centrará en las medidas preventivas más comunes en tu código PHP.
Sanear las entradas
Uno de los primeros pasos que debes seguir al desarrollar es asegurarte de que cualquier entrada del usuario esté saneada. Esto significa que cualquier dato que se transfiera desde el usuario, como el envío de un formulario o un parámetro de una URL, se comprueba para garantizar que sea seguro de utilizar.
En este ejemplo de código, los campos de nombre y correo electrónico se envían desde un formulario que genera un plugin y después se guardan en una tabla personalizada de la base de datos llamada form_submissions;
$name = $_POST['name'];
$email = $_POST['email'];
global $wpdb;
$table_name = $wpdb->prefix . 'form_submissions';
$sql = "INSERT INTO $table_name (name, email) VALUES ('$name', '$email')";
$result = $wpdb->query($sql);
Como puedes ver, los datos se guardan directamente en la base de datos, sin ningún saneamiento.
Esto significa que si un usuario enviara un nombre como John'; DROP TABLE form_submissions;--, se ejecutaría la consulta SQL INSERT, seguida de la consulta DROP, ¡y la tabla se eliminaría de la base de datos!
WordPress cuenta con una API de saneamiento que puede utilizarse para sanear los datos entrantes. Puedes utilizar las funciones sanitize_text_field y sanitize_email para sanear los campos de nombre y correo electrónico antes de utilizarlos en la consulta.
$name = sanitize_text_field( $_POST['name'] );
$email = sanitize_email( $_POST['email'] );
global $wpdb;
$table_name = $wpdb->prefix . 'form_submissions';
$sql = "INSERT INTO $table_name (name, email) VALUES ('$name', '$email')";
$result = $wpdb->query($sql);
Observa que el código sigue un principio clave del saneamiento de datos: hacerlo lo antes posible.
Para leer más sobre las funciones de saneamiento disponibles para los desarrolladores de WordPress, consulta la página Sanear datos de la documentación para desarrolladores de WordPress.
Validar datos (4:06)
Validar datos es el proceso de comprobarlos frente a un patrón o conjunto de patrones predefinidos con un resultado definitivo: válido o no válido.
Los datos que no son de confianza pueden proceder de muchas fuentes: usuarios, datos de API de terceros e incluso los datos de tu base de datos pueden considerarse no confiables, especialmente si algo más los ha modificado. Incluso los administradores del sitio pueden cometer errores e introducir datos incorrectos o inseguros, por lo que es importante comprobar siempre tus datos.
En este ejemplo, una función de eliminación requiere que se envíe un ID numérico a una devolución de llamada de admin-ajax:
add_action( 'wp_ajax_delete_form_submission', 'wp_learn_delete_form_submission' );
function wp_learn_delete_form_submission() {
if ( ! isset( $_POST['id'] ) ) {
wp_send_json_error( 'ID no válido' );
}
$id = $_POST['id'];
global $wpdb;
$table_name = $wpdb->prefix . 'form_submissions';
$sql = "DELETE FROM $table_name WHERE id = $id";
$result = $wpdb->get_results( $sql );
return wp_send_json( array( 'result' => $result ) );
}
Aquí el ID se utiliza directamente en la consulta SQL, sin ninguna validación. De nuevo, esto significa que si un usuario enviara un ID como 1; DROP TABLE form_submissions;, se ejecutaría la misma consulta SQL DROP después de la eliminación y la tabla desaparecería.
Para evitarlo, puedes utilizar la funcionalidad de conversión de tipos de PHP para garantizar que el valor de $id siempre sea un entero. Esto puede hacerse añadiendo (int) antes del nombre de la variable, así:
$id = (int) $_POST['id'];
Ten en cuenta que esto solo funcionará si el primer carácter de la cadena pasada mediante el array $_POST se puede convertir en un entero; de lo contrario, el valor de $id será 0. En ese caso, es recomendable actualizar el código para gestionar esta situación.
El código también sigue un principio clave de la validación de datos: hacerlo lo antes posible.
if ($id === 0){
// devolver pronto con un error
return wp_send_json( array( 'result' => 'Se ha pasado un ID no válido' ) );
}
Para leer más sobre las distintas formas de validar datos, consulta la sección Validar datos de la documentación para desarrolladores de WordPress.
Escapar las salidas
Otro aspecto de la seguridad consiste en garantizar que cualquier información que muestres en el navegador sea segura, incluidos textos, HTML o código JavaScript, así como datos de la base de datos.
Aunque tu código no sea responsable del origen de los datos que se muestran, sí es responsable de mostrarlos de forma segura.
En este ejemplo, el código obtiene los envíos del formulario de la base de datos, recorre los envíos y muestra sus datos en una pantalla de administración del escritorio de WordPress:
$submissions = wp_learn_get_form_submissions();
?>
<div class="wrap" id="wp_learn_admin">
<h1>Administración</h1>
<table>
<thead>
<tr>
<th>Nombre</th>
<th>Correo electrónico</th>
</tr>
</thead>
<?php foreach ($submissions as $submission){ ?>
<tr>
<td><?php echo $submission->name; ?></td>
<td><?php echo $submission->email; ?></td>
<td><a class="delete-submission" data-id="<?php echo $submission->id?>" style="cursor:pointer;">Eliminar</a></td>
</tr>
<?php } ?>
</table>
</div>
<?php
Aquí tenemos tres datos que deben escaparse: los campos $submission->name y $submission->email, además de $submission->id.
<td><a class="delete-submission" data-id="<?php echo (int) $submission->id?>" style="cursor:pointer;">Eliminar</a></td>
Para los campos de nombre y correo electrónico, podemos utilizar la función de escape integrada de WordPress esc_html(). El ID puede escaparse convirtiéndolo en un entero, como harías para validar los datos.
<td><?php echo esc_html( $submission->name ); ?></td>
<td><?php echo esc_html( $submission->email ); ?></td>
<td><a class="delete-submission" data-id="<?php echo (int) $submission->id?>" style="cursor:pointer;">Eliminar</a></td>
Observa que este código sigue un principio clave del escape de datos: escapar los datos lo más tarde posible.
Para leer más sobre las distintas formas de escapar salidas, consulta la sección Escapar datos de la documentación para desarrolladores de WordPress.
Prevenir solicitudes no válidas
Cada vez que se realiza una solicitud, es importante comprobar que sea válida. Esto significa comprobar que la solicitud proceda de una fuente de confianza.
Por ejemplo, podrías tener un shortcode que muestre un formulario en el que los usuarios puedan enviar su información.
La función para mostrar el formulario podría ser algo parecido a esto.
add_shortcode( 'wp_learn_form_shortcode', 'wp_learn_form_shortcode' );
function wp_learn_form_shortcode() {
ob_start();
?>
<form method="post">
<input type="hidden" name="wp_learn_form" value="submit">
<div>
<label for="email">Nombre</label>
<input type="text" id="name" name="name" placeholder="Name">
</div>
<div>
<label for="email">Dirección de correo electrónico</label>
<input type="text" id="email" name="email" placeholder="Email address">
</div>
<div>
<input type="submit" id="submit" name="submit" value="Submit">
</div>
</form>
<?php
$form = ob_get_clean();
return $form;
}
?>
Cuando el usuario envía el formulario, los datos se envían al servidor. Después, los datos se procesan, se sanean y se almacenan en la base de datos.
add_action( 'wp', 'wp_learn_maybe_process_form' );
function wp_learn_maybe_process_form() {
if (!isset($_POST['wp_learn_form'])){
return;
}
$name = sanitize_text_field( $_POST['name'] );
$email = sanitize_email( $_POST['email'] );
global $wpdb;
$table_name = $wpdb->prefix . 'form_submissions';
$sql = "INSERT INTO $table_name (name, email) VALUES ('$name', '$email')";
$result = $wpdb->query($sql);
if ( 0 < $result ) {
wp_redirect( WPLEARN_SUCCESS_PAGE_SLUG );
die();
}
wp_redirect( WPLEARN_ERROR_PAGE_SLUG );
die();
}
Como el formulario podría aparecer en cualquier página en la que se utilice el shortcode, un usuario malicioso podría intentar enviar una solicitud POST al formulario, ya sea buscando una vulnerabilidad en el plugin o enviando varias solicitudes al formulario.
Para evitarlo, puedes comprobar que la solicitud proceda de una fuente de confianza. Para ello, puedes implementar algo llamado nonce, o número utilizado una sola vez.
Primero, en el propio formulario, puedes añadir un campo nonce utilizando la función wp_nonce_field y pasando una acción nonce y un nombre nonce a la función:
wp_nonce_field( 'wp_learn_form_nonce_action', 'wp_learn_form_nonce_field' );
Cuando el formulario se muestra en la interfaz, se añade un campo oculto al formulario utilizando el nombre nonce como atributos id y name del campo oculto, y el nonce generado como valor del campo. Estos datos se envían cuando se presenta el formulario.
Después, en la función que procesa los datos del formulario, puedes verificar que el nonce sea válido mediante la función wp_verify_nonce, pasando a esta función el campo nonce enviado por POST y la acción nonce. Si el resultado de esta verificación es falso, puedes salir pronto y evitar la ejecución de código adicional.
if ( ! wp_verify_nonce( $_POST['wp_learn_form_nonce_field'], 'wp_learn_form_nonce_action' ) {
wp_redirect( WPLEARN_ERROR_PAGE_SLUG );
die();
}
Cada vez que tu código realice una solicitud web, ya sea mediante una redirección a una nueva URL, el envío de datos POST a un formulario o una solicitud AJAX, debes comprobar que la solicitud sea válida.
Para leer más sobre cómo utilizar nonces en tus plugins, consulta la sección Nonces de la documentación para desarrolladores de WordPress.
Prevenir usuarios no autenticados
Dependiendo de la funcionalidad de tu código, es recomendable restringir ciertas funciones únicamente a usuarios con un nivel de permisos específico. Por ejemplo, podrías tener una función que elimine datos de la base de datos.
function wp_learn_delete_form_submission() {
if ( ! isset( $_POST['id'] ) ) {
wp_send_json_error( 'ID no válido' );
}
$id = (int) $_POST['id'];
global $wpdb;
$table_name = $wpdb->prefix . 'form_submissions';
$sql = "DELETE FROM $table_name WHERE id = $id";
$result = $wpdb->get_results( $sql );
return wp_send_json( array( 'result' => $result ) );
}
Aunque hagas todo lo posible por impedir que alguien sin permiso ejecute esta función, sigue siendo recomendable incluir comprobaciones para esta situación.
WordPress incluye un sólido sistema de roles y capacidades de usuario, que te permite utilizar los roles y capacidades de usuario predeterminados o crear unos personalizados.
En este caso, podría bastar con permitir únicamente a los usuarios que tengan la capacidad de gestionar las opciones del sitio, una capacidad estándar incluida en el rol de administrador.
function wp_learn_delete_form_submission() {
if ( ! current_user_can( 'manage_options' ) ) {
return wp_send_json( array( 'result' => 'Error de autenticación' ) );
}
// resto del código de la función
}
La documentación para desarrolladores de WordPress tiene una sección detallada sobre roles y capacidades de usuario, que incluye una lista de las capacidades predeterminadas y explica cómo crear otras personalizadas.
Para seguir leyendo
Para prepararte para desarrollar teniendo en cuenta la seguridad, asegúrate de leer la entrada sobre Seguridad de la documentación para desarrolladores de WordPress, ya que incluye todos los ejemplos de esta lección, además de información adicional sobre las prácticas recomendadas de seguridad, vulnerabilidades comunes y más ejemplos de código.