Como permitir subir SVG de forma segura con sanitización en WordPress

Contents

Introducción

Los archivos SVG (Scalable Vector Graphics) ofrecen enormes ventajas: son vectoriales, escalables sin pérdida y suelen pesar poco. Sin embargo, un SVG no es solo una imagen: puede contener código XML/HTML, JavaScript embebido, referencias externas y atributos que ejecutan comportamientos en el navegador. Por eso, permitir la subida de SVG en WordPress sin medidas de seguridad puede abrir brechas importantes (XSS, fugas de información, ejecución remota).

Objetivo del tutorial

Explicar de forma detallada y práctica cómo permitir la subida de SVG en WordPress de forma segura, aplicando sanitización al momento de la subida (y proporcionando herramientas para sanitizar archivos ya subidos), usando una librería especializada recomendada y mostrando ejemplos listos para implementar como plugin.

Riesgos principales al aceptar SVG sin filtrar

  • Cross-Site Scripting (XSS): etiquetas ltscriptgt, eventos inline (onload, onclick), URIs javascript:.
  • Inyección de HTML: uso de ltforeignObjectgt para incrustar HTML con scripts.
  • Referencias externas: imágenes, fuentes o scripts que apunten a dominios externos (fugas de información o CSRF).
  • Animaciones o intencionalidades ocultas: SMIL/JS que ejecuten acciones no deseadas.
  • Mime-sniffing / ejecución como HTML: servidores mal configurados pueden servir SVG como HTML y permitir ejecución.

Estrategia recomendada

  1. Permitir la extensión/mime SVG en WordPress con filtros controlados.
  2. Restringir qué usuarios pueden subir SVG (por ejemplo, administradores o roles concretos).
  3. Sanitizar el contenido del SVG en el proceso de subida usando una librería probada (no un simple regex).
  4. Reescribir el archivo temporal con la versión sanitizada antes de que WordPress lo mueva a /uploads.
  5. Opcional: sanitizar al mostrar (doble verificación) o servir SVGes desde un endpoint que los vuelva a filtrar.
  6. Mantener la librería de sanitización actualizada y auditar periódicamente.

Qué librería usar

En PHP hay librerías específicas para sanitizar SVG. La más utilizada y madura es enshrined/svg-sanitize. Hace un parsing correcto del XML y elimina elementos/atributos peligrosos según configuración. Es preferible a soluciones caseras porque entiende la estructura del SVG.

Instalación (composer):

composer require enshrined/svg-sanitize

Implementación paso a paso: plugin ejemplo

El ejemplo que sigue muestra un plugin mínimo que:

  • Permite MIME types SVG
  • Corrige la comprobación interna de WordPress para la extensión
  • Sanitiza el contenido al subir
  • Restringe la subida a usuarios con la capacidad configurada

Coloca este archivo como plugin (por ejemplo: wp-content/plugins/svg-sanitizer/svg-sanitizer.php) y crea el directorio vendor con composer autoload que incluye enshrined/svg-sanitize.

lt?php
/
Plugin Name: SVG Upload Sanitizer
Description: Permite subir SVGs y los sanitiza con enshrined/svg-sanitize. Restrinja la subida por capacidad.
Version: 1.0
Author: Ejemplo
/

if ( ! defined( ABSPATH ) ) {
    exit
}

// Si usas Composer, cargamos autoload
if ( file_exists( __DIR__ . /vendor/autoload.php ) ) {
    require_once __DIR__ . /vendor/autoload.php
}

class WP_SVG_Upload_Sanitizer {

    // Capacidad mínima para permitir subir SVG. Cambia según necesidad.
    private capability = manage_options

    public function __construct() {
        add_filter( upload_mimes, array( this, allow_svg_mime ) )
        add_filter( wp_check_filetype_and_ext, array( this, fix_svg_filetype_check ), 10, 4 )
        add_filter( wp_handle_upload_prefilter, array( this, sanitize_svg_on_upload ) )
    }

    // Añade resoluciones MIME
    public function allow_svg_mime( mimes ) {
        mimes[svg]  = image/svg xml
        mimes[svgz] = image/svg xml
        return mimes
    }

    // Evitar que WP rechace el archivo por extensión/tipo
    public function fix_svg_filetype_check( data, file, filename, mimes ) {
        ext = pathinfo( filename, PATHINFO_EXTENSION )
        if ( strtolower( ext ) === svg  strtolower( ext ) === svgz ) {
            data[ext]  = svg
            data[type] = image/svg xml
        }
        return data
    }

