ElementInclude en Python: XInclude seguro

Publicado el: 23/08/2026
Tempo de leitura: 5 minutos
Detailed shot of a Jungle Carpet Python (Morelia spilota cheynei) in its natural habitat.

El módulo xml.etree.ElementInclude añade soporte limitado para XInclude en árboles creados con xml.etree.ElementTree. XInclude permite que un documento XML declare que otro archivo XML o recurso de texto debe insertarse en un punto específico.

Puede simplificar configuraciones, documentación, catálogos y documentos grandes divididos en secciones reutilizables. También crea riesgos: lectura de archivos fuera de un directorio permitido, solicitudes remotas, ciclos, expansión de contenido y dependencias ocultas. Los loaders personalizados, rutas autorizadas y límites estrictos son esenciales.

Qué hace XInclude

XInclude usa el namespace http://www.w3.org/2001/XInclude. Un elemento xi:include incluye href y puede usar parse="xml" o parse="text".

<documento xmlns:xi="http://www.w3.org/2001/XInclude">
  <titulo>Informe</titulo>
  <xi:include href="secciones/resumen.xml" parse="xml"/>
</documento>

Durante la expansión, el elemento se sustituye por la raíz del XML referenciado. En modo texto, se inserta contenido de caracteres.

Primer ejemplo

from xml.etree import ElementTree, ElementInclude

arbol = ElementTree.parse("documento.xml")
raiz = arbol.getroot()

ElementInclude.include(raiz)

arbol.write(
    "resultado.xml",
    encoding="utf-8",
    xml_declaration=True,
)

El loader por defecto trata href como nombre de archivo. Es cómodo para proyectos locales confiables, pero no es seguro cuando el XML o las rutas pueden ser controlados por usuarios.

Integración con ElementTree

ElementInclude trabaja con objetos Element o ElementTree. Antes de usar XInclude, conviene dominar parsing, namespaces, búsquedas y serialización. Consulta ElementTree en Python.

La expansión modifica el árbol original. Si necesitas conservar la versión previa, vuelve a parsear la fuente o crea una copia controlada.

Modo XML y modo texto

Con parse="xml", el loader debe devolver un Element. Con parse="text", devuelve un string. El encoding de texto por defecto es UTF-8, salvo indicación contraria.

<documento xmlns:xi="http://www.w3.org/2001/XInclude">
  <pie>
    <xi:include href="anio.txt" parse="text" encoding="utf-8"/>
  </pie>
</documento>

El texto incluido no se convierte automáticamente en markup XML. Permanece como contenido y será escapado al serializar.

Referencias relativas con base_url

El argumento base_url ayuda a resolver referencias relativas respecto al documento principal.

from pathlib import Path
from xml.etree import ElementTree, ElementInclude

archivo = Path("configs/principal.xml").resolve()
arbol = ElementTree.parse(archivo)

ElementInclude.include(
    arbol.getroot(),
    base_url=archivo.as_uri(),
    max_depth=4,
)

base_url no es una barrera de seguridad. No bloquea ../, rutas absolutas ni URLs remotas si el loader las acepta.

Profundidad máxima

include() usa max_depth=6 por defecto. El límite reduce recursión y explosión de contenido. Pasar None desactiva el límite y rara vez es apropiado.

Elige un valor menor cuando la estructura sea conocida. Una configuración con uno o dos niveles debería usar una política equivalente.

Ciclos de inclusión

Existe un ciclo cuando a.xml incluye b.xml y b.xml vuelve a incluir a.xml. La profundidad máxima termina deteniendo el proceso, pero un loader debería detectar rutas repetidas y producir un error claro.

Loader local con directorio permitido

from pathlib import Path
from xml.etree import ElementTree

RAIZ = Path("contenidos").resolve()

class LoaderLocal:
    def __init__(self):
        self.visitados = set()

    def __call__(self, href, parse, encoding=None):
        ruta = (RAIZ / href).resolve()

        if RAIZ not in ruta.parents and ruta != RAIZ:
            raise ValueError("Inclusión fuera del directorio permitido")

        if ruta in self.visitados:
            raise ValueError("Ciclo XInclude detectado")
        self.visitados.add(ruta)

        if ruta.stat().st_size > 2 * 1024 * 1024:
            raise ValueError("Archivo incluido demasiado grande")

        if parse == "xml":
            return ElementTree.parse(ruta).getroot()
        if parse == "text":
            return ruta.read_text(encoding=encoding or "utf-8")

        raise ValueError(f"Modo no soportado: {parse}")

Pasa el loader de forma explícita:

loader = LoaderLocal()
ElementInclude.include(
    arbol.getroot(),
    loader=loader,
    max_depth=4,
)

El ejemplo bloquea path traversal, ciclos simples y archivos grandes. En producción también limita bytes totales, número de archivos, profundidad y tiempo.

Enlaces simbólicos

Path.resolve() revela enlaces simbólicos y componentes .. antes de validar el directorio. Todavía puede existir una carrera entre comprobación y apertura. En entornos hostiles usa directorios controlados, permisos restrictivos y APIs seguras del sistema.

Evitar inclusiones remotas

Un loader puede buscar URLs, pero eso introduce SSRF, redirects, DNS rebinding, respuestas enormes y problemas de disponibilidad. Para XML de terceros, es preferible prohibir HTTP y HTTPS.

Si la red es imprescindible, usa allowlist de hosts y protocolos, valida IPs resueltas, limita redirects, timeout y bytes. El guía de urllib.request en Python explica descargas limitadas.

Archivos temporales y caché

Si los recursos remotos se descargan antes de la expansión, usa almacenamiento temporal seguro y caché con integridad. Consulta tempfile en Python y hashlib en Python.

No uses el href no confiable directamente como nombre local. Genera un identificador interno.

Limitaciones

Python ofrece soporte limitado y no implementa XPointer completo. No supongas compatibilidad con todas las herramientas XML avanzadas. Prueba los documentos reales de la integración.

Para schemas, XPath completo, validación avanzada o resolución compleja, puede ser mejor una biblioteca especializada.

Validar después de expandir

Un fragmento puede introducir elementos, namespaces o valores inesperados. Valida el árbol final, no solo cada archivo aislado.

items = arbol.getroot().findall(".//item")
if len(items) > 10_000:
    raise ValueError("Demasiados items")

for item in items:
    if not item.get("id"):
        raise ValueError("Item sin ID")

Salida atómica

Al guardar el documento expandido, escribe en un archivo temporal del mismo directorio y reemplaza el destino solo después de una serialización exitosa. Así evitas XML parcial.

Logs y diagnóstico

Registra documento principal, ruta normalizada, tamaño, profundidad y resultado. No registres contenido sensible. Los errores deben identificar la inclusión fallida sin revelar rutas internas innecesarias.

Pruebas recomendadas

Prueba inclusiones XML y texto, encoding inválido, archivo ausente, rutas absolutas, ../, enlaces simbólicos, ciclos directos e indirectos, profundidad excedida, archivos grandes, modo inválido, namespaces y validación del árbol final.

Errores comunes

Los fallos frecuentes son usar el loader por defecto con XML externo, desactivar max_depth, permitir URLs arbitrarias, no detectar ciclos, validar antes de resolver la ruta, confiar en href como nombre local y no validar el resultado.

Conclusión

xml.etree.ElementInclude permite dividir XML en archivos reutilizables y expandir XInclude dentro de árboles ElementTree. La función es sencilla, pero la resolución de recursos debe tratarse como operación sensible.

Usa un loader personalizado, un directorio permitido, límites de profundidad y tamaño y validación final. Consulta la documentación oficial de XInclude en ElementTree y la recomendación XInclude del W3C.

Compartilhe:

Facebook
WhatsApp
Twitter
LinkedIn

Contenido del artículo

    Artículos relacionados

    Desarrollador configurando un servidor HTTPS y certificado TLS con Python
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    HTTPSServer: crea un servidor HTTPS local en Python

    Aprende HTTPSServer en Python para crear servicios HTTPS locales, configurar certificados, usar hilos y comprender sus límites.

    Ler mais

    Tempo de leitura: 4 minutos
    08/10/2026
    Archivos protegidos que representan extracción segura de TAR con Python
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    tarfile extraction_filter: extrae TAR con seguridad

    Aprende tarfile extraction_filter en Python para extraer archivos TAR con validación, seguridad y control de rutas.

    Ler mais

    Tempo de leitura: 6 minutos
    08/10/2026
    Código Python mostrando avisos controlados con catch_warnings
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    catch_warnings: captura warnings en pruebas Python

    Aprende catch_warnings en Python para capturar, probar y controlar avisos con filtros específicos y alcance seguro.

    Ler mais

    Tempo de leitura: 5 minutos
    07/10/2026
    Código Python que representa referencias persistentes de pickle
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    pickle persistent_id: serializa referencias externas

    Aprende pickle persistent_id en Python para referencias externas estables, validación, seguridad, rendimiento y compatibilidad.

    Ler mais

    Tempo de leitura: 5 minutos
    07/10/2026
    Código binario que representa buffers y vistas de memoria en Python
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    memoryview.count: cuenta valores sin copiar buffers

    Aprende memoryview.count en Python para contar bytes y valores en buffers sin copias, con formatos, límites y buenas prácticas.

    Ler mais

    Tempo de leitura: 6 minutos
    06/10/2026
    Código Python y anotaciones de tipos
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    annotationlib: evita imports circulares en anotaciones

    Aprende annotationlib en Python para inspeccionar anotaciones diferidas, evitar imports circulares y crear herramientas seguras.

    Ler mais

    Tempo de leitura: 6 minutos
    06/10/2026