diff options
| author | Saravanan <saravanan.kadambathursubramaniyam@windriver.com> | 2025-10-13 17:22:44 +0530 |
|---|---|---|
| committer | Steve Sakoman <steve@sakoman.com> | 2025-10-24 06:23:39 -0700 |
| commit | 2ab1bedda9e081dbf01d4c3069d247efc6c3c55e (patch) | |
| tree | cfa19813920bfa49eb1059e23cbef19710625777 /meta/recipes-devtools/python/python3-xmltodict | |
| parent | a04f9ab3a5f5d813b581a4dfa19ac21dd6c5ac67 (diff) | |
| download | poky-2ab1bedda9e081dbf01d4c3069d247efc6c3c55e.tar.gz | |
python3-xmltodict: fix CVE-2025-9375
Reference:
https://nvd.nist.gov/vuln/detail/CVE-2025-9375
https://security-tracker.debian.org/tracker/CVE-2025-9375
https://git.launchpad.net/ubuntu/+source/python-xmltodict/commit/?id=e8110a20e00d80db31d5fc9f8f4577328385d6b6
Upstream-patch:
https://github.com/martinblech/xmltodict/commit/ecd456ab88d379514b116ef9293318b74e5ed3ee
https://github.com/martinblech/xmltodict/commit/f98c90f071228ed73df997807298e1df4f790c33
(From OE-Core rev: 30624cce634cade0b030aa71a03be754abbf3da9)
Signed-off-by: Saravanan <saravanan.kadambathursubramaniyam@windriver.com>
Signed-off-by: Steve Sakoman <steve@sakoman.com>
Diffstat (limited to 'meta/recipes-devtools/python/python3-xmltodict')
| -rw-r--r-- | meta/recipes-devtools/python/python3-xmltodict/CVE-2025-9375-1.patch | 111 | ||||
| -rw-r--r-- | meta/recipes-devtools/python/python3-xmltodict/CVE-2025-9375-2.patch | 176 |
2 files changed, 287 insertions, 0 deletions
diff --git a/meta/recipes-devtools/python/python3-xmltodict/CVE-2025-9375-1.patch b/meta/recipes-devtools/python/python3-xmltodict/CVE-2025-9375-1.patch new file mode 100644 index 0000000000..e8977dd2ea --- /dev/null +++ b/meta/recipes-devtools/python/python3-xmltodict/CVE-2025-9375-1.patch | |||
| @@ -0,0 +1,111 @@ | |||
| 1 | From ecd456ab88d379514b116ef9293318b74e5ed3ee Mon Sep 17 00:00:00 2001 | ||
| 2 | From: Martin Blech <78768+martinblech@users.noreply.github.com> | ||
| 3 | Date: Thu, 4 Sep 2025 17:25:39 -0700 | ||
| 4 | Subject: [PATCH] Prevent XML injection: reject '<'/'>' in element/attr names | ||
| 5 | (incl. @xmlns) | ||
| 6 | |||
| 7 | * Add tests for tag names, attribute names, and @xmlns prefixes; confirm attr values are escaped. | ||
| 8 | |||
| 9 | CVE: CVE-2025-9375 | ||
| 10 | |||
| 11 | Upstream-Status: Backport | ||
| 12 | https://github.com/martinblech/xmltodict/commit/ecd456ab88d379514b116ef9293318b74e5ed3ee | ||
| 13 | https://git.launchpad.net/ubuntu/+source/python-xmltodict/commit/?id=e8110a20e00d80db31d5fc9f8f4577328385d6b6 | ||
| 14 | |||
| 15 | Signed-off-by: Saravanan <saravanan.kadambathursubramaniyam@windriver.com> | ||
| 16 | |||
| 17 | --- | ||
| 18 | tests/test_dicttoxml.py | 32 ++++++++++++++++++++++++++++++++ | ||
| 19 | xmltodict.py | 20 +++++++++++++++++++- | ||
| 20 | 2 files changed, 51 insertions(+), 1 deletion(-) | ||
| 21 | |||
| 22 | Index: python-xmltodict-0.13.0/tests/test_dicttoxml.py | ||
| 23 | =================================================================== | ||
| 24 | --- python-xmltodict-0.13.0.orig/tests/test_dicttoxml.py | ||
| 25 | +++ python-xmltodict-0.13.0/tests/test_dicttoxml.py | ||
| 26 | @@ -213,3 +213,35 @@ xmlns:b="http://b.com/"><x a:attr="val"> | ||
| 27 | expected_xml = '<?xml version="1.0" encoding="utf-8"?>\n<x>false</x>' | ||
| 28 | xml = unparse(dict(x=False)) | ||
| 29 | self.assertEqual(xml, expected_xml) | ||
| 30 | + | ||
| 31 | + def test_rejects_tag_name_with_angle_brackets(self): | ||
| 32 | + # Minimal guard: disallow '<' or '>' to prevent breaking tag context | ||
| 33 | + with self.assertRaises(ValueError): | ||
| 34 | + unparse({"m><tag>content</tag": "unsafe"}, full_document=False) | ||
| 35 | + | ||
| 36 | + def test_rejects_attribute_name_with_angle_brackets(self): | ||
| 37 | + # Now we expect bad attribute names to be rejected | ||
| 38 | + with self.assertRaises(ValueError): | ||
| 39 | + unparse( | ||
| 40 | + {"a": {"@m><tag>content</tag": "unsafe", "#text": "x"}}, | ||
| 41 | + full_document=False, | ||
| 42 | + ) | ||
| 43 | + | ||
| 44 | + def test_rejects_malicious_xmlns_prefix(self): | ||
| 45 | + # xmlns prefixes go under @xmlns mapping; reject angle brackets in prefix | ||
| 46 | + with self.assertRaises(ValueError): | ||
| 47 | + unparse( | ||
| 48 | + { | ||
| 49 | + "a": { | ||
| 50 | + "@xmlns": {"m><bad": "http://example.com/"}, | ||
| 51 | + "#text": "x", | ||
| 52 | + } | ||
| 53 | + }, | ||
| 54 | + full_document=False, | ||
| 55 | + ) | ||
| 56 | + | ||
| 57 | + def test_attribute_values_with_angle_brackets_are_escaped(self): | ||
| 58 | + # Attribute values should be escaped by XMLGenerator | ||
| 59 | + xml = unparse({"a": {"@attr": "1<middle>2", "#text": "x"}}, full_document=False) | ||
| 60 | + # The generated XML should contain escaped '<' and '>' within the attribute value | ||
| 61 | + self.assertIn('attr="1<middle>2"', xml) | ||
| 62 | Index: python-xmltodict-0.13.0/xmltodict.py | ||
| 63 | =================================================================== | ||
| 64 | --- python-xmltodict-0.13.0.orig/xmltodict.py | ||
| 65 | +++ python-xmltodict-0.13.0/xmltodict.py | ||
| 66 | @@ -379,6 +379,14 @@ def parse(xml_input, encoding=None, expa | ||
| 67 | return handler.item | ||
| 68 | |||
| 69 | |||
| 70 | +def _has_angle_brackets(value): | ||
| 71 | + """Return True if value (a str) contains '<' or '>'. | ||
| 72 | + | ||
| 73 | + Non-string values return False. Uses fast substring checks implemented in C. | ||
| 74 | + """ | ||
| 75 | + return isinstance(value, str) and ("<" in value or ">" in value) | ||
| 76 | + | ||
| 77 | + | ||
| 78 | def _process_namespace(name, namespaces, ns_sep=':', attr_prefix='@'): | ||
| 79 | if not namespaces: | ||
| 80 | return name | ||
| 81 | @@ -412,6 +420,9 @@ def _emit(key, value, content_handler, | ||
| 82 | if result is None: | ||
| 83 | return | ||
| 84 | key, value = result | ||
| 85 | + # Minimal validation to avoid breaking out of tag context | ||
| 86 | + if _has_angle_brackets(key): | ||
| 87 | + raise ValueError('Invalid element name: "<" or ">" not allowed') | ||
| 88 | if (not hasattr(value, '__iter__') | ||
| 89 | or isinstance(value, _basestring) | ||
| 90 | or isinstance(value, dict)): | ||
| 91 | @@ -445,12 +456,19 @@ def _emit(key, value, content_handler, | ||
| 92 | attr_prefix) | ||
| 93 | if ik == '@xmlns' and isinstance(iv, dict): | ||
| 94 | for k, v in iv.items(): | ||
| 95 | + if _has_angle_brackets(k): | ||
| 96 | + raise ValueError( | ||
| 97 | + 'Invalid attribute name: "<" or ">" not allowed' | ||
| 98 | + ) | ||
| 99 | attr = 'xmlns{}'.format(':{}'.format(k) if k else '') | ||
| 100 | attrs[attr] = _unicode(v) | ||
| 101 | continue | ||
| 102 | if not isinstance(iv, _unicode): | ||
| 103 | iv = _unicode(iv) | ||
| 104 | - attrs[ik[len(attr_prefix):]] = iv | ||
| 105 | + attr_name = ik[len(attr_prefix) :] | ||
| 106 | + if _has_angle_brackets(attr_name): | ||
| 107 | + raise ValueError('Invalid attribute name: "<" or ">" not allowed') | ||
| 108 | + attrs[attr_name] = iv | ||
| 109 | continue | ||
| 110 | children.append((ik, iv)) | ||
| 111 | if pretty: | ||
diff --git a/meta/recipes-devtools/python/python3-xmltodict/CVE-2025-9375-2.patch b/meta/recipes-devtools/python/python3-xmltodict/CVE-2025-9375-2.patch new file mode 100644 index 0000000000..1be22cff6e --- /dev/null +++ b/meta/recipes-devtools/python/python3-xmltodict/CVE-2025-9375-2.patch | |||
| @@ -0,0 +1,176 @@ | |||
| 1 | From f98c90f071228ed73df997807298e1df4f790c33 Mon Sep 17 00:00:00 2001 | ||
| 2 | From: Martin Blech <78768+martinblech@users.noreply.github.com> | ||
| 3 | Date: Mon, 8 Sep 2025 11:18:33 -0700 | ||
| 4 | Subject: [PATCH] Enhance unparse() XML name validation with stricter rules and | ||
| 5 | tests | ||
| 6 | |||
| 7 | Extend existing validation (previously only for "<" and ">") to also | ||
| 8 | reject element, attribute, and xmlns prefix names that are non-string, | ||
| 9 | start with "?" or "!", or contain "/", spaces, tabs, or newlines. | ||
| 10 | Update _emit and namespace handling to use _validate_name. Add tests | ||
| 11 | covering these new invalid name cases. | ||
| 12 | |||
| 13 | CVE: CVE-2025-9375 | ||
| 14 | |||
| 15 | Upstream-Status: Backport | ||
| 16 | https://github.com/martinblech/xmltodict/commit/f98c90f071228ed73df997807298e1df4f790c33 | ||
| 17 | https://git.launchpad.net/ubuntu/+source/python-xmltodict/commit/?id=e8110a20e00d80db31d5fc9f8f4577328385d6b6 | ||
| 18 | |||
| 19 | Signed-off-by: Saravanan <saravanan.kadambathursubramaniyam@windriver.com | ||
| 20 | --- | ||
| 21 | tests/test_dicttoxml.py | 60 +++++++++++++++++++++++++++++++++++++++++ | ||
| 22 | xmltodict.py | 48 ++++++++++++++++++++++++++------- | ||
| 23 | 2 files changed, 99 insertions(+), 9 deletions(-) | ||
| 24 | |||
| 25 | Index: python-xmltodict-0.13.0/tests/test_dicttoxml.py | ||
| 26 | =================================================================== | ||
| 27 | --- python-xmltodict-0.13.0.orig/tests/test_dicttoxml.py | ||
| 28 | +++ python-xmltodict-0.13.0/tests/test_dicttoxml.py | ||
| 29 | @@ -245,3 +245,63 @@ xmlns:b="http://b.com/"><x a:attr="val"> | ||
| 30 | xml = unparse({"a": {"@attr": "1<middle>2", "#text": "x"}}, full_document=False) | ||
| 31 | # The generated XML should contain escaped '<' and '>' within the attribute value | ||
| 32 | self.assertIn('attr="1<middle>2"', xml) | ||
| 33 | + | ||
| 34 | + def test_rejects_tag_name_starting_with_question(self): | ||
| 35 | + with self.assertRaises(ValueError): | ||
| 36 | + unparse({"?pi": "data"}, full_document=False) | ||
| 37 | + | ||
| 38 | + def test_rejects_tag_name_starting_with_bang(self): | ||
| 39 | + with self.assertRaises(ValueError): | ||
| 40 | + unparse({"!decl": "data"}, full_document=False) | ||
| 41 | + | ||
| 42 | + def test_rejects_attribute_name_starting_with_question(self): | ||
| 43 | + with self.assertRaises(ValueError): | ||
| 44 | + unparse({"a": {"@?weird": "x"}}, full_document=False) | ||
| 45 | + | ||
| 46 | + def test_rejects_attribute_name_starting_with_bang(self): | ||
| 47 | + with self.assertRaises(ValueError): | ||
| 48 | + unparse({"a": {"@!weird": "x"}}, full_document=False) | ||
| 49 | + | ||
| 50 | + def test_rejects_xmlns_prefix_starting_with_question_or_bang(self): | ||
| 51 | + with self.assertRaises(ValueError): | ||
| 52 | + unparse({"a": {"@xmlns": {"?p": "http://e/"}}}, full_document=False) | ||
| 53 | + with self.assertRaises(ValueError): | ||
| 54 | + unparse({"a": {"@xmlns": {"!p": "http://e/"}}}, full_document=False) | ||
| 55 | + | ||
| 56 | + def test_rejects_non_string_names(self): | ||
| 57 | + class Weird: | ||
| 58 | + def __str__(self): | ||
| 59 | + return "bad>name" | ||
| 60 | + | ||
| 61 | + # Non-string element key | ||
| 62 | + with self.assertRaises(ValueError): | ||
| 63 | + unparse({Weird(): "x"}, full_document=False) | ||
| 64 | + # Non-string attribute key | ||
| 65 | + with self.assertRaises(ValueError): | ||
| 66 | + unparse({"a": {Weird(): "x"}}, full_document=False) | ||
| 67 | + | ||
| 68 | + def test_rejects_tag_name_with_slash(self): | ||
| 69 | + with self.assertRaises(ValueError): | ||
| 70 | + unparse({"bad/name": "x"}, full_document=False) | ||
| 71 | + | ||
| 72 | + def test_rejects_tag_name_with_whitespace(self): | ||
| 73 | + for name in ["bad name", "bad\tname", "bad\nname"]: | ||
| 74 | + with self.assertRaises(ValueError): | ||
| 75 | + unparse({name: "x"}, full_document=False) | ||
| 76 | + | ||
| 77 | + def test_rejects_attribute_name_with_slash(self): | ||
| 78 | + with self.assertRaises(ValueError): | ||
| 79 | + unparse({"a": {"@bad/name": "x"}}, full_document=False) | ||
| 80 | + | ||
| 81 | + def test_rejects_attribute_name_with_whitespace(self): | ||
| 82 | + for name in ["@bad name", "@bad\tname", "@bad\nname"]: | ||
| 83 | + with self.assertRaises(ValueError): | ||
| 84 | + unparse({"a": {name: "x"}}, full_document=False) | ||
| 85 | + | ||
| 86 | + def test_rejects_xmlns_prefix_with_slash_or_whitespace(self): | ||
| 87 | + # Slash | ||
| 88 | + with self.assertRaises(ValueError): | ||
| 89 | + unparse({"a": {"@xmlns": {"bad/prefix": "http://e/"}}}, full_document=False) | ||
| 90 | + # Whitespace | ||
| 91 | + with self.assertRaises(ValueError): | ||
| 92 | + unparse({"a": {"@xmlns": {"bad prefix": "http://e/"}}}, full_document=False) | ||
| 93 | Index: python-xmltodict-0.13.0/xmltodict.py | ||
| 94 | =================================================================== | ||
| 95 | --- python-xmltodict-0.13.0.orig/xmltodict.py | ||
| 96 | +++ python-xmltodict-0.13.0/xmltodict.py | ||
| 97 | @@ -387,7 +387,42 @@ def _has_angle_brackets(value): | ||
| 98 | return isinstance(value, str) and ("<" in value or ">" in value) | ||
| 99 | |||
| 100 | |||
| 101 | +def _has_invalid_name_chars(value): | ||
| 102 | + """Return True if value (a str) contains any disallowed name characters. | ||
| 103 | + | ||
| 104 | + Disallowed: '<', '>', '/', or any whitespace character. | ||
| 105 | + Non-string values return False. | ||
| 106 | + """ | ||
| 107 | + if not isinstance(value, str): | ||
| 108 | + return False | ||
| 109 | + if "<" in value or ">" in value or "/" in value: | ||
| 110 | + return True | ||
| 111 | + # Check for any whitespace (spaces, tabs, newlines, etc.) | ||
| 112 | + return any(ch.isspace() for ch in value) | ||
| 113 | + | ||
| 114 | + | ||
| 115 | +def _validate_name(value, kind): | ||
| 116 | + """Validate an element/attribute name for XML safety. | ||
| 117 | + | ||
| 118 | + Raises ValueError with a specific reason when invalid. | ||
| 119 | + | ||
| 120 | + kind: 'element' or 'attribute' (used in error messages) | ||
| 121 | + """ | ||
| 122 | + if not isinstance(value, str): | ||
| 123 | + raise ValueError(f"{kind} name must be a string") | ||
| 124 | + if value.startswith("?") or value.startswith("!"): | ||
| 125 | + raise ValueError(f'Invalid {kind} name: cannot start with "?" or "!"') | ||
| 126 | + if "<" in value or ">" in value: | ||
| 127 | + raise ValueError(f'Invalid {kind} name: "<" or ">" not allowed') | ||
| 128 | + if "/" in value: | ||
| 129 | + raise ValueError(f'Invalid {kind} name: "/" not allowed') | ||
| 130 | + if any(ch.isspace() for ch in value): | ||
| 131 | + raise ValueError(f"Invalid {kind} name: whitespace not allowed") | ||
| 132 | + | ||
| 133 | + | ||
| 134 | def _process_namespace(name, namespaces, ns_sep=':', attr_prefix='@'): | ||
| 135 | + if not isinstance(name, str): | ||
| 136 | + return name | ||
| 137 | if not namespaces: | ||
| 138 | return name | ||
| 139 | try: | ||
| 140 | @@ -421,8 +456,7 @@ def _emit(key, value, content_handler, | ||
| 141 | return | ||
| 142 | key, value = result | ||
| 143 | # Minimal validation to avoid breaking out of tag context | ||
| 144 | - if _has_angle_brackets(key): | ||
| 145 | - raise ValueError('Invalid element name: "<" or ">" not allowed') | ||
| 146 | + _validate_name(key, "element") | ||
| 147 | if (not hasattr(value, '__iter__') | ||
| 148 | or isinstance(value, _basestring) | ||
| 149 | or isinstance(value, dict)): | ||
| 150 | @@ -451,23 +485,19 @@ def _emit(key, value, content_handler, | ||
| 151 | if ik == cdata_key: | ||
| 152 | cdata = iv | ||
| 153 | continue | ||
| 154 | - if ik.startswith(attr_prefix): | ||
| 155 | + if isinstance(ik, str) and ik.startswith(attr_prefix): | ||
| 156 | ik = _process_namespace(ik, namespaces, namespace_separator, | ||
| 157 | attr_prefix) | ||
| 158 | if ik == '@xmlns' and isinstance(iv, dict): | ||
| 159 | for k, v in iv.items(): | ||
| 160 | - if _has_angle_brackets(k): | ||
| 161 | - raise ValueError( | ||
| 162 | - 'Invalid attribute name: "<" or ">" not allowed' | ||
| 163 | - ) | ||
| 164 | + _validate_name(k, "attribute") | ||
| 165 | attr = 'xmlns{}'.format(':{}'.format(k) if k else '') | ||
| 166 | attrs[attr] = _unicode(v) | ||
| 167 | continue | ||
| 168 | if not isinstance(iv, _unicode): | ||
| 169 | iv = _unicode(iv) | ||
| 170 | attr_name = ik[len(attr_prefix) :] | ||
| 171 | - if _has_angle_brackets(attr_name): | ||
| 172 | - raise ValueError('Invalid attribute name: "<" or ">" not allowed') | ||
| 173 | + _validate_name(attr_name, "attribute") | ||
| 174 | attrs[attr_name] = iv | ||
| 175 | continue | ||
| 176 | children.append((ik, iv)) | ||
