From a1d0e299ef35a0ab2aaca59d40ae53e425d903cb Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Wed, 22 Jul 2026 23:34:43 -0700 Subject: [PATCH 1/2] docs: clarify stdlib xml vs defusedxml usage at every import site All XML parsing of server responses already uses defusedxml (safe against XXE/entity expansion). Stdlib xml is retained only for building outbound request bodies (no defusedxml equivalent) and for the ParseError exception type (which defusedxml raises unchanged). Added inline comments at each import site so future contributors don't replace these with defusedxml unnecessarily, and updated pyproject.toml dependency comment to explain the split. Co-Authored-By: Claude Sonnet 4.6 --- pyproject.toml | 2 +- tableauserverclient/models/collection_item.py | 2 +- tableauserverclient/models/data_freshness_policy_item.py | 2 +- tableauserverclient/models/datasource_item.py | 2 +- tableauserverclient/models/extract_item.py | 2 +- tableauserverclient/models/flow_item.py | 2 +- tableauserverclient/models/groupset_item.py | 2 +- tableauserverclient/models/location_item.py | 2 +- tableauserverclient/models/metric_item.py | 2 +- tableauserverclient/models/permissions_item.py | 2 +- tableauserverclient/models/project_item.py | 2 +- tableauserverclient/models/schedule_item.py | 2 +- tableauserverclient/models/server_info_item.py | 2 +- tableauserverclient/models/site_item.py | 2 +- tableauserverclient/models/tag_item.py | 2 +- tableauserverclient/models/user_item.py | 2 +- tableauserverclient/models/virtual_connection_item.py | 2 +- tableauserverclient/models/webhook_item.py | 2 +- tableauserverclient/models/workbook_item.py | 2 +- tableauserverclient/server/endpoint/endpoint.py | 2 +- tableauserverclient/server/request_factory.py | 2 +- 21 files changed, 21 insertions(+), 21 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e5a330c39..5e006766a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ license-files = ["LICENSE"] readme = "README.md" dependencies = [ - 'defusedxml>=0.7.1', # latest as at 7/31/23 + 'defusedxml>=0.7.1', # latest as at 7/31/23; use for all XML parsing — stdlib xml is unsafe against XXE. XML building (Element/SubElement/tostring) still uses stdlib as defusedxml has no equivalents. 'packaging>=23.1', # latest as at 7/31/23 'requests>=2.32', # latest as at 7/31/23 'urllib3>=2.6.0,<3', diff --git a/tableauserverclient/models/collection_item.py b/tableauserverclient/models/collection_item.py index 786e9492b..e2c075413 100644 --- a/tableauserverclient/models/collection_item.py +++ b/tableauserverclient/models/collection_item.py @@ -1,5 +1,5 @@ from datetime import datetime -from xml.etree.ElementTree import Element +from xml.etree.ElementTree import Element # building XML request bodies only; use defusedxml for parsing from defusedxml.ElementTree import fromstring from typing_extensions import Self diff --git a/tableauserverclient/models/data_freshness_policy_item.py b/tableauserverclient/models/data_freshness_policy_item.py index 439d53caa..ec70be1cc 100644 --- a/tableauserverclient/models/data_freshness_policy_item.py +++ b/tableauserverclient/models/data_freshness_policy_item.py @@ -1,4 +1,4 @@ -import xml.etree.ElementTree as ET +import xml.etree.ElementTree as ET # building XML request bodies only; use defusedxml for parsing from tableauserverclient.models.property_decorators import property_is_enum, property_not_nullable from .interval_item import IntervalItem diff --git a/tableauserverclient/models/datasource_item.py b/tableauserverclient/models/datasource_item.py index 6ed1fbdd4..614ab8762 100644 --- a/tableauserverclient/models/datasource_item.py +++ b/tableauserverclient/models/datasource_item.py @@ -1,6 +1,6 @@ import copy import datetime -import xml.etree.ElementTree as ET +import xml.etree.ElementTree as ET # building XML request bodies only; use defusedxml for parsing from defusedxml.ElementTree import fromstring diff --git a/tableauserverclient/models/extract_item.py b/tableauserverclient/models/extract_item.py index d8a149ec4..7a97168c1 100644 --- a/tableauserverclient/models/extract_item.py +++ b/tableauserverclient/models/extract_item.py @@ -1,5 +1,5 @@ from defusedxml.ElementTree import fromstring -import xml.etree.ElementTree as ET +import xml.etree.ElementTree as ET # building XML request bodies only; use defusedxml for parsing class ExtractItem: diff --git a/tableauserverclient/models/flow_item.py b/tableauserverclient/models/flow_item.py index ede80915c..d41a679e5 100644 --- a/tableauserverclient/models/flow_item.py +++ b/tableauserverclient/models/flow_item.py @@ -1,7 +1,7 @@ from collections.abc import Iterable import copy import datetime -import xml.etree.ElementTree as ET +import xml.etree.ElementTree as ET # building XML request bodies only; use defusedxml for parsing from defusedxml.ElementTree import fromstring diff --git a/tableauserverclient/models/groupset_item.py b/tableauserverclient/models/groupset_item.py index da1e88f8d..86ae93e62 100644 --- a/tableauserverclient/models/groupset_item.py +++ b/tableauserverclient/models/groupset_item.py @@ -1,4 +1,4 @@ -import xml.etree.ElementTree as ET +import xml.etree.ElementTree as ET # building XML request bodies only; use defusedxml for parsing from defusedxml.ElementTree import fromstring from typing_extensions import Self diff --git a/tableauserverclient/models/location_item.py b/tableauserverclient/models/location_item.py index e1ecd0039..bcea5a172 100644 --- a/tableauserverclient/models/location_item.py +++ b/tableauserverclient/models/location_item.py @@ -1,4 +1,4 @@ -import xml.etree.ElementTree as ET +import xml.etree.ElementTree as ET # building XML request bodies only; use defusedxml for parsing class LocationItem: diff --git a/tableauserverclient/models/metric_item.py b/tableauserverclient/models/metric_item.py index 287ebc8a1..cd93c1c72 100644 --- a/tableauserverclient/models/metric_item.py +++ b/tableauserverclient/models/metric_item.py @@ -1,4 +1,4 @@ -import xml.etree.ElementTree as ET +import xml.etree.ElementTree as ET # building XML request bodies only; use defusedxml for parsing from datetime import datetime from tableauserverclient.datetime_helpers import parse_datetime diff --git a/tableauserverclient/models/permissions_item.py b/tableauserverclient/models/permissions_item.py index 3603c37c4..73b7e1e17 100644 --- a/tableauserverclient/models/permissions_item.py +++ b/tableauserverclient/models/permissions_item.py @@ -1,4 +1,4 @@ -import xml.etree.ElementTree as ET +import xml.etree.ElementTree as ET # building XML request bodies only; use defusedxml for parsing from defusedxml.ElementTree import fromstring diff --git a/tableauserverclient/models/project_item.py b/tableauserverclient/models/project_item.py index 70adc8a76..b8b72f3aa 100644 --- a/tableauserverclient/models/project_item.py +++ b/tableauserverclient/models/project_item.py @@ -1,4 +1,4 @@ -import xml.etree.ElementTree as ET +import xml.etree.ElementTree as ET # building XML request bodies only; use defusedxml for parsing from typing import overload from defusedxml.ElementTree import fromstring diff --git a/tableauserverclient/models/schedule_item.py b/tableauserverclient/models/schedule_item.py index d15a15345..8d5fd2515 100644 --- a/tableauserverclient/models/schedule_item.py +++ b/tableauserverclient/models/schedule_item.py @@ -1,4 +1,4 @@ -import xml.etree.ElementTree as ET +import xml.etree.ElementTree as ET # building XML request bodies only; use defusedxml for parsing from datetime import datetime from typing import TYPE_CHECKING diff --git a/tableauserverclient/models/server_info_item.py b/tableauserverclient/models/server_info_item.py index b13f26740..eb744c933 100644 --- a/tableauserverclient/models/server_info_item.py +++ b/tableauserverclient/models/server_info_item.py @@ -1,6 +1,6 @@ import logging import warnings -import xml +import xml # for xml.etree.ElementTree.ParseError exception type only; defusedxml raises this same class from defusedxml.ElementTree import fromstring from tableauserverclient.helpers.logging import logger diff --git a/tableauserverclient/models/site_item.py b/tableauserverclient/models/site_item.py index 382ca63db..a1a6a106d 100644 --- a/tableauserverclient/models/site_item.py +++ b/tableauserverclient/models/site_item.py @@ -1,5 +1,5 @@ import warnings -import xml.etree.ElementTree as ET +import xml.etree.ElementTree as ET # building XML request bodies only; use defusedxml for parsing from defusedxml.ElementTree import fromstring diff --git a/tableauserverclient/models/tag_item.py b/tableauserverclient/models/tag_item.py index cde755f05..162b9c420 100644 --- a/tableauserverclient/models/tag_item.py +++ b/tableauserverclient/models/tag_item.py @@ -1,4 +1,4 @@ -import xml.etree.ElementTree as ET +import xml.etree.ElementTree as ET # building XML request bodies only; use defusedxml for parsing from defusedxml.ElementTree import fromstring diff --git a/tableauserverclient/models/user_item.py b/tableauserverclient/models/user_item.py index 0ba1e8eb2..60e07bddb 100644 --- a/tableauserverclient/models/user_item.py +++ b/tableauserverclient/models/user_item.py @@ -1,5 +1,5 @@ import io -import xml.etree.ElementTree as ET +import xml.etree.ElementTree as ET # building XML request bodies only; use defusedxml for parsing from datetime import datetime from enum import IntEnum from typing import TYPE_CHECKING diff --git a/tableauserverclient/models/virtual_connection_item.py b/tableauserverclient/models/virtual_connection_item.py index 7ec2efe36..1406ac28f 100644 --- a/tableauserverclient/models/virtual_connection_item.py +++ b/tableauserverclient/models/virtual_connection_item.py @@ -2,7 +2,7 @@ import json from typing import Callable from collections.abc import Iterable -from xml.etree.ElementTree import Element +from xml.etree.ElementTree import Element # building XML request bodies only; use defusedxml for parsing from defusedxml.ElementTree import fromstring diff --git a/tableauserverclient/models/webhook_item.py b/tableauserverclient/models/webhook_item.py index a8dfe3ef0..a6e0a0cf4 100644 --- a/tableauserverclient/models/webhook_item.py +++ b/tableauserverclient/models/webhook_item.py @@ -1,5 +1,5 @@ import re -import xml.etree.ElementTree as ET +import xml.etree.ElementTree as ET # building XML request bodies only; use defusedxml for parsing from defusedxml.ElementTree import fromstring diff --git a/tableauserverclient/models/workbook_item.py b/tableauserverclient/models/workbook_item.py index 352923389..cfa5a2897 100644 --- a/tableauserverclient/models/workbook_item.py +++ b/tableauserverclient/models/workbook_item.py @@ -1,7 +1,7 @@ import copy import datetime import uuid -import xml.etree.ElementTree as ET +import xml.etree.ElementTree as ET # building XML request bodies only; use defusedxml for parsing from typing import Callable, overload from defusedxml.ElementTree import fromstring diff --git a/tableauserverclient/server/endpoint/endpoint.py b/tableauserverclient/server/endpoint/endpoint.py index 31a0806dc..4b1e2126a 100644 --- a/tableauserverclient/server/endpoint/endpoint.py +++ b/tableauserverclient/server/endpoint/endpoint.py @@ -8,7 +8,7 @@ import abc from packaging.version import Version from functools import wraps -from xml.etree.ElementTree import ParseError +from xml.etree.ElementTree import ParseError # exception type only; defusedxml raises this same class from typing import ( Any, Callable, diff --git a/tableauserverclient/server/request_factory.py b/tableauserverclient/server/request_factory.py index fc4694c01..44e234647 100644 --- a/tableauserverclient/server/request_factory.py +++ b/tableauserverclient/server/request_factory.py @@ -1,4 +1,4 @@ -import xml.etree.ElementTree as ET +import xml.etree.ElementTree as ET # building XML request bodies only; use defusedxml for parsing from typing import Any, Callable, TypeVar, TYPE_CHECKING from collections.abc import Iterable From 0c12c742497ecd7b93212846417480e56831d507 Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Tue, 18 Aug 2026 02:38:15 -0700 Subject: [PATCH 2/2] fix: parse metric server responses with defusedxml, not stdlib ET metric_item.py's from_response() was parsing untrusted server response bytes with xml.etree.ElementTree.fromstring, which is exactly the scenario defusedxml exists to defend against. Switch to defusedxml.ElementTree.fromstring so metric responses go through the same hardened parser as every other from_response path in this package. Also correct import-site comments to reflect actual usage: - location_item, data_freshness_policy_item and 14 other models use stdlib ET only for type annotations (ET.Element hints, isinstance narrowing). Comment now reads "type annotation only; parsing uses defusedxml" instead of the inaccurate "building XML request bodies only". - server_info_item.py used a bare `import xml` and reached into xml.etree.ElementTree.ParseError, which only worked because defusedxml happens to import xml.etree transitively. Replace with an explicit `from xml.etree.ElementTree import ParseError` and match the except clause. defusedxml.ElementTree.fromstring raises this same class for malformed input. Co-Authored-By: Claude Opus 4.7 (1M context) --- tableauserverclient/models/collection_item.py | 2 +- tableauserverclient/models/data_freshness_policy_item.py | 2 +- tableauserverclient/models/datasource_item.py | 2 +- tableauserverclient/models/extract_item.py | 2 +- tableauserverclient/models/flow_item.py | 2 +- tableauserverclient/models/groupset_item.py | 2 +- tableauserverclient/models/location_item.py | 2 +- tableauserverclient/models/metric_item.py | 4 ++-- tableauserverclient/models/permissions_item.py | 2 +- tableauserverclient/models/project_item.py | 2 +- tableauserverclient/models/schedule_item.py | 2 +- tableauserverclient/models/server_info_item.py | 4 ++-- tableauserverclient/models/site_item.py | 2 +- tableauserverclient/models/tag_item.py | 2 +- tableauserverclient/models/user_item.py | 2 +- tableauserverclient/models/virtual_connection_item.py | 2 +- tableauserverclient/models/webhook_item.py | 2 +- tableauserverclient/models/workbook_item.py | 2 +- 18 files changed, 20 insertions(+), 20 deletions(-) diff --git a/tableauserverclient/models/collection_item.py b/tableauserverclient/models/collection_item.py index e2c075413..e696c615d 100644 --- a/tableauserverclient/models/collection_item.py +++ b/tableauserverclient/models/collection_item.py @@ -1,5 +1,5 @@ from datetime import datetime -from xml.etree.ElementTree import Element # building XML request bodies only; use defusedxml for parsing +from xml.etree.ElementTree import Element # type annotation only; parsing uses defusedxml from defusedxml.ElementTree import fromstring from typing_extensions import Self diff --git a/tableauserverclient/models/data_freshness_policy_item.py b/tableauserverclient/models/data_freshness_policy_item.py index ec70be1cc..80a53018f 100644 --- a/tableauserverclient/models/data_freshness_policy_item.py +++ b/tableauserverclient/models/data_freshness_policy_item.py @@ -1,4 +1,4 @@ -import xml.etree.ElementTree as ET # building XML request bodies only; use defusedxml for parsing +import xml.etree.ElementTree as ET # type annotation only; parsing uses defusedxml from tableauserverclient.models.property_decorators import property_is_enum, property_not_nullable from .interval_item import IntervalItem diff --git a/tableauserverclient/models/datasource_item.py b/tableauserverclient/models/datasource_item.py index 614ab8762..2a106eb06 100644 --- a/tableauserverclient/models/datasource_item.py +++ b/tableauserverclient/models/datasource_item.py @@ -1,6 +1,6 @@ import copy import datetime -import xml.etree.ElementTree as ET # building XML request bodies only; use defusedxml for parsing +import xml.etree.ElementTree as ET # type annotation only; parsing uses defusedxml from defusedxml.ElementTree import fromstring diff --git a/tableauserverclient/models/extract_item.py b/tableauserverclient/models/extract_item.py index 7a97168c1..2d157a87a 100644 --- a/tableauserverclient/models/extract_item.py +++ b/tableauserverclient/models/extract_item.py @@ -1,5 +1,5 @@ from defusedxml.ElementTree import fromstring -import xml.etree.ElementTree as ET # building XML request bodies only; use defusedxml for parsing +import xml.etree.ElementTree as ET # type annotation only; parsing uses defusedxml class ExtractItem: diff --git a/tableauserverclient/models/flow_item.py b/tableauserverclient/models/flow_item.py index d41a679e5..3382218d5 100644 --- a/tableauserverclient/models/flow_item.py +++ b/tableauserverclient/models/flow_item.py @@ -1,7 +1,7 @@ from collections.abc import Iterable import copy import datetime -import xml.etree.ElementTree as ET # building XML request bodies only; use defusedxml for parsing +import xml.etree.ElementTree as ET # type annotation only; parsing uses defusedxml from defusedxml.ElementTree import fromstring diff --git a/tableauserverclient/models/groupset_item.py b/tableauserverclient/models/groupset_item.py index 86ae93e62..c92a8a800 100644 --- a/tableauserverclient/models/groupset_item.py +++ b/tableauserverclient/models/groupset_item.py @@ -1,4 +1,4 @@ -import xml.etree.ElementTree as ET # building XML request bodies only; use defusedxml for parsing +import xml.etree.ElementTree as ET # type annotation only; parsing uses defusedxml from defusedxml.ElementTree import fromstring from typing_extensions import Self diff --git a/tableauserverclient/models/location_item.py b/tableauserverclient/models/location_item.py index bcea5a172..a52cea33f 100644 --- a/tableauserverclient/models/location_item.py +++ b/tableauserverclient/models/location_item.py @@ -1,4 +1,4 @@ -import xml.etree.ElementTree as ET # building XML request bodies only; use defusedxml for parsing +import xml.etree.ElementTree as ET # type annotation only; parsing uses defusedxml class LocationItem: diff --git a/tableauserverclient/models/metric_item.py b/tableauserverclient/models/metric_item.py index cd93c1c72..fdaed2f5e 100644 --- a/tableauserverclient/models/metric_item.py +++ b/tableauserverclient/models/metric_item.py @@ -1,4 +1,4 @@ -import xml.etree.ElementTree as ET # building XML request bodies only; use defusedxml for parsing +from defusedxml.ElementTree import fromstring # parsing server responses; defusedxml protects against XML attacks from datetime import datetime from tableauserverclient.datetime_helpers import parse_datetime @@ -127,7 +127,7 @@ def from_response( ns, ) -> list["MetricItem"]: all_metric_items = list() - parsed_response = ET.fromstring(resp) + parsed_response = fromstring(resp) all_metric_xml = parsed_response.findall(".//t:metric", namespaces=ns) for metric_xml in all_metric_xml: all_metric_items.append(cls.from_xml(metric_xml, ns)) diff --git a/tableauserverclient/models/permissions_item.py b/tableauserverclient/models/permissions_item.py index 73b7e1e17..cf24c6320 100644 --- a/tableauserverclient/models/permissions_item.py +++ b/tableauserverclient/models/permissions_item.py @@ -1,4 +1,4 @@ -import xml.etree.ElementTree as ET # building XML request bodies only; use defusedxml for parsing +import xml.etree.ElementTree as ET # type annotation only; parsing uses defusedxml from defusedxml.ElementTree import fromstring diff --git a/tableauserverclient/models/project_item.py b/tableauserverclient/models/project_item.py index b8b72f3aa..e7103bc58 100644 --- a/tableauserverclient/models/project_item.py +++ b/tableauserverclient/models/project_item.py @@ -1,4 +1,4 @@ -import xml.etree.ElementTree as ET # building XML request bodies only; use defusedxml for parsing +import xml.etree.ElementTree as ET # type annotation only; parsing uses defusedxml from typing import overload from defusedxml.ElementTree import fromstring diff --git a/tableauserverclient/models/schedule_item.py b/tableauserverclient/models/schedule_item.py index 8d5fd2515..1ef2fa4ac 100644 --- a/tableauserverclient/models/schedule_item.py +++ b/tableauserverclient/models/schedule_item.py @@ -1,4 +1,4 @@ -import xml.etree.ElementTree as ET # building XML request bodies only; use defusedxml for parsing +import xml.etree.ElementTree as ET # type annotation only; parsing uses defusedxml from datetime import datetime from typing import TYPE_CHECKING diff --git a/tableauserverclient/models/server_info_item.py b/tableauserverclient/models/server_info_item.py index eb744c933..86d8dd12b 100644 --- a/tableauserverclient/models/server_info_item.py +++ b/tableauserverclient/models/server_info_item.py @@ -1,6 +1,6 @@ import logging import warnings -import xml # for xml.etree.ElementTree.ParseError exception type only; defusedxml raises this same class +from xml.etree.ElementTree import ParseError # exception class only; defusedxml raises this same class from defusedxml.ElementTree import fromstring from tableauserverclient.helpers.logging import logger @@ -61,7 +61,7 @@ def rest_api_version(self): def from_response(cls, resp, ns): try: parsed_response = fromstring(resp) - except xml.etree.ElementTree.ParseError as error: + except ParseError as error: logger.exception(f"Unexpected response for ServerInfo: {resp}") return cls("Unknown", "Unknown", "Unknown") except Exception as error: diff --git a/tableauserverclient/models/site_item.py b/tableauserverclient/models/site_item.py index a1a6a106d..86c545afb 100644 --- a/tableauserverclient/models/site_item.py +++ b/tableauserverclient/models/site_item.py @@ -1,5 +1,5 @@ import warnings -import xml.etree.ElementTree as ET # building XML request bodies only; use defusedxml for parsing +import xml.etree.ElementTree as ET # type annotation only; parsing uses defusedxml from defusedxml.ElementTree import fromstring diff --git a/tableauserverclient/models/tag_item.py b/tableauserverclient/models/tag_item.py index 162b9c420..e7cac335f 100644 --- a/tableauserverclient/models/tag_item.py +++ b/tableauserverclient/models/tag_item.py @@ -1,4 +1,4 @@ -import xml.etree.ElementTree as ET # building XML request bodies only; use defusedxml for parsing +import xml.etree.ElementTree as ET # type annotation only; parsing uses defusedxml from defusedxml.ElementTree import fromstring diff --git a/tableauserverclient/models/user_item.py b/tableauserverclient/models/user_item.py index 60e07bddb..55bb56b2d 100644 --- a/tableauserverclient/models/user_item.py +++ b/tableauserverclient/models/user_item.py @@ -1,5 +1,5 @@ import io -import xml.etree.ElementTree as ET # building XML request bodies only; use defusedxml for parsing +import xml.etree.ElementTree as ET # type annotation only; parsing uses defusedxml from datetime import datetime from enum import IntEnum from typing import TYPE_CHECKING diff --git a/tableauserverclient/models/virtual_connection_item.py b/tableauserverclient/models/virtual_connection_item.py index 1406ac28f..abbb92396 100644 --- a/tableauserverclient/models/virtual_connection_item.py +++ b/tableauserverclient/models/virtual_connection_item.py @@ -2,7 +2,7 @@ import json from typing import Callable from collections.abc import Iterable -from xml.etree.ElementTree import Element # building XML request bodies only; use defusedxml for parsing +from xml.etree.ElementTree import Element # type annotation only; parsing uses defusedxml from defusedxml.ElementTree import fromstring diff --git a/tableauserverclient/models/webhook_item.py b/tableauserverclient/models/webhook_item.py index a6e0a0cf4..110218fcf 100644 --- a/tableauserverclient/models/webhook_item.py +++ b/tableauserverclient/models/webhook_item.py @@ -1,5 +1,5 @@ import re -import xml.etree.ElementTree as ET # building XML request bodies only; use defusedxml for parsing +import xml.etree.ElementTree as ET # type annotation only; parsing uses defusedxml from defusedxml.ElementTree import fromstring diff --git a/tableauserverclient/models/workbook_item.py b/tableauserverclient/models/workbook_item.py index cfa5a2897..fa3e94fee 100644 --- a/tableauserverclient/models/workbook_item.py +++ b/tableauserverclient/models/workbook_item.py @@ -1,7 +1,7 @@ import copy import datetime import uuid -import xml.etree.ElementTree as ET # building XML request bodies only; use defusedxml for parsing +import xml.etree.ElementTree as ET # type annotation only; parsing uses defusedxml from typing import Callable, overload from defusedxml.ElementTree import fromstring