Skip to content

Commit 2c4451e

Browse files
authored
Merge commit from fork
3.x: Merge Errors Fast
2 parents 4ab61bf + 86d101a commit 2c4451e

File tree

4 files changed

+47
-14
lines changed

4 files changed

+47
-14
lines changed

CHANGELOG.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
Changelog
22
---------
33

4+
3.26.2 (2025-12-19)
5+
++++++++++++++++++
6+
7+
Bug fixes:
8+
9+
- :cve:`CVE-2025-68480`: Merge error store messages without rebuilding collections.
10+
Thanks 카푸치노 for reporting and :user:`deckar01` for the fix.
11+
412
3.26.1 (2025-02-03)
513
*******************
614

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "marshmallow"
3-
version = "3.26.1"
3+
version = "3.26.2"
44
description = "A lightweight library for converting complex datatypes to and from native Python datatypes."
55
readme = "README.rst"
66
license = { file = "LICENSE" }

src/marshmallow/error_store.py

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,19 @@ def store_error(self, messages, field_name=SCHEMA, index=None):
1818
# field error -> store/merge error messages under field name key
1919
# schema error -> if string or list, store/merge under _schema key
2020
# -> if dict, store/merge with other top-level keys
21+
messages = copy_containers(messages)
2122
if field_name != SCHEMA or not isinstance(messages, dict):
2223
messages = {field_name: messages}
2324
if index is not None:
2425
messages = {index: messages}
2526
self.errors = merge_errors(self.errors, messages)
2627

28+
def copy_containers(errors):
29+
if isinstance(errors, list):
30+
return [copy_containers(val) for val in errors]
31+
if isinstance(errors, dict):
32+
return {key: copy_containers(val) for key, val in errors.items()}
33+
return errors
2734

2835
def merge_errors(errors1, errors2): # noqa: PLR0911
2936
"""Deeply merge two error messages.
@@ -37,24 +44,26 @@ def merge_errors(errors1, errors2): # noqa: PLR0911
3744
return errors1
3845
if isinstance(errors1, list):
3946
if isinstance(errors2, list):
40-
return errors1 + errors2
47+
errors1.extend(errors2)
48+
return errors1
4149
if isinstance(errors2, dict):
42-
return dict(errors2, **{SCHEMA: merge_errors(errors1, errors2.get(SCHEMA))})
43-
return [*errors1, errors2]
50+
errors2[SCHEMA] = merge_errors(errors1, errors2.get(SCHEMA))
51+
return errors2
52+
errors1.append(errors2)
53+
return errors1
4454
if isinstance(errors1, dict):
45-
if isinstance(errors2, list):
46-
return dict(errors1, **{SCHEMA: merge_errors(errors1.get(SCHEMA), errors2)})
4755
if isinstance(errors2, dict):
48-
errors = dict(errors1)
4956
for key, val in errors2.items():
50-
if key in errors:
51-
errors[key] = merge_errors(errors[key], val)
57+
if key in errors1:
58+
errors1[key] = merge_errors(errors1[key], val)
5259
else:
53-
errors[key] = val
54-
return errors
55-
return dict(errors1, **{SCHEMA: merge_errors(errors1.get(SCHEMA), errors2)})
60+
errors1[key] = val
61+
return errors1
62+
errors1[SCHEMA] = merge_errors(errors1.get(SCHEMA), errors2)
63+
return errors1
5664
if isinstance(errors2, list):
5765
return [errors1, *errors2]
5866
if isinstance(errors2, dict):
59-
return dict(errors2, **{SCHEMA: merge_errors(errors1, errors2.get(SCHEMA))})
67+
errors2[SCHEMA] = merge_errors(errors1, errors2.get(SCHEMA))
68+
return errors2
6069
return [errors1, errors2]

tests/test_error_store.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from typing import NamedTuple
22

33
from marshmallow import missing
4-
from marshmallow.error_store import merge_errors
4+
from marshmallow.error_store import merge_errors, ErrorStore
55

66

77
def test_missing_is_falsy():
@@ -149,3 +149,19 @@ def test_deep_merging_dicts(self):
149149
assert merge_errors(
150150
{"field1": {"field2": "error1"}}, {"field1": {"field2": "error2"}}
151151
) == {"field1": {"field2": ["error1", "error2"]}}
152+
153+
def test_list_not_changed(self):
154+
store = ErrorStore()
155+
message = ["foo"]
156+
store.store_error(message)
157+
store.store_error(message)
158+
assert message == ["foo"]
159+
assert store.errors == {"_schema": ["foo", "foo"]}
160+
161+
def test_dict_not_changed(self):
162+
store = ErrorStore()
163+
message = {"foo": ["bar"]}
164+
store.store_error(message)
165+
store.store_error(message)
166+
assert message == {"foo": ["bar"]}
167+
assert store.errors == {"foo": ["bar", "bar"]}

0 commit comments

Comments
 (0)