Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
from crewai.tools import BaseTool
from pydantic import BaseModel, Field, field_validator

from crewai_tools.security.safe_path import validate_url


class FixedSeleniumScrapingToolSchema(BaseModel):
"""Input for SeleniumScrapingTool."""
Expand Down Expand Up @@ -45,7 +47,9 @@ def validate_website_url(cls, v: str) -> str:
if re.search(r"\s", v):
raise ValueError("URL cannot contain whitespace")

return v
# Align with Firecrawl/Scrapfly: block private/reserved SSRF targets
# before the local Chrome WebDriver navigates to the URL.
return validate_url(v)


class SeleniumScrapingTool(BaseTool):
Expand Down Expand Up @@ -121,9 +125,9 @@ def __init__(
self.css_element = css_element

if website_url is not None:
self.website_url = website_url
self.website_url = validate_url(website_url)
self.description = (
f"A tool that can be used to read {website_url}'s content."
f"A tool that can be used to read {self.website_url}'s content."
Comment on lines +128 to +130

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate files =="
git ls-files | rg 'selenium_scraping_tool\.py$|selenium_scraping_tool_test\.py$' || true

echo
echo "== production file outline =="
ast-grep outline lib/crewai-tools/src/crewai_tools/tools/selenium_scraping_tool/selenium_scraping_tool.py --view expanded | sed -n '1,220p' || true

echo
echo "== target production lines =="
cat -n lib/crewai-tools/src/crewai_tools/tools/selenium_scraping_tool/selenium_scraping_tool.py | sed -n '1,190p'

echo
echo "== target test lines =="
cat -n lib/crewai-tools/tests/tools/selenium_scraping_tool_test.py | sed -n '130,180p'

echo
echo "== search validate_url and __init__ usage =="
rg -n "def __init__|webdriver\.Chrome|validate_url|def validate_url|private/reserved|localhost" lib/crewai-tools/src/crewai-tools/tools/lib/crewai-tools -g '*.py' || true

Repository: crewAIInc/crewAI

Length of output: 11238


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== validate_url definitions =="
git ls-files | rg 'safe_path|security' | xargs -r rg -n "def validate_url|is_private|localhost|private/reserved|validate_url" -g '*.py' || true

echo
echo "== imports of safe_path == rg =="
rg -n "from .*safe_path import|import .*safe_path|validate_url\(" lib/crewai-tools/src/crewai-tools -g '*.py' || true

echo
echo "== constructor ordering verifier =="
python3 - <<'PY'
from pathlib import Path
src = Path('lib/crewai-tools/src/crewai_tools/tools/selenium_scraping_tool/selenium_scraping_tool.py')
text = src.read_text()

webdriver_line = next(
    (i for i, line in enumerate(text.splitlines(), 1) if 'webdriver.Chrome(' in line),
    None
)
validate_init_line = next(
    (i for i, line in enumerate(text.splitlines(), 1) if 'validate_url(website_url)' in line),
    None
)

print(f"webdriver.Chrome line: {webdriver_line}")
print(f"validate_url(website_url) line: {validate_init_line}")
print(f"constructor validation runs after Chrome initialization: {validate_init_line is not None and webdriver_line is not None and validate_init_line > webdriver_line}")

pytest = Path('lib/crewai-tools/tests/tools/selenium_scraping_tool_test.py')
test = pytest.read_text().splitlines()
start = next((i for i, line in enumerate(test, 1) if 'test_fixed_url_constructor_rejects_private_targets' in line), None)
print(f"test start line: {start}")
if start:
    snippet = '\n'.join(test[start-1:start+12])
    print("contains mocked_chrome.assert_not_called():", 'mocked_chrome.assert_not_called()' in snippet)
PY

Repository: crewAIInc/crewAI

Length of output: 50372


Validate fixed URLs before creating the Selenium WebDriver.

SeleniumScrapingTool.__init__ creates webdriver.Chrome before validate_url(website_url) runs for a fixed URL. Move the validation before driver initialization, or close the driver when validation fails. The fixed-URL constructor regression test should also assert mocked_chrome.assert_not_called() after confirming the ValueError.

📍 Affects 2 files
  • lib/crewai-tools/src/crewai_tools/tools/selenium_scraping_tool/selenium_scraping_tool.py#L128-L130 (this comment)
  • lib/crewai-tools/tests/tools/selenium_scraping_tool_test.py#L156-L165
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@lib/crewai-tools/src/crewai_tools/tools/selenium_scraping_tool/selenium_scraping_tool.py`
around lines 128 - 130, Update SeleniumScrapingTool.__init__ so fixed website
URLs are validated before webdriver.Chrome is initialized, preventing driver
creation when validate_url raises ValueError. In
lib/crewai-tools/tests/tools/selenium_scraping_tool_test.py lines 156-165,
extend the fixed-URL constructor regression test to assert
mocked_chrome.assert_not_called() after confirming the ValueError.

)
self.args_schema = FixedSeleniumScrapingToolSchema

Expand Down Expand Up @@ -195,6 +199,9 @@ def _make_request(
if not re.match(r"^https?://", url):
raise ValueError("URL must start with http:// or https://")

# Defense in depth for the fixed-URL schema path (no pydantic re-check).
url = validate_url(url)

Comment on lines +202 to +204

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate file =="
git ls-files | rg 'selenium_scraping_tool\.py$|selenium' || true

echo "== target outline =="
ast-grep outline lib/crewai-tools/src/crewai_tools/tools/selenium_scraping_tool/selenium_scraping_tool.py --view expanded || true

echo "== relevant source =="
sed -n '1,260p' lib/crewai-tools/src/crewai_tools/tools/selenium_scraping_tool/selenium_scraping_tool.py

echo "== validate_url definitions/usages =="
rg -n "def validate_url|validate_url|SeleniumBase|Crawler|driver\.get|driver\.add_" lib/crewai-tools/src/crewai_tools/tools/selenium_scraping_tool -S

Repository: crewAIInc/crewAI

Length of output: 17379


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== safe_path outline =="
ast-grep outline lib/crewai-tools/src/crewai_tools/security/safe_path.py --view expanded || true

echo "== safe_path source =="
sed -n '1,220p' lib//crewai_tools/src/crewai_tools/security/safe_path.py || true

echo "== validate_url search =="
rg -rn "def validate_url|resolve_hostname|ipaddress|ALLOWED|BLOCK|private|reserved|safe_path" lib/crewai-tools/src/crewai_tools -S

Repository: crewAIInc/crewAI

Length of output: 9401


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== safe_path exact source =="
sed -n '140,210p' lib/crewai-tools/src/crewai_tools/security/safe_path.py

echo "== selenium source around driver navigation =="
sed -n '193,215p' lib/crewai-tools/src/crewai_tools/tools/selenium_scraping_tool/selenium_scraping_tool.py

Repository: crewAIInc/crewAI

Length of output: 3379


Enforce the SSRF policy at Chrome egress.

validate_url(url) before driver.get(url) only checks the initial URL once. Chrome can still follow redirects and load page subresources outside Python’s reach, including DNS-rebinding targets. Apply an outbound SSRF policy on the local WebDriver host or intercept/validate every browser request.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@lib/crewai-tools/src/crewai_tools/tools/selenium_scraping_tool/selenium_scraping_tool.py`
around lines 202 - 204, Update the Selenium browser flow around validate_url and
driver.get to enforce SSRF checks at Chrome egress, not only on the initial URL.
Configure the local WebDriver host or request interception to validate every
navigation, redirect, and subresource destination against the existing SSRF
policy, including DNS-rebinding targets, before allowing the request.

if self.driver is None:
raise RuntimeError("Driver not initialized. Call _run first.")
sleep_time = wait_time or 0
Expand Down
34 changes: 34 additions & 0 deletions lib/crewai-tools/tests/tools/selenium_scraping_tool_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,37 @@ def test_initialization_with_driver(_mocked_chrome_driver):
mock_driver = MagicMock()
tool = initialize_tool_with(mock_driver)
assert tool.driver == mock_driver


@patch("selenium.webdriver.Chrome")
def test_rejects_loopback_ssrf_targets(_mocked_chrome_driver):
mock_driver = mock_driver_with_html("<html><body>nope</body></html>")
tool = initialize_tool_with(mock_driver)

result = tool._run(website_url="http://127.0.0.1/admin")

assert "private/reserved" in result.lower() or "Error scraping website" in result
mock_driver.get.assert_not_called()


@patch("selenium.webdriver.Chrome")
def test_rejects_cloud_metadata_ssrf_targets(_mocked_chrome_driver):
mock_driver = mock_driver_with_html("<html><body>nope</body></html>")
tool = initialize_tool_with(mock_driver)

result = tool._run(website_url="http://169.254.169.254/latest/meta-data/")

assert "private/reserved" in result.lower() or "Error scraping website" in result
mock_driver.get.assert_not_called()


@patch("selenium.webdriver.Chrome")
def test_fixed_url_constructor_rejects_private_targets(mocked_chrome):
mocked_chrome.return_value = MagicMock()
try:
SeleniumScrapingTool(website_url="http://localhost/internal")
raised = False
except ValueError as exc:
raised = True
assert "private/reserved" in str(exc).lower() or "localhost" in str(exc).lower()
assert raised