    // Sanitize cuando se sube
    public function sanitize_svg_on_upload( file ) {
        if ( empty( file[name] ) ) {
            return file
        }

        ext = strtolower( pathinfo( file[name], PATHINFO_EXTENSION ) )

        if ( ext !== svg  ext !== svgz ) {
            return file // no es SVG
        }

        // Restricción por capacidad: solo permitir a determinados usuarios
        if ( ! current_user_can( this->capability ) ) {
            file[error] = No tienes permiso para subir archivos SVG.
            return file
        }

        tmp_name = file[tmp_name]

        if ( ! is_readable( tmp_name ) ) {
            file[error] = Error leyendo el archivo SVG.
            return file
        }

        contents = file_get_contents( tmp_name )
        if ( contents === false ) {
            file[error] = No se pudo leer el SVG.
            return file
        }

        // En caso de svgz (gzip) lo descomprimimos antes de sanitizar
        if ( ext === svgz ) {
            decoded = @gzdecode( contents )
            if ( decoded === false ) {
                file[error] = SVGZ inválido o comprimido incorrectamente.
                return file
            }
            contents = decoded
        }

        // Comprobación básica: contiene sanitize( contents )

            if ( clean === false ) {
                file[error] = El SVG no pudo ser sanitizado. Rechazado.
                return file
            }

            // Si era svgz, recomprimimos el contenido limpio en gzip para mantener extensión svgz
            if ( ext === svgz ) {
                encoded = gzencode( clean )
                if ( encoded === false ) {
                    file[error] = Error al recomprimir SVGZ.
                    return file
                }
                clean_to_write = encoded
            } else {
                clean_to_write = clean
            }

            // Sobrescribimos el temporal con la versión sanitizada
            if ( file_put_contents( tmp_name, clean_to_write ) === false ) {
                file[error] = No se pudo escribir la versión sanitizada del SVG.
                return file
            }

            // Forzamos el tipo correcto
            file[type] = image/svg xml

            return file

        } catch ( Exception e ) {
            file[error] = Excepción durante la sanitización:  . e->getMessage()
            return file
        }
    }
}

new WP_SVG_Upload_Sanitizer()

Notas sobre el plugin de ejemplo

  • Cambia la propiedad capability si quieres permitir la subida a otros roles (por ejemplo upload_files o una capacidad personalizada).
  • El plugin usa composer autoload asegúrate de que vendor/autoload.php esté presente o adapta la carga de la librería manualmente.
  • El ejemplo maneja svgz (gzip) descomprimiendo y recomprimiendo puedes eliminar esa parte si no necesitas soporte svgz.

Sanitizar SVGs ya existentes

Si ya tienes SVGs subidos y quieres sanitizarlos en masa o de uno en uno, puedes usar funciones administrativas que reescriban los archivos en uploads tras sanitizarlos.

// Sanitiza un attachment SVG por ID (usa desde un script administrativo)
function sanitize_attachment_svg_by_id( attachment_id ) {
    if ( ! function_exists( get_attached_file ) ) {
        return new WP_Error( missing_function, Función get_attached_file no disponible )
    }

    path = get_attached_file( attachment_id )
    if ( ! path  ! file_exists( path ) ) {
        return new WP_Error( no_file, Archivo no encontrado )
    }

    ext = strtolower( pathinfo( path, PATHINFO_EXTENSION ) )
    if ( ext !== svg  ext !== svgz ) {
        return new WP_Error( not_svg, No es un SVG. )
    }

    contents = file_get_contents( path )
    if ( contents === false ) {
        return new WP_Error( read_failed, No se pudo leer. )
    }

    if ( ext === svgz ) {
        decoded = @gzdecode( contents )
        if ( decoded === false ) {
            return new WP_Error( invalid_svgz, SVGZ inválido. )
        }
        contents = decoded
    }

    sanitizer = new enshrinedsvgSanitizerSanitizer()
    clean = sanitizer->sanitize( contents )

    if ( clean === false ) {
        return new WP_Error( sanitize_failed, No se pudo sanitizar. )
    }

    to_write = ( ext === svgz ) ? gzencode( clean ) : clean
    if ( file_put_contents( path, to_write ) === false ) {
        return new WP_Error( write_failed, Error al escribir el fichero. )
    }

    return true
}

Medidas complementarias de seguridad

  • Headers HTTP: asegúrate de servir SVG con Content-Type: image/svg xml y añade X-Content-Type-Options: nosniff para evitar sniffing.
  • Política CSP (Content-Security-Policy): establece un CSP restrictivo para bloquear la ejecución de scripts desde orígenes no permitidos.
  • Evitar inline SVG no sanitizado: si insertas SVG inline en HTML (por ejemplo, mediante editor), vuelve a sanitizar antes de echo en plantilla.
  • Backups: haz copias antes de reescribir archivos en masa.
  • Reglas de servidor: considera servir SVG desde un subdominio separado o configurar cabeceras y políticas de seguridad específicas para /wp-content/uploads/.
  • Auditoría y actualizaciones: mantén la librería de sanitización actualizada y revisa logs de carga para detectar intentos maliciosos.

Tests y validación

  • Prueba subir SVGs benignos (iconos, logos) y SVGs maliciosos (con ltscriptgt, onload, javascript: etc.) para comprobar rechazo/limpieza.
  • Inspecciona el archivo final en /uploads para confirmar que los elementos/atributos peligrosos se han eliminado.
  • Usa herramientas automáticas o linters SVG para verificar validez XML tras sanitizar.

Consideraciones finales

Permitir SVG en WordPress es posible y muy útil, pero exige un enfoque conservador: solo usuarios de confianza deberían poder subirlos, siempre hay que aplicar sanitización con una librería especializada y revisar cabeceras/servicio del servidor. El enfoque de sanitizar al subir minimiza el riesgo, y añadir una sanitización adicional al servir o al insertar inline ofrece una segunda línea de defensa.

Recursos



Acepto donaciones de BAT's mediante el navegador Brave 🙂



Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *