mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-24 05:41:40 +00:00
fix(skills-hub): cover remaining SSRF fetch paths after #10029
This commit is contained in:
parent
af9df46525
commit
0c5c4d1b8d
3 changed files with 135 additions and 25 deletions
|
|
@ -560,6 +560,11 @@ class TestFindSkillInRepoTree:
|
|||
|
||||
|
||||
class TestWellKnownSkillSource:
|
||||
@pytest.fixture(autouse=True)
|
||||
def _allow_public_skill_fetches(self, monkeypatch):
|
||||
monkeypatch.setattr("tools.skills_hub.is_safe_url", lambda _url: True)
|
||||
monkeypatch.setattr("tools.skills_hub.check_website_access", lambda _url: None)
|
||||
|
||||
def _source(self):
|
||||
return WellKnownSkillSource()
|
||||
|
||||
|
|
@ -675,6 +680,11 @@ class TestWellKnownSkillSource:
|
|||
|
||||
|
||||
class TestUrlSource:
|
||||
@pytest.fixture(autouse=True)
|
||||
def _allow_public_skill_fetches(self, monkeypatch):
|
||||
monkeypatch.setattr("tools.skills_hub.is_safe_url", lambda _url: True)
|
||||
monkeypatch.setattr("tools.skills_hub.check_website_access", lambda _url: None)
|
||||
|
||||
def _source(self):
|
||||
return UrlSource()
|
||||
|
||||
|
|
@ -753,6 +763,13 @@ class TestUrlSource:
|
|||
mock_get.side_effect = httpx.HTTPError("boom")
|
||||
assert self._source().inspect("https://example.com/SKILL.md") is None
|
||||
|
||||
@patch("tools.skills_hub.httpx.get")
|
||||
@patch("tools.skills_hub.check_website_access", return_value=None)
|
||||
@patch("tools.skills_hub.is_safe_url", return_value=False)
|
||||
def test_inspect_blocks_private_url(self, _mock_safe, _mock_policy, mock_get):
|
||||
assert self._source().inspect("http://127.0.0.1/SKILL.md") is None
|
||||
mock_get.assert_not_called()
|
||||
|
||||
@patch("tools.skills_hub.httpx.get")
|
||||
def test_inspect_flags_awaiting_name_when_unresolvable(self, mock_get):
|
||||
# No frontmatter name + a URL path that can't produce a valid slug
|
||||
|
|
@ -855,6 +872,24 @@ class TestUrlSource:
|
|||
mock_get.return_value = MagicMock(status_code=404)
|
||||
assert self._source().fetch("https://example.com/SKILL.md") is None
|
||||
|
||||
@patch("tools.skills_hub.httpx.get")
|
||||
@patch("tools.skills_hub.check_website_access", return_value=None)
|
||||
@patch("tools.skills_hub.is_safe_url", side_effect=[True, False])
|
||||
def test_fetch_blocks_redirect_to_private_url(self, _mock_safe, _mock_policy, mock_get):
|
||||
redirect = MagicMock(status_code=302)
|
||||
redirect.headers = {"location": "http://127.0.0.1/private/SKILL.md"}
|
||||
mock_get.return_value = redirect
|
||||
|
||||
assert self._source().fetch("https://example.com/SKILL.md") is None
|
||||
assert mock_get.call_count == 1
|
||||
|
||||
@patch("tools.skills_hub.httpx.get")
|
||||
@patch("tools.skills_hub.check_website_access", return_value=None)
|
||||
@patch("tools.skills_hub.is_safe_url", return_value=False)
|
||||
def test_fetch_blocks_private_url(self, _mock_safe, _mock_policy, mock_get):
|
||||
assert self._source().fetch("http://127.0.0.1/SKILL.md") is None
|
||||
mock_get.assert_not_called()
|
||||
|
||||
@patch("tools.skills_hub.httpx.get")
|
||||
def test_fetch_skips_non_matching_identifier(self, mock_get):
|
||||
assert self._source().fetch("owner/repo/skill") is None
|
||||
|
|
|
|||
|
|
@ -7,10 +7,11 @@ from tools.skills_hub import ClawHubSource, SkillMeta
|
|||
|
||||
|
||||
class _MockResponse:
|
||||
def __init__(self, status_code=200, json_data=None, text=""):
|
||||
def __init__(self, status_code=200, json_data=None, text="", headers=None):
|
||||
self.status_code = status_code
|
||||
self._json_data = json_data
|
||||
self.text = text
|
||||
self.headers = headers or {}
|
||||
|
||||
def json(self):
|
||||
return self._json_data
|
||||
|
|
@ -19,6 +20,14 @@ class _MockResponse:
|
|||
class TestClawHubSource(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.src = ClawHubSource()
|
||||
self._safe_patcher = patch("tools.skills_hub.is_safe_url", return_value=True)
|
||||
self._policy_patcher = patch("tools.skills_hub.check_website_access", return_value=None)
|
||||
self._safe_patcher.start()
|
||||
self._policy_patcher.start()
|
||||
|
||||
def tearDown(self):
|
||||
self._policy_patcher.stop()
|
||||
self._safe_patcher.stop()
|
||||
|
||||
@patch("tools.skills_hub._write_index_cache")
|
||||
@patch("tools.skills_hub._read_index_cache", return_value=None)
|
||||
|
|
@ -255,6 +264,40 @@ class TestClawHubSource(unittest.TestCase):
|
|||
self.assertIsNotNone(bundle)
|
||||
self.assertEqual(bundle.files["SKILL.md"], "# Skill")
|
||||
|
||||
@patch("tools.skills_hub.check_website_access", return_value=None)
|
||||
@patch("tools.skills_hub.is_safe_url")
|
||||
@patch("tools.skills_hub.httpx.get")
|
||||
def test_fetch_blocks_private_raw_url(self, mock_get, mock_safe, _mock_policy):
|
||||
def side_effect(url, *args, **kwargs):
|
||||
if url.endswith("/skills/caldav-calendar"):
|
||||
return _MockResponse(
|
||||
status_code=200,
|
||||
json_data={
|
||||
"slug": "caldav-calendar",
|
||||
"latestVersion": {"version": "1.0.1"},
|
||||
},
|
||||
)
|
||||
if url.endswith("/download"):
|
||||
return _MockResponse(status_code=404)
|
||||
if url.endswith("/skills/caldav-calendar/versions/1.0.1"):
|
||||
return _MockResponse(
|
||||
status_code=200,
|
||||
json_data={
|
||||
"files": [
|
||||
{"path": "SKILL.md", "rawUrl": "http://127.0.0.1/private-skill"},
|
||||
]
|
||||
},
|
||||
)
|
||||
return _MockResponse(status_code=404, json_data={})
|
||||
|
||||
mock_get.side_effect = side_effect
|
||||
mock_safe.side_effect = lambda url: not url.startswith("http://127.0.0.1/")
|
||||
|
||||
bundle = self.src.fetch("caldav-calendar")
|
||||
|
||||
self.assertIsNone(bundle)
|
||||
self.assertEqual(mock_get.call_count, 3)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue