Files
cleveragents-core/features/steps/pyyaml_security_steps.py
T
HAL9000 021d09991a fix(tests): resolve AmbiguousStep collisions in pyyaml_security tests
Three step patterns in pyyaml_security_steps.py clashed with existing
step files, causing all Behave features to error at load time:
- "I call load_yaml_text with YAML text" clashed with actor_config_coverage_boost_steps.py:103
- "the load_yaml_text result should have key" clashed with actor_config_coverage_boost_steps.py:90
- "a ValueError should be raised" clashed with lsp_registry_steps.py:475

Rename all three to unique patterns and update pyyaml_security.feature
to match. Also fix typings/behave/runner.pyi ruff format (.pyi convention:
single blank line before class, no blank lines between stub methods) and
add missing fastapi>=0.100.0 to pyproject.toml (asgi_app.py imports
fastapi but it was absent from declared dependencies, causing typecheck
and integration test failures).

Refs: #9055
2026-06-14 15:50:15 -04:00

101 lines
3.9 KiB
Python

"""Step definitions for pyyaml_security.feature.
Verifies that PyYAML is pinned to a secure version (>=6.0.3) and that
all YAML loading in the codebase uses safe_load to prevent arbitrary
code execution (CVE-2017-18342 mitigation, issue #9055).
"""
from __future__ import annotations
from importlib.metadata import version as pkg_version
from typing import Any
from behave import given, then, when
from behave.runner import Context
from cleveragents.actor.yaml_loader import load_yaml_text
def _version_tuple(version_str: str) -> tuple[int, ...]:
"""Convert a version string like '6.0.3' to a comparable tuple."""
return tuple(int(part) for part in version_str.split(".")[:3])
# ── PyYAML version constraint ────────────────────────────────────────
@given("the PyYAML package is installed")
def step_pyyaml_installed(context: Context) -> None:
"""Verify PyYAML is importable and its version is accessible."""
import yaml # noqa: F401 — presence check
context.pyyaml_version = pkg_version("pyyaml")
@when("I check the installed PyYAML version")
def step_check_pyyaml_version(context: Context) -> None:
"""Record the installed PyYAML version tuple for assertion."""
context.pyyaml_version_tuple = _version_tuple(context.pyyaml_version)
@then("the version should be at least 6.0.3")
def step_assert_pyyaml_version(context: Context) -> None:
"""Assert that the installed PyYAML version is >= 6.0.3."""
minimum = (6, 0, 3)
assert context.pyyaml_version_tuple >= minimum, (
f"PyYAML version {context.pyyaml_version} is below the required "
f"minimum 6.0.3. Upgrade PyYAML to address CVE-2017-18342."
)
# ── yaml_loader safe_load enforcement ───────────────────────────────
@when('I load the YAML text "{yaml_text}" using the secure yaml_loader')
def step_load_yaml_text(context: Context, yaml_text: str) -> None:
"""Call load_yaml_text with the given YAML text and store the result.
The YAML text is passed as a quoted string in the step, with ``\\n``
escape sequences representing literal newlines.
"""
normalised = yaml_text.replace("\\n", "\n")
context.load_yaml_result: dict[str, Any] = load_yaml_text(normalised)
@then('the secure yaml_loader result for key "{key}" should be "{value}"')
def step_assert_yaml_result_key(context: Context, key: str, value: str) -> None:
"""Assert that the parsed YAML result contains the expected key/value pair."""
result: dict[str, Any] = context.load_yaml_result
assert key in result, (
f"Expected key '{key}' in load_yaml_text result, but it was not found. "
f"Actual keys: {list(result.keys())}"
)
assert result[key] == value, (
f"Expected result['{key}'] == '{value}', but got '{result[key]}'."
)
@when("I call load_yaml_text with unsafe YAML containing a Python object tag")
def step_load_unsafe_yaml(context: Context) -> None:
"""Attempt to load YAML with a Python object constructor tag.
PyYAML's safe_load rejects ``!!python/object`` tags, raising an error.
This verifies that the yaml_loader does not use the unsafe ``yaml.load``
with the default Loader (which would execute arbitrary Python code).
"""
context.caught_error = None
unsafe_yaml = "!!python/object/apply:os.system ['echo pwned']"
try:
load_yaml_text(unsafe_yaml)
except Exception as exc:
context.caught_error = exc
@then("a pyyaml security ValueError should be raised")
def step_assert_value_error_raised(context: Context) -> None:
"""Assert that an error was raised when loading unsafe YAML."""
assert context.caught_error is not None, (
"Expected an error when loading YAML with Python object tags, "
"but none was raised. This may indicate unsafe YAML loading is in use."
)