mirror of
https://github.com/crewAIInc/crewAI.git
synced 2025-12-24 08:18:31 +00:00
Compare commits
3 Commits
fix/unsafe
...
0.203.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20373f673d | ||
|
|
814c962196 | ||
|
|
2ebb2e845f |
@@ -775,4 +775,3 @@ A: Yes, CrewAI provides extensive beginner-friendly tutorials, courses, and docu
|
||||
### Q: Can CrewAI automate human-in-the-loop workflows?
|
||||
|
||||
A: Yes, CrewAI fully supports human-in-the-loop workflows, allowing seamless collaboration between human experts and AI agents for enhanced decision-making.
|
||||
# test
|
||||
|
||||
@@ -40,7 +40,7 @@ def _suppress_pydantic_deprecation_warnings() -> None:
|
||||
|
||||
_suppress_pydantic_deprecation_warnings()
|
||||
|
||||
__version__ = "0.203.0"
|
||||
__version__ = "0.203.2"
|
||||
_telemetry_submitted = False
|
||||
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ def validate_jwt_token(
|
||||
algorithms=["RS256"],
|
||||
audience=audience,
|
||||
issuer=issuer,
|
||||
leeway=10.0,
|
||||
options={
|
||||
"verify_signature": True,
|
||||
"verify_exp": True,
|
||||
|
||||
@@ -5,7 +5,7 @@ description = "{{name}} using crewAI"
|
||||
authors = [{ name = "Your Name", email = "you@example.com" }]
|
||||
requires-python = ">=3.10,<3.14"
|
||||
dependencies = [
|
||||
"crewai[tools]>=0.203.0,<1.0.0"
|
||||
"crewai[tools]>=0.203.1,<1.0.0"
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
||||
@@ -5,7 +5,7 @@ description = "{{name}} using crewAI"
|
||||
authors = [{ name = "Your Name", email = "you@example.com" }]
|
||||
requires-python = ">=3.10,<3.14"
|
||||
dependencies = [
|
||||
"crewai[tools]>=0.203.0,<1.0.0",
|
||||
"crewai[tools]>=0.203.1,<1.0.0",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
||||
@@ -5,7 +5,7 @@ description = "Power up your crews with {{folder_name}}"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10,<3.14"
|
||||
dependencies = [
|
||||
"crewai[tools]>=0.203.0"
|
||||
"crewai[tools]>=0.203.1"
|
||||
]
|
||||
|
||||
[tool.crewai]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import ast
|
||||
import datetime
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
from difflib import SequenceMatcher
|
||||
from json import JSONDecodeError
|
||||
@@ -44,183 +44,6 @@ OPENAI_BIGGER_MODELS = [
|
||||
]
|
||||
|
||||
|
||||
def _safe_literal_parse(input_str: str) -> Any:
|
||||
"""
|
||||
Safely parse a limited subset of Python literal syntax without using ast.literal_eval.
|
||||
Only supports: strings (single/double quotes), numbers, booleans, None, lists, dicts.
|
||||
Rejects any input that could lead to code execution.
|
||||
|
||||
Args:
|
||||
input_str: String to parse
|
||||
|
||||
Returns:
|
||||
Parsed Python object
|
||||
|
||||
Raises:
|
||||
ValueError: If input contains unsafe or unsupported syntax
|
||||
"""
|
||||
if not isinstance(input_str, str):
|
||||
raise ValueError("Input must be a string")
|
||||
|
||||
stripped = input_str.strip()
|
||||
if not stripped:
|
||||
raise ValueError("Input cannot be empty")
|
||||
|
||||
# Check for potentially dangerous patterns
|
||||
dangerous_patterns = [
|
||||
r'__.*__', # dunder methods
|
||||
r'import\b', # import statements
|
||||
r'exec\b', # exec function
|
||||
r'eval\b', # eval function
|
||||
r'lambda\b', # lambda functions
|
||||
r'def\b', # function definitions
|
||||
r'class\b', # class definitions
|
||||
r'@\w+', # decorators
|
||||
r'\.\.\.', # ellipsis (could be used in slicing)
|
||||
r'->[^\]]*\]', # type hints in lists
|
||||
]
|
||||
|
||||
for pattern in dangerous_patterns:
|
||||
if re.search(pattern, stripped, re.IGNORECASE):
|
||||
raise ValueError(f"Potentially dangerous pattern detected: {pattern}")
|
||||
|
||||
# Only allow specific characters
|
||||
allowed_chars = r'[\s\w\.\-\+\*/\(\)\[\]\{\}:\'"<>!=,!=\?%&|~^`]'
|
||||
if not re.fullmatch(f'{allowed_chars}*', stripped):
|
||||
raise ValueError("Input contains unsupported characters")
|
||||
|
||||
# Try JSON parsing first (safest)
|
||||
try:
|
||||
return json.loads(stripped)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
pass
|
||||
|
||||
# Manual parsing for simple Python literals (JSON with single quotes, etc.)
|
||||
try:
|
||||
return _parse_python_literal_safe(stripped)
|
||||
except Exception as e:
|
||||
raise ValueError(f"Failed to parse input safely: {e}")
|
||||
|
||||
|
||||
def _parse_python_literal_safe(input_str: str) -> Any:
|
||||
"""
|
||||
Parse a limited subset of Python literals safely.
|
||||
|
||||
Args:
|
||||
input_str: String to parse
|
||||
|
||||
Returns:
|
||||
Parsed Python object
|
||||
"""
|
||||
# Handle None
|
||||
if input_str == 'None':
|
||||
return None
|
||||
|
||||
# Handle booleans
|
||||
if input_str == 'True':
|
||||
return True
|
||||
if input_str == 'False':
|
||||
return False
|
||||
|
||||
# Handle numbers
|
||||
if re.fullmatch(r'-?\d+$', input_str):
|
||||
return int(input_str)
|
||||
if re.fullmatch(r'-?\d+\.\d+$', input_str):
|
||||
return float(input_str)
|
||||
|
||||
# Handle strings with single quotes (convert to JSON format)
|
||||
if (input_str.startswith("'") and input_str.endswith("'")) or \
|
||||
(input_str.startswith('"') and input_str.endswith('"')):
|
||||
# Simple string - just remove quotes and escape common sequences
|
||||
inner = input_str[1:-1]
|
||||
# Handle common escape sequences safely
|
||||
inner = inner.replace("\\'", "'").replace('\\"', '"').replace("\\\\", "\\")
|
||||
return inner
|
||||
|
||||
# Handle lists
|
||||
if input_str.startswith('[') and input_str.endswith(']'):
|
||||
inner = input_str[1:-1].strip()
|
||||
if not inner:
|
||||
return []
|
||||
|
||||
items = _split_items_safe(inner)
|
||||
return [_parse_python_literal_safe(item.strip()) for item in items]
|
||||
|
||||
# Handle dictionaries
|
||||
if input_str.startswith('{') and input_str.endswith('}'):
|
||||
inner = input_str[1:-1].strip()
|
||||
if not inner:
|
||||
return {}
|
||||
|
||||
pairs = _split_items_safe(inner)
|
||||
result = {}
|
||||
for pair in pairs:
|
||||
if ':' not in pair:
|
||||
raise ValueError(f"Invalid dict pair: {pair}")
|
||||
|
||||
key_str, value_str = pair.split(':', 1)
|
||||
key = _parse_python_literal_safe(key_str.strip())
|
||||
value = _parse_python_literal_safe(value_str.strip())
|
||||
if not isinstance(key, str):
|
||||
raise ValueError(f"Dict keys must be strings, got {type(key)}")
|
||||
result[key] = value
|
||||
|
||||
return result
|
||||
|
||||
raise ValueError(f"Unsupported literal format: {input_str}")
|
||||
|
||||
|
||||
def _split_items_safe(input_str: str, delimiter: str = ',') -> list[str]:
|
||||
"""
|
||||
Split a list or dict string into items, respecting nested structures.
|
||||
|
||||
Args:
|
||||
input_str: String to split
|
||||
delimiter: Delimiter to split on
|
||||
|
||||
Returns:
|
||||
List of item strings
|
||||
"""
|
||||
items = []
|
||||
current = []
|
||||
depth = 0
|
||||
in_string = False
|
||||
string_char = None
|
||||
i = 0
|
||||
|
||||
while i < len(input_str):
|
||||
char = input_str[i]
|
||||
|
||||
# Handle string literals
|
||||
if char in ('"', "'") and (i == 0 or input_str[i-1] != '\\'):
|
||||
if not in_string:
|
||||
in_string = True
|
||||
string_char = char
|
||||
elif char == string_char:
|
||||
in_string = False
|
||||
string_char = None
|
||||
|
||||
# Track nesting depth when not in strings
|
||||
elif not in_string:
|
||||
if char in ('[', '(', '{'):
|
||||
depth += 1
|
||||
elif char in (']', ')', '}'):
|
||||
depth -= 1
|
||||
elif char == delimiter and depth == 0:
|
||||
items.append(''.join(current).strip())
|
||||
current = []
|
||||
i += 1
|
||||
continue
|
||||
|
||||
current.append(char)
|
||||
i += 1
|
||||
|
||||
if current:
|
||||
items.append(''.join(current).strip())
|
||||
|
||||
return items
|
||||
|
||||
|
||||
class ToolUsageError(Exception):
|
||||
"""Exception raised for errors in the tool usage."""
|
||||
|
||||
@@ -701,14 +524,14 @@ class ToolUsage:
|
||||
except (JSONDecodeError, TypeError):
|
||||
pass # Continue to the next parsing attempt
|
||||
|
||||
# Attempt 2: Parse as Python literal (safe alternative to ast.literal_eval)
|
||||
# Attempt 2: Parse as Python literal
|
||||
try:
|
||||
arguments = _safe_literal_parse(tool_input)
|
||||
arguments = ast.literal_eval(tool_input)
|
||||
if isinstance(arguments, dict):
|
||||
return arguments
|
||||
except ValueError:
|
||||
except (ValueError, SyntaxError):
|
||||
repaired_input = repair_json(tool_input)
|
||||
# Continue to the next parsing attempt
|
||||
pass
|
||||
|
||||
# Attempt 3: Parse as JSON5
|
||||
try:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import jwt
|
||||
import unittest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import jwt
|
||||
|
||||
from crewai.cli.authentication.utils import validate_jwt_token
|
||||
|
||||
@@ -17,19 +17,22 @@ class TestUtils(unittest.TestCase):
|
||||
key="mock_signing_key"
|
||||
)
|
||||
|
||||
jwt_token = "aaaaa.bbbbbb.cccccc" # noqa: S105
|
||||
|
||||
decoded_token = validate_jwt_token(
|
||||
jwt_token="aaaaa.bbbbbb.cccccc",
|
||||
jwt_token=jwt_token,
|
||||
jwks_url="https://mock_jwks_url",
|
||||
issuer="https://mock_issuer",
|
||||
audience="app_id_xxxx",
|
||||
)
|
||||
|
||||
mock_jwt.decode.assert_called_with(
|
||||
"aaaaa.bbbbbb.cccccc",
|
||||
jwt_token,
|
||||
"mock_signing_key",
|
||||
algorithms=["RS256"],
|
||||
audience="app_id_xxxx",
|
||||
issuer="https://mock_issuer",
|
||||
leeway=10.0,
|
||||
options={
|
||||
"verify_signature": True,
|
||||
"verify_exp": True,
|
||||
@@ -43,9 +46,9 @@ class TestUtils(unittest.TestCase):
|
||||
|
||||
def test_validate_jwt_token_expired(self, mock_jwt, mock_pyjwkclient):
|
||||
mock_jwt.decode.side_effect = jwt.ExpiredSignatureError
|
||||
with self.assertRaises(Exception):
|
||||
with self.assertRaises(Exception): # noqa: B017
|
||||
validate_jwt_token(
|
||||
jwt_token="aaaaa.bbbbbb.cccccc",
|
||||
jwt_token="aaaaa.bbbbbb.cccccc", # noqa: S106
|
||||
jwks_url="https://mock_jwks_url",
|
||||
issuer="https://mock_issuer",
|
||||
audience="app_id_xxxx",
|
||||
@@ -53,9 +56,9 @@ class TestUtils(unittest.TestCase):
|
||||
|
||||
def test_validate_jwt_token_invalid_audience(self, mock_jwt, mock_pyjwkclient):
|
||||
mock_jwt.decode.side_effect = jwt.InvalidAudienceError
|
||||
with self.assertRaises(Exception):
|
||||
with self.assertRaises(Exception): # noqa: B017
|
||||
validate_jwt_token(
|
||||
jwt_token="aaaaa.bbbbbb.cccccc",
|
||||
jwt_token="aaaaa.bbbbbb.cccccc", # noqa: S106
|
||||
jwks_url="https://mock_jwks_url",
|
||||
issuer="https://mock_issuer",
|
||||
audience="app_id_xxxx",
|
||||
@@ -63,9 +66,9 @@ class TestUtils(unittest.TestCase):
|
||||
|
||||
def test_validate_jwt_token_invalid_issuer(self, mock_jwt, mock_pyjwkclient):
|
||||
mock_jwt.decode.side_effect = jwt.InvalidIssuerError
|
||||
with self.assertRaises(Exception):
|
||||
with self.assertRaises(Exception): # noqa: B017
|
||||
validate_jwt_token(
|
||||
jwt_token="aaaaa.bbbbbb.cccccc",
|
||||
jwt_token="aaaaa.bbbbbb.cccccc", # noqa: S106
|
||||
jwks_url="https://mock_jwks_url",
|
||||
issuer="https://mock_issuer",
|
||||
audience="app_id_xxxx",
|
||||
@@ -75,9 +78,9 @@ class TestUtils(unittest.TestCase):
|
||||
self, mock_jwt, mock_pyjwkclient
|
||||
):
|
||||
mock_jwt.decode.side_effect = jwt.MissingRequiredClaimError
|
||||
with self.assertRaises(Exception):
|
||||
with self.assertRaises(Exception): # noqa: B017
|
||||
validate_jwt_token(
|
||||
jwt_token="aaaaa.bbbbbb.cccccc",
|
||||
jwt_token="aaaaa.bbbbbb.cccccc", # noqa: S106
|
||||
jwks_url="https://mock_jwks_url",
|
||||
issuer="https://mock_issuer",
|
||||
audience="app_id_xxxx",
|
||||
@@ -85,9 +88,9 @@ class TestUtils(unittest.TestCase):
|
||||
|
||||
def test_validate_jwt_token_jwks_error(self, mock_jwt, mock_pyjwkclient):
|
||||
mock_jwt.decode.side_effect = jwt.exceptions.PyJWKClientError
|
||||
with self.assertRaises(Exception):
|
||||
with self.assertRaises(Exception): # noqa: B017
|
||||
validate_jwt_token(
|
||||
jwt_token="aaaaa.bbbbbb.cccccc",
|
||||
jwt_token="aaaaa.bbbbbb.cccccc", # noqa: S106
|
||||
jwks_url="https://mock_jwks_url",
|
||||
issuer="https://mock_issuer",
|
||||
audience="app_id_xxxx",
|
||||
@@ -95,9 +98,9 @@ class TestUtils(unittest.TestCase):
|
||||
|
||||
def test_validate_jwt_token_invalid_token(self, mock_jwt, mock_pyjwkclient):
|
||||
mock_jwt.decode.side_effect = jwt.InvalidTokenError
|
||||
with self.assertRaises(Exception):
|
||||
with self.assertRaises(Exception): # noqa: B017
|
||||
validate_jwt_token(
|
||||
jwt_token="aaaaa.bbbbbb.cccccc",
|
||||
jwt_token="aaaaa.bbbbbb.cccccc", # noqa: S106
|
||||
jwks_url="https://mock_jwks_url",
|
||||
issuer="https://mock_issuer",
|
||||
audience="app_id_xxxx",
|
||||
|
||||
Reference in New Issue
Block a user