fix(security): harden bundle and plugin validation
This commit is contained in:
@@ -106,6 +106,30 @@ class EditorialBundlesTests(unittest.TestCase):
|
||||
)
|
||||
self.assertEqual(generated_plugins, expected_plugins)
|
||||
|
||||
def test_manifest_rejects_bundle_ids_with_path_traversal(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
temp_root = pathlib.Path(temp_dir)
|
||||
skill_dir = temp_root / "skills" / "safe-skill"
|
||||
skill_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
payload = {
|
||||
"bundles": [
|
||||
{
|
||||
"id": "../../outside",
|
||||
"name": "Safe Bundle",
|
||||
"group": "Security",
|
||||
"emoji": "🛡️",
|
||||
"tagline": "Test bundle",
|
||||
"audience": "Testers",
|
||||
"description": "Testers",
|
||||
"skills": [{"id": "safe-skill", "summary": "ok"}],
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
with self.assertRaisesRegex(ValueError, "Invalid editorial bundle id"):
|
||||
editorial_bundles._validate_editorial_bundles(temp_root, payload)
|
||||
|
||||
def test_sample_bundle_copy_matches_source_file_inventory(self):
|
||||
sample_bundle = next(bundle for bundle in self.manifest_bundles if bundle["id"] == "documents-presentations")
|
||||
plugin_skills_root = REPO_ROOT / "plugins" / "antigravity-bundle-documents-presentations" / "skills"
|
||||
|
||||
@@ -21,6 +21,10 @@ def load_module(relative_path: str, module_name: str):
|
||||
|
||||
|
||||
generate_index = load_module("tools/scripts/generate_index.py", "generate_index")
|
||||
legacy_generate_index = load_module(
|
||||
"skill_categorization/tools/scripts/generate_index.py",
|
||||
"legacy_generate_index",
|
||||
)
|
||||
validate_skills = load_module("tools/scripts/validate_skills.py", "validate_skills")
|
||||
|
||||
|
||||
@@ -80,6 +84,34 @@ class FrontmatterParsingSecurityTests(unittest.TestCase):
|
||||
self.assertEqual(skills[0]["date_added"], "2026-03-15")
|
||||
self.assertIn('"date_added": "2026-03-15"', output_file.read_text(encoding="utf-8"))
|
||||
|
||||
def test_generate_index_normalizes_binary_frontmatter_scalars(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
root = Path(temp_dir)
|
||||
skills_dir = root / "skills"
|
||||
skill_dir = skills_dir / "demo"
|
||||
output_file = root / "skills_index.json"
|
||||
|
||||
skill_dir.mkdir(parents=True)
|
||||
(skill_dir / "SKILL.md").write_text(
|
||||
"---\nname: !!binary aGVsbG8=\ndescription: !!binary d29ybGQ=\n---\nBody\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
skills = generate_index.generate_index(str(skills_dir), str(output_file))
|
||||
|
||||
self.assertEqual(skills[0]["name"], "hello")
|
||||
self.assertEqual(skills[0]["description"], "world")
|
||||
self.assertIn('"name": "hello"', output_file.read_text(encoding="utf-8"))
|
||||
|
||||
def test_legacy_generate_index_rejects_non_mapping_and_normalizes_binary_scalars(self):
|
||||
self.assertEqual(legacy_generate_index.parse_frontmatter("---\njust-a-string\n---\nbody\n"), {})
|
||||
metadata = legacy_generate_index.parse_frontmatter(
|
||||
"---\nname: !!binary aGVsbG8=\ndescription: !!binary d29ybGQ=\n---\nbody\n"
|
||||
)
|
||||
|
||||
self.assertEqual(metadata["name"], "hello")
|
||||
self.assertEqual(metadata["description"], "world")
|
||||
|
||||
def test_generate_index_ignores_symlinked_skill_markdown(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
root = Path(temp_dir)
|
||||
|
||||
@@ -78,6 +78,30 @@ class PluginCompatibilityTests(unittest.TestCase):
|
||||
self.assertEqual(entry["targets"]["claude"], "blocked")
|
||||
self.assertIn("undeclared_runtime_dependency", entry["reasons"])
|
||||
|
||||
def test_relative_links_cannot_escape_skill_directory(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
skills_dir = pathlib.Path(temp_dir) / "skills"
|
||||
self._write_skill(
|
||||
skills_dir,
|
||||
"escaping-link-skill",
|
||||
(
|
||||
"---\n"
|
||||
"name: escaping-link-skill\n"
|
||||
"description: Example\n"
|
||||
"---\n"
|
||||
"Read [secret](../../outside/secret.txt)\n"
|
||||
),
|
||||
)
|
||||
outside_dir = pathlib.Path(temp_dir) / "outside"
|
||||
outside_dir.mkdir(parents=True, exist_ok=True)
|
||||
(outside_dir / "secret.txt").write_text("secret", encoding="utf-8")
|
||||
|
||||
report = plugin_compatibility.build_report(skills_dir)
|
||||
entry = report["skills"][0]
|
||||
self.assertEqual(entry["targets"]["codex"], "blocked")
|
||||
self.assertEqual(entry["targets"]["claude"], "blocked")
|
||||
self.assertIn("escaped_local_reference", entry["reasons"])
|
||||
|
||||
def test_manual_setup_metadata_can_make_runtime_skill_supported(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
skills_dir = pathlib.Path(temp_dir) / "skills"
|
||||
|
||||
Reference in New Issue
Block a user