Skip to content

Commit 51d9cc6

Browse files
dont allow yaml tags in dict
1 parent 033b6a1 commit 51d9cc6

File tree

3 files changed

+36
-90
lines changed

3 files changed

+36
-90
lines changed

pyamlo/sources.py

Lines changed: 2 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -8,72 +8,6 @@
88
from pyamlo.security import SecurityPolicy
99

1010

11-
def _process_dict_tags(data: Any, security_policy: SecurityPolicy) -> Any:
12-
"""Process dictionary data - handle both raw dicts and dicts with YAML tag strings."""
13-
14-
def has_yaml_tags(obj):
15-
"""Check if the data structure contains YAML tag-like strings."""
16-
if isinstance(obj, dict):
17-
return any(has_yaml_tags(v) for v in obj.values())
18-
elif isinstance(obj, list):
19-
return any(has_yaml_tags(item) for item in obj)
20-
elif isinstance(obj, str):
21-
return obj.startswith('!')
22-
return False
23-
24-
# If the dict contains no YAML tag strings, it's already fully resolved
25-
if not has_yaml_tags(data):
26-
return data
27-
28-
# Otherwise, process YAML tags using the YAML stream approach (simpler than direct parsing)
29-
from io import StringIO
30-
import yaml
31-
32-
def dict_to_yaml_text(obj, indent=0):
33-
"""Convert dict to YAML text without escaping tags."""
34-
def format_value(val):
35-
if isinstance(val, str):
36-
# Don't quote strings that are YAML tags
37-
if val.startswith('!'):
38-
return val
39-
# Quote strings to preserve them as strings
40-
dumped = yaml.safe_dump(val, default_flow_style=True).strip()
41-
# Remove document separators
42-
if dumped.endswith('\n...'):
43-
dumped = dumped[:-4]
44-
elif dumped.endswith('...'):
45-
dumped = dumped[:-3]
46-
return dumped
47-
return str(val)
48-
49-
if isinstance(obj, dict):
50-
lines = []
51-
for key, value in obj.items():
52-
if isinstance(value, dict):
53-
lines.append(" " * indent + f"{key}:")
54-
lines.append(dict_to_yaml_text(value, indent + 1))
55-
elif isinstance(value, list):
56-
lines.append(" " * indent + f"{key}:")
57-
for item in value:
58-
if isinstance(item, dict):
59-
lines.append(" " * (indent + 1) + "-")
60-
lines.append(dict_to_yaml_text(item, indent + 2))
61-
else:
62-
lines.append(" " * (indent + 1) + f"- {format_value(item)}")
63-
else:
64-
lines.append(" " * indent + f"{key}: {format_value(value)}")
65-
return '\n'.join(lines)
66-
return str(obj)
67-
68-
# Convert to YAML text and parse with ConfigLoader
69-
yaml_text = dict_to_yaml_text(data)
70-
loader = ConfigLoader(StringIO(yaml_text), security_policy=security_policy)
71-
try:
72-
return loader.get_single_data()
73-
finally:
74-
loader.dispose()
75-
76-
7711
def _load_source(
7812
source: Union[str, Path, IO[str]], security_policy: SecurityPolicy
7913
) -> dict[str, Any]:
@@ -93,7 +27,8 @@ def _process_single_source(
9327
src: Union[str, Path, IO[str], dict], security_policy: SecurityPolicy
9428
) -> dict[str, Any]:
9529
if isinstance(src, dict):
96-
raw = _process_dict_tags(src.copy(), security_policy)
30+
# Dict sources contain already resolved Python objects, use as-is
31+
raw = src.copy()
9732
src_path = "<dict>"
9833
else:
9934
raw = _load_source(src, security_policy=security_policy)

tests/test_dict_sources.py

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -110,18 +110,22 @@ def test_dict_with_interpolation():
110110

111111

112112
def test_dict_with_object_instantiation():
113-
"""Test dictionary source with object instantiation."""
113+
"""Test dictionary source with actual Python objects."""
114+
from pathlib import Path
115+
114116
config_dict = {
115117
"paths": {
116-
"base": "!@pathlib.Path /tmp/test",
117-
"data": "!@pathlib.Path /tmp/test/data.txt"
118+
"base": Path("/tmp/test"),
119+
"data": Path("/tmp/test/data.txt")
118120
}
119121
}
120122

121123
result = load_config(config_dict)
122124

123125
assert str(result["paths"]["base"]) == "/tmp/test"
124126
assert str(result["paths"]["data"]) == "/tmp/test/data.txt"
127+
assert isinstance(result["paths"]["base"], Path)
128+
assert isinstance(result["paths"]["data"], Path)
125129

126130

127131
def test_empty_dict_source():
@@ -131,25 +135,28 @@ def test_empty_dict_source():
131135

132136

133137
def test_dict_with_security_policy():
134-
"""Test dictionary source with restrictive security policy."""
138+
"""Test dictionary source with actual Python objects and security policy."""
139+
from pathlib import Path
140+
135141
config_dict = {
136142
"safe": {
137143
"value": "test"
138144
},
139-
"unsafe": {
140-
"path": "!@pathlib.Path /tmp"
145+
"paths": {
146+
"path": Path("/tmp")
141147
}
142148
}
143149

144150
restrictive_policy = SecurityPolicy(restrictive=True)
145151

146-
# Should work with non-restrictive policy
147-
result = load_config(config_dict)
148-
assert str(result["unsafe"]["path"]) == "/tmp"
152+
# Should work with both policies since dict contains actual objects, not tags
153+
result1 = load_config(config_dict)
154+
assert str(result1["paths"]["path"]) == "/tmp"
155+
assert isinstance(result1["paths"]["path"], Path)
149156

150-
# Should fail with restrictive policy
151-
with pytest.raises(Exception):
152-
load_config(config_dict, security_policy=restrictive_policy)
157+
result2 = load_config(config_dict, security_policy=restrictive_policy)
158+
assert str(result2["paths"]["path"]) == "/tmp"
159+
assert isinstance(result2["paths"]["path"], Path)
153160

154161

155162
def test_nested_dict_merging():

tests/test_nested_interpolation_bug.py

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -95,17 +95,21 @@ def test_deep_nested_interpolation():
9595

9696
def test_object_property_access():
9797
"""Test accessing properties of instantiated objects."""
98-
config1 = {
99-
"stage": {
100-
"step": {
101-
"component1": {
102-
"path": "!@pathlib.Path /tmp/test/example.txt",
103-
"version": 1.0,
104-
}
105-
}
106-
}
107-
}
98+
from io import StringIO
99+
100+
# Step 1: Build config with YAML tags from YAML source
101+
config1_yaml = """
102+
stage:
103+
step:
104+
component1:
105+
path: !@pathlib.Path /tmp/test/example.txt
106+
version: 1.0
107+
"""
108+
109+
# Load and resolve the YAML config first
110+
config1_built = load_config(StringIO(config1_yaml))
108111

112+
# Step 2: Use the built config with dict containing interpolations
109113
config2 = {
110114
"stage": {
111115
"step": {
@@ -118,7 +122,7 @@ def test_object_property_access():
118122
}
119123
}
120124

121-
result = load_config([config1, config2])
125+
result = load_config([config1_built, config2])
122126

123127
# Check that the object was created correctly
124128
path_obj = result["stage"]["step"]["component1"]["path"]

0 commit comments

Comments
 (0)