diff --git a/lib/crewai/pyproject.toml b/lib/crewai/pyproject.toml index 2e484df9d..23c9bb0a0 100644 --- a/lib/crewai/pyproject.toml +++ b/lib/crewai/pyproject.toml @@ -32,7 +32,7 @@ dependencies = [ "click>=8.1.7,<9", "appdirs~=1.4.4", "jsonref~=1.1.0", - "json-repair~=0.25.2", + "json-repair>=0.60.1,<1.0.0", "cel-python>=0.5.0,<0.6", "tomli-w~=1.1.0", "tomli~=2.0.2", diff --git a/lib/crewai/src/crewai/agents/constants.py b/lib/crewai/src/crewai/agents/constants.py index 7a180f947..9b824d4ba 100644 --- a/lib/crewai/src/crewai/agents/constants.py +++ b/lib/crewai/src/crewai/agents/constants.py @@ -14,7 +14,8 @@ MISSING_ACTION_INPUT_AFTER_ACTION_ERROR_MESSAGE: Final[str] = ( FINAL_ANSWER_AND_PARSABLE_ACTION_ERROR_MESSAGE: Final[str] = ( "I did it wrong. Tried to both perform Action and give a Final Answer at the same time, I must do one or the other" ) -UNABLE_TO_REPAIR_JSON_RESULTS: Final[list[str]] = ['""', "{}"] +# json-repair < 0.60 signaled unrepairable input with '""'; >= 0.60 returns "". +UNABLE_TO_REPAIR_JSON_RESULTS: Final[list[str]] = ['""', "{}", ""] ACTION_INPUT_REGEX: Final[re.Pattern[str]] = re.compile( r"Action\s*\d*\s*:\s*(.*?)\s*Action\s*\d*\s*Input\s*\d*\s*:\s*(.*)", re.DOTALL ) diff --git a/lib/crewai/src/crewai/agents/parser.py b/lib/crewai/src/crewai/agents/parser.py index c59719226..61c792271 100644 --- a/lib/crewai/src/crewai/agents/parser.py +++ b/lib/crewai/src/crewai/agents/parser.py @@ -6,6 +6,7 @@ AgentAction or AgentFinish objects. """ from dataclasses import dataclass +import json from json_repair import repair_json # type: ignore[import-untyped] from pydantic import BaseModel @@ -176,4 +177,14 @@ def _safe_repair_json(tool_input: str) -> str: if result in UNABLE_TO_REPAIR_JSON_RESULTS: return tool_input + # Non-JSON input (e.g. "{temperature in SF}") gets coerced into JSON + # scalars/arrays by json-repair rather than repaired; only a repaired + # object is a plausible tool input, so keep the original otherwise. + try: + repaired_object = json.loads(str(result)) + except (ValueError, TypeError): + return tool_input + if not isinstance(repaired_object, dict): + return tool_input + return str(result) diff --git a/lib/crewai/tests/security/test_dependency_pins.py b/lib/crewai/tests/security/test_dependency_pins.py new file mode 100644 index 000000000..7157c6a05 --- /dev/null +++ b/lib/crewai/tests/security/test_dependency_pins.py @@ -0,0 +1,47 @@ +"""Guardrails for security-sensitive dependency pins in crewai's pyproject. + +These tests parse the published requirement specifiers so that a stale pin +cannot silently reintroduce a known-vulnerable dependency range. +""" + +from pathlib import Path + +from packaging.requirements import Requirement + +try: + import tomllib +except ModuleNotFoundError: # Python 3.10 + import tomli as tomllib # type: ignore[no-redef] + +PYPROJECT_PATH = Path(__file__).parents[2] / "pyproject.toml" + + +def _get_requirement(name: str) -> Requirement: + data = tomllib.loads(PYPROJECT_PATH.read_text()) + for spec in data["project"]["dependencies"]: + requirement = Requirement(spec) + if requirement.name == name: + return requirement + raise AssertionError(f"{name} not found in [project.dependencies]") + + +class TestJsonRepairPin: + """GHSA-xf7x-x43h-rpqh: json-repair < 0.60.1 has an unbounded-CPU DoS on + circular JSON Schema $ref structures. crewai feeds LLM output to + json_repair, so the vulnerable range is a real DoS surface (OSS-91).""" + + def test_requirement_excludes_vulnerable_range(self) -> None: + requirement = _get_requirement("json-repair") + for vulnerable in ("0.25.2", "0.25.3", "0.59.0", "0.60.0"): + assert not requirement.specifier.contains(vulnerable), ( + f"json-repair requirement '{requirement}' admits {vulnerable}, " + "which is vulnerable to GHSA-xf7x-x43h-rpqh (fixed in 0.60.1)" + ) + + def test_requirement_admits_patched_releases(self) -> None: + requirement = _get_requirement("json-repair") + for patched in ("0.60.1", "0.61.4"): + assert requirement.specifier.contains(patched), ( + f"json-repair requirement '{requirement}' blocks patched " + f"version {patched}" + ) diff --git a/uv.lock b/uv.lock index 8e892e4a3..31af0676f 100644 --- a/uv.lock +++ b/uv.lock @@ -13,7 +13,7 @@ resolution-markers = [ ] [options] -exclude-newer = "2026-07-04T15:35:51.457693Z" +exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. exclude-newer-span = "P3D" [options.exclude-newer-package] @@ -1444,7 +1444,7 @@ requires-dist = [ { name = "httpx-sse", marker = "extra == 'a2a'", specifier = "~=0.4.0" }, { name = "ibm-watsonx-ai", marker = "extra == 'watson'", specifier = "~=1.3.39" }, { name = "instructor", specifier = ">=1.3.3" }, - { name = "json-repair", specifier = "~=0.25.2" }, + { name = "json-repair", specifier = ">=0.60.1,<1.0.0" }, { name = "json5", specifier = "~=0.10.0" }, { name = "jsonref", specifier = "~=1.1.0" }, { name = "lancedb", specifier = ">=0.29.2,<0.30.1" }, @@ -3631,11 +3631,11 @@ wheels = [ [[package]] name = "json-repair" -version = "0.25.3" +version = "0.61.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7c/60/484ee009c1867ddc5ffe0ff2131b82e80bbf13fdb59f3d93834f98e56a9f/json_repair-0.25.3.tar.gz", hash = "sha256:4ee970581a05b0b258b749eb8bcac21de380edda97c3717a4edfafc519ec21a4", size = 20619, upload-time = "2024-07-10T13:42:18.977Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ff/7c/6d7c931fb6348f957154f9af926a77777a03ef0f6108c51cc2d7c9876a22/json_repair-0.61.2.tar.gz", hash = "sha256:b63ae5ad44c8720158e24bdd7e33506f7036174c287831b187a51619a6f58a34", size = 50539, upload-time = "2026-07-05T11:53:08.137Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f0/9e/2ab68cc0ff030e1ef78329d7b933473d3ad2c7d0e66aede6a7c87f74753c/json_repair-0.25.3-py3-none-any.whl", hash = "sha256:f00b510dd21b31ebe72581bdb07e66381df2883d6f640c89605e482882c12b17", size = 12812, upload-time = "2024-07-10T13:42:16.918Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ba/c90076ab3fb22d43a31e2e53f56ffbaea25d524d8db3b38082779a562789/json_repair-0.61.2-py3-none-any.whl", hash = "sha256:779a0cdb48f609d6eb669273023cf4c92b5ac6d80a496eb919ee93c84c351ec8", size = 48977, upload-time = "2026-07-05T11:53:06.774Z" }, ] [[package]]