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

    A person reads 'Python for Unix and Linux System Administration' indoors.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    xmlreader en Python: controla parsers SAX

    Aprende xmlreader en Python para configurar parsers SAX, InputSource, parsing incremental, atributos, locators y seguridad XML.

    Ler mais

    Tempo de leitura: 5 minutos
    23/08/2026
    High-angle view of woman coding on a laptop, with a Python book nearby. Ideal for programming and tech content.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    saxutils en Python: utilidades para XML

    Aprende saxutils en Python para escapar XML, preparar atributos, generar documentos, crear filtros SAX y evitar errores de contexto.

    Ler mais

    Tempo de leitura: 5 minutos
    23/08/2026
    High-angle view of woman coding on a laptop, with a Python book nearby. Ideal for programming and tech content.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    pulldom en Python: DOM parcial para XML

    Aprende pulldom en Python para procesar XML por eventos, expandir subárboles selectivos y reducir memoria con límites seguros.

    Ler mais

    Tempo de leitura: 5 minutos
    23/08/2026
    Close-up of a laptop screen with code and a coffee mug, perfect for tech abstract themes.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    xml.sax en Python: procesa XML por eventos

    Aprende xml.sax en Python para procesar XML por eventos con poca memoria, namespaces, handlers, límites y entidades seguras.

    Ler mais

    Tempo de leitura: 5 minutos
    22/08/2026
    Close-up view of a computer screen displaying code in a software development environment.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    minidom en Python: manipula XML con DOM

    Aprende xml.dom.minidom en Python para leer, navegar, crear y serializar XML con nodos DOM, namespaces, memoria y seguridad.

    Ler mais

    Tempo de leitura: 5 minutos
    22/08/2026
    Vibrant green snake coiled on a tree branch amidst lush jungle foliage.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    ElementTree: lee y modifica XML en Python

    Aprende ElementTree en Python para leer, buscar, modificar y generar XML con namespaces, parsing incremental, límites y seguridad.

    Ler mais

    Tempo de leitura: 5 minutos
    22/08/2026