OpenClaw affected by Stored XSS in Control UI via unsanitized assistant name/avatar in inline script injection
Description
OpenClaw is a personal AI assistant. In versions 2026.2.17 and below, skills/skill-creator/scripts/package_skill.py (a local helper script used when authors package skills) previously followed symlinks while building .skill archives. If an author runs this script on a crafted local skill directory containing symlinks to files outside the skill root, the resulting archive can include unintended file contents. If exploited, this vulnerability can lead to potential unintentional disclosure of local files from the packaging machine into a generated .skill artifact, but requires local execution of the packaging script on attacker-controlled skill contents. This issue has been fixed in version 2026.2.18.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
openclawnpm | < 2026.2.19 | 2026.2.19 |
Affected products
1Patches
2ee1d6427b544fix(security): enforce symlink-safe skill packaging
4 files changed · +71 −257
CHANGELOG.md+1 −0 modified@@ -16,6 +16,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Gateway/WebChat: block `sessions.patch` and `sessions.delete` for WebChat clients so session-store mutations stay restricted to non-WebChat operator flows. Thanks @allsmog for reporting. +- Security/Skills: for the next npm release, reject symlinks during skill packaging to prevent external file inclusion in distributed `.skill` archives. Thanks @aether-ai-agent for reporting. - Security/Feishu: prevent path traversal in Feishu inbound media temp-file writes by replacing key-derived temp filenames with UUID-based names. Thanks @allsmog for reporting. - LINE/Security: harden inbound media temp-file naming by using UUID-based temp paths for downloaded media instead of external message IDs. (#20792) Thanks @mbelinky. - Security/Media: harden local media ingestion against TOCTOU/symlink swap attacks by pinning reads to a single file descriptor with symlink rejection and inode/device verification in `saveMediaSource`. Thanks @dorjoos for reporting.
skills/skill-creator/scripts/package_skill.py+1 −8 modified@@ -69,7 +69,7 @@ def package_skill(skill_path, output_dir=None): with zipfile.ZipFile(skill_filename, "w", zipfile.ZIP_DEFLATED) as zipf: # Walk through the skill directory for file_path in skill_path.rglob("*"): - # Security Check 1: Reject symlinks to prevent supply chain attacks + # Security: never follow or package symlinks. if file_path.is_symlink(): print(f"[ERROR] Symlinks are not allowed in skills: {file_path}") print(" This is a security restriction to prevent including arbitrary files.") @@ -79,13 +79,6 @@ def package_skill(skill_path, output_dir=None): # Calculate the relative path within the zip arcname = file_path.relative_to(skill_path.parent) - # Security Check 2: Validate arcname to prevent Zip Slip attacks - # Ensure the path doesn't escape the skill directory using ".." or absolute paths - if ".." in arcname.parts or arcname.is_absolute(): - print(f"[ERROR] Invalid path in skill (possible Zip Slip attack): {arcname}") - print(" Paths with '..' or absolute paths are not allowed.") - return None - zipf.write(file_path, arcname) print(f" Added: {arcname}")
skills/skill-creator/scripts/test_package_skill.py+67 −249 modified@@ -1,285 +1,103 @@ #!/usr/bin/env python3 """ -Test suite for package_skill.py security fixes. - -Tests for: -1. Symlink detection and rejection -2. Path traversal (Zip Slip) prevention -3. Normal file packaging functionality +Regression tests for skill packaging security behavior. """ -import os import sys import tempfile +import types import zipfile from pathlib import Path from unittest import TestCase, main -# Import the module being tested + +fake_quick_validate = types.ModuleType("quick_validate") +fake_quick_validate.validate_skill = lambda _path: (True, "Skill is valid!") +sys.modules["quick_validate"] = fake_quick_validate + from package_skill import package_skill class TestPackageSkillSecurity(TestCase): - """Test security features of package_skill.py""" - def setUp(self): - """Create temporary directories for testing""" - self.test_dir = tempfile.mkdtemp(prefix="test_skill_") - self.temp_dir = Path(self.test_dir) + self.temp_dir = Path(tempfile.mkdtemp(prefix="test_skill_")) def tearDown(self): - """Clean up test directories""" import shutil + if self.temp_dir.exists(): shutil.rmtree(self.temp_dir) - def create_test_skill(self, name="test-skill"): - """Create a minimal valid skill structure for testing""" + def create_skill(self, name="test-skill"): skill_dir = self.temp_dir / name skill_dir.mkdir(parents=True, exist_ok=True) - - # Create required SKILL.md with YAML frontmatter - skill_md = skill_dir / "SKILL.md" - skill_md.write_text(f"""--- -name: {name} -description: A test skill for security testing ---- - -# Test Skill - -## Description -A test skill for security testing. - -## Usage -Test usage example. -""") - - # Create a manifest file - manifest = skill_dir / "manifest.json" - manifest.write_text('{"name": "test-skill", "version": "1.0.0"}') - - # Create a sample script - script = skill_dir / "script.py" - script.write_text('print("Hello from skill")') - + (skill_dir / "SKILL.md").write_text("---\nname: test-skill\ndescription: test\n---\n") + (skill_dir / "script.py").write_text("print('ok')\n") return skill_dir - def test_normal_file_packaging(self): - """Test that normal files are packaged correctly""" - skill_dir = self.create_test_skill("normal-skill") - output_dir = self.temp_dir / "output" - output_dir.mkdir() - - # Package the skill - result = package_skill(str(skill_dir), str(output_dir)) - - # Verify packaging succeeded - self.assertIsNotNone(result, "Packaging should succeed for normal skills") - skill_file = output_dir / "normal-skill.skill" - self.assertTrue(skill_file.exists(), "Skill file should be created") + def test_packages_normal_files(self): + skill_dir = self.create_skill("normal-skill") + out_dir = self.temp_dir / "out" + out_dir.mkdir() + + result = package_skill(str(skill_dir), str(out_dir)) + + self.assertIsNotNone(result) + skill_file = out_dir / "normal-skill.skill" + self.assertTrue(skill_file.exists()) + with zipfile.ZipFile(skill_file, "r") as archive: + names = set(archive.namelist()) + self.assertIn("normal-skill/SKILL.md", names) + self.assertIn("normal-skill/script.py", names) + + def test_rejects_symlink_to_external_file(self): + skill_dir = self.create_skill("symlink-file-skill") + outside = self.temp_dir / "outside-secret.txt" + outside.write_text("super-secret\n") + link = skill_dir / "loot.txt" + out_dir = self.temp_dir / "out" + out_dir.mkdir() - # Verify zip contents - with zipfile.ZipFile(skill_file, "r") as zipf: - names = zipf.namelist() - self.assertIn("normal-skill/SKILL.md", names) - self.assertIn("normal-skill/manifest.json", names) - self.assertIn("normal-skill/script.py", names) - - def test_symlink_rejection(self): - """Test that symlinks are detected and rejected""" - skill_dir = self.create_test_skill("symlink-skill") - - # Create a target file outside the skill directory - external_file = self.temp_dir / "external_secret.txt" - external_file.write_text("SECRET DATA - Should not be included") - - # Create a symlink inside the skill directory pointing to external file - symlink_path = skill_dir / "secrets" try: - symlink_path.symlink_to(external_file) + link.symlink_to(outside) except (OSError, NotImplementedError): - # Skip test if symlinks are not supported (e.g., Windows without admin) - self.skipTest("Symlinks not supported on this system") - - output_dir = self.temp_dir / "output" - output_dir.mkdir() - - # Attempt to package - should fail - result = package_skill(str(skill_dir), str(output_dir)) - - self.assertIsNone(result, "Packaging should fail when symlinks are present") - - def test_symlink_to_sensitive_file(self): - """Test rejection of symlink pointing to /etc/passwd-like sensitive files""" - skill_dir = self.create_test_skill("sensitive-symlink-skill") - - # Create a mock sensitive file - sensitive_file = self.temp_dir / "mock_passwd" - sensitive_file.write_text("root:x:0:0:root:/root:/bin/bash") - - # Create a symlink inside skill directory - symlink_path = skill_dir / "secrets" / "passwd" - symlink_path.parent.mkdir(parents=True, exist_ok=True) - - try: - symlink_path.symlink_to(sensitive_file) - except (OSError, NotImplementedError): - self.skipTest("Symlinks not supported on this system") - - output_dir = self.temp_dir / "output" - output_dir.mkdir() - - # Should reject due to symlink - result = package_skill(str(skill_dir), str(output_dir)) - - self.assertIsNone(result, "Should reject symlinks to sensitive files") - - def test_zip_slip_prevention(self): - """Test that Zip Slip attacks are prevented""" - skill_dir = self.create_test_skill("zip-slip-skill") - - # Since we can't easily create ".." in filenames through normal Python, - # we test the validation logic by checking if the path validation - # would catch such attempts. - - # Create a subdirectory with test file - subdir = skill_dir / "subdir" - subdir.mkdir() - test_file = subdir / "test.txt" - test_file.write_text("test content") - - output_dir = self.temp_dir / "output" - output_dir.mkdir() - - # Normal packaging should work fine - result = package_skill(str(skill_dir), str(output_dir)) - - # This should succeed - valid structure - self.assertIsNotNone(result, "Normal subdirectories should package successfully") + self.skipTest("symlink unsupported on this platform") - def test_absolute_path_prevention(self): - """Test that absolute paths in arcname are rejected""" - # This is tested indirectly through the validation logic - # The code checks: if ".." in arcname.parts or arcname.is_absolute() + result = package_skill(str(skill_dir), str(out_dir)) + self.assertIsNone(result) - skill_dir = self.create_test_skill("absolute-path-skill") - test_file = skill_dir / "normal.txt" - test_file.write_text("normal file") - - output_dir = self.temp_dir / "output" - output_dir.mkdir() - - # Should succeed with normal files - result = package_skill(str(skill_dir), str(output_dir)) - self.assertIsNotNone(result, "Normal files should package successfully") - - def test_nested_files_allowed(self): - """Test that properly nested files are allowed""" - skill_dir = self.create_test_skill("nested-skill") - - # Create nested directories with files - nested_dir = skill_dir / "lib" / "utils" / "helpers" - nested_dir.mkdir(parents=True, exist_ok=True) - - nested_file = nested_dir / "utility.py" - nested_file.write_text("def helper(): pass") - - output_dir = self.temp_dir / "output" - output_dir.mkdir() - - result = package_skill(str(skill_dir), str(output_dir)) - - self.assertIsNotNone(result, "Nested files should be allowed") - self.assertTrue((output_dir / "nested-skill.skill").exists()) - - # Verify nested file is in zip - with zipfile.ZipFile(output_dir / "nested-skill.skill", "r") as zipf: - names = zipf.namelist() - self.assertIn("nested-skill/lib/utils/helpers/utility.py", names) - - def test_multiple_files_with_symlink_mixed(self): - """Test that one symlink among many files causes entire packaging to fail""" - skill_dir = self.create_test_skill("mixed-skill") - - # Add multiple normal files - for i in range(5): - file = skill_dir / f"file_{i}.txt" - file.write_text(f"content {i}") - - # Add one symlink - external = self.temp_dir / "external.txt" - external.write_text("external") + def test_rejects_symlink_directory(self): + skill_dir = self.create_skill("symlink-dir-skill") + outside_dir = self.temp_dir / "outside" + outside_dir.mkdir() + (outside_dir / "secret.txt").write_text("secret\n") + link = skill_dir / "docs" + out_dir = self.temp_dir / "out" + out_dir.mkdir() try: - (skill_dir / "symlinked").symlink_to(external) + link.symlink_to(outside_dir, target_is_directory=True) except (OSError, NotImplementedError): - self.skipTest("Symlinks not supported on this system") - - output_dir = self.temp_dir / "output" - output_dir.mkdir() - - # Should fail due to symlink - result = package_skill(str(skill_dir), str(output_dir)) - - self.assertIsNone(result, "Packaging should fail if any symlink is present") - - def test_large_skill_with_many_files(self): - """Test packaging a skill with many files (no symlinks or traversal)""" - skill_dir = self.create_test_skill("large-skill") - - # Create many files - for i in range(100): - subdir = skill_dir / f"dir_{i // 10}" - subdir.mkdir(parents=True, exist_ok=True) - file = subdir / f"file_{i}.txt" - file.write_text(f"file {i}") - - output_dir = self.temp_dir / "output" - output_dir.mkdir() - - result = package_skill(str(skill_dir), str(output_dir)) - - self.assertIsNotNone(result, "Large skill should package successfully") - skill_file = output_dir / "large-skill.skill" - - with zipfile.ZipFile(skill_file, "r") as zipf: - # Should have SKILL.md + manifest.json + script.py + 100 files - self.assertGreaterEqual(len(zipf.namelist()), 100) - - -class TestPackageSkillValidation(TestCase): - """Test validation and error handling""" - - def setUp(self): - self.test_dir = tempfile.mkdtemp(prefix="test_validation_") - self.temp_dir = Path(self.test_dir) - - def tearDown(self): - import shutil - if self.temp_dir.exists(): - shutil.rmtree(self.temp_dir) - - def test_missing_skill_directory(self): - """Test error handling for missing skill directory""" - result = package_skill("/nonexistent/skill/path") - self.assertIsNone(result, "Should fail for missing directory") - - def test_file_instead_of_directory(self): - """Test error handling when path is a file, not directory""" - file_path = self.temp_dir / "file.txt" - file_path.write_text("not a directory") - - result = package_skill(str(file_path)) - self.assertIsNone(result, "Should fail when path is a file") - - def test_missing_skill_md(self): - """Test error handling for missing SKILL.md""" - skill_dir = self.temp_dir / "invalid-skill" - skill_dir.mkdir() - (skill_dir / "file.txt").write_text("content") - - result = package_skill(str(skill_dir)) - self.assertIsNone(result, "Should fail without SKILL.md") + self.skipTest("symlink unsupported on this platform") + + result = package_skill(str(skill_dir), str(out_dir)) + self.assertIsNone(result) + + def test_allows_nested_regular_files(self): + skill_dir = self.create_skill("nested-skill") + nested = skill_dir / "lib" / "helpers" + nested.mkdir(parents=True, exist_ok=True) + (nested / "util.py").write_text("def run():\n return 1\n") + out_dir = self.temp_dir / "out" + out_dir.mkdir() + + result = package_skill(str(skill_dir), str(out_dir)) + + self.assertIsNotNone(result) + skill_file = out_dir / "nested-skill.skill" + with zipfile.ZipFile(skill_file, "r") as archive: + names = set(archive.namelist()) + self.assertIn("nested-skill/lib/helpers/util.py", names) if __name__ == "__main__":
skills/skill-creator/SKILL.md+2 −0 modified@@ -356,6 +356,8 @@ The packaging script will: 2. **Package** the skill if validation passes, creating a .skill file named after the skill (e.g., `my-skill.skill`) that includes all files and maintains the proper directory structure for distribution. The .skill file is a zip file with a .skill extension. + Security restriction: symlinks are rejected and packaging fails when any symlink is present. + If validation fails, the script will report the errors and exit without creating a package. Fix any validation errors and run the packaging command again. ### Step 6: Iterate
c275932aa423fix(security): OC-22 prevent Zip Slip and symlink following in skill packaging
2 files changed · +300 −0
skills/skill-creator/scripts/package_skill.py+14 −0 modified@@ -69,9 +69,23 @@ def package_skill(skill_path, output_dir=None): with zipfile.ZipFile(skill_filename, "w", zipfile.ZIP_DEFLATED) as zipf: # Walk through the skill directory for file_path in skill_path.rglob("*"): + # Security Check 1: Reject symlinks to prevent supply chain attacks + if file_path.is_symlink(): + print(f"[ERROR] Symlinks are not allowed in skills: {file_path}") + print(" This is a security restriction to prevent including arbitrary files.") + return None + if file_path.is_file(): # Calculate the relative path within the zip arcname = file_path.relative_to(skill_path.parent) + + # Security Check 2: Validate arcname to prevent Zip Slip attacks + # Ensure the path doesn't escape the skill directory using ".." or absolute paths + if ".." in arcname.parts or arcname.is_absolute(): + print(f"[ERROR] Invalid path in skill (possible Zip Slip attack): {arcname}") + print(" Paths with '..' or absolute paths are not allowed.") + return None + zipf.write(file_path, arcname) print(f" Added: {arcname}")
skills/skill-creator/scripts/test_package_skill.py+286 −0 added@@ -0,0 +1,286 @@ +#!/usr/bin/env python3 +""" +Test suite for package_skill.py security fixes. + +Tests for: +1. Symlink detection and rejection +2. Path traversal (Zip Slip) prevention +3. Normal file packaging functionality +""" + +import os +import sys +import tempfile +import zipfile +from pathlib import Path +from unittest import TestCase, main + +# Import the module being tested +from package_skill import package_skill + + +class TestPackageSkillSecurity(TestCase): + """Test security features of package_skill.py""" + + def setUp(self): + """Create temporary directories for testing""" + self.test_dir = tempfile.mkdtemp(prefix="test_skill_") + self.temp_dir = Path(self.test_dir) + + def tearDown(self): + """Clean up test directories""" + import shutil + if self.temp_dir.exists(): + shutil.rmtree(self.temp_dir) + + def create_test_skill(self, name="test-skill"): + """Create a minimal valid skill structure for testing""" + skill_dir = self.temp_dir / name + skill_dir.mkdir(parents=True, exist_ok=True) + + # Create required SKILL.md with YAML frontmatter + skill_md = skill_dir / "SKILL.md" + skill_md.write_text(f"""--- +name: {name} +description: A test skill for security testing +--- + +# Test Skill + +## Description +A test skill for security testing. + +## Usage +Test usage example. +""") + + # Create a manifest file + manifest = skill_dir / "manifest.json" + manifest.write_text('{"name": "test-skill", "version": "1.0.0"}') + + # Create a sample script + script = skill_dir / "script.py" + script.write_text('print("Hello from skill")') + + return skill_dir + + def test_normal_file_packaging(self): + """Test that normal files are packaged correctly""" + skill_dir = self.create_test_skill("normal-skill") + output_dir = self.temp_dir / "output" + output_dir.mkdir() + + # Package the skill + result = package_skill(str(skill_dir), str(output_dir)) + + # Verify packaging succeeded + self.assertIsNotNone(result, "Packaging should succeed for normal skills") + skill_file = output_dir / "normal-skill.skill" + self.assertTrue(skill_file.exists(), "Skill file should be created") + + # Verify zip contents + with zipfile.ZipFile(skill_file, "r") as zipf: + names = zipf.namelist() + self.assertIn("normal-skill/SKILL.md", names) + self.assertIn("normal-skill/manifest.json", names) + self.assertIn("normal-skill/script.py", names) + + def test_symlink_rejection(self): + """Test that symlinks are detected and rejected""" + skill_dir = self.create_test_skill("symlink-skill") + + # Create a target file outside the skill directory + external_file = self.temp_dir / "external_secret.txt" + external_file.write_text("SECRET DATA - Should not be included") + + # Create a symlink inside the skill directory pointing to external file + symlink_path = skill_dir / "secrets" + try: + symlink_path.symlink_to(external_file) + except (OSError, NotImplementedError): + # Skip test if symlinks are not supported (e.g., Windows without admin) + self.skipTest("Symlinks not supported on this system") + + output_dir = self.temp_dir / "output" + output_dir.mkdir() + + # Attempt to package - should fail + result = package_skill(str(skill_dir), str(output_dir)) + + self.assertIsNone(result, "Packaging should fail when symlinks are present") + + def test_symlink_to_sensitive_file(self): + """Test rejection of symlink pointing to /etc/passwd-like sensitive files""" + skill_dir = self.create_test_skill("sensitive-symlink-skill") + + # Create a mock sensitive file + sensitive_file = self.temp_dir / "mock_passwd" + sensitive_file.write_text("root:x:0:0:root:/root:/bin/bash") + + # Create a symlink inside skill directory + symlink_path = skill_dir / "secrets" / "passwd" + symlink_path.parent.mkdir(parents=True, exist_ok=True) + + try: + symlink_path.symlink_to(sensitive_file) + except (OSError, NotImplementedError): + self.skipTest("Symlinks not supported on this system") + + output_dir = self.temp_dir / "output" + output_dir.mkdir() + + # Should reject due to symlink + result = package_skill(str(skill_dir), str(output_dir)) + + self.assertIsNone(result, "Should reject symlinks to sensitive files") + + def test_zip_slip_prevention(self): + """Test that Zip Slip attacks are prevented""" + skill_dir = self.create_test_skill("zip-slip-skill") + + # Since we can't easily create ".." in filenames through normal Python, + # we test the validation logic by checking if the path validation + # would catch such attempts. + + # Create a subdirectory with test file + subdir = skill_dir / "subdir" + subdir.mkdir() + test_file = subdir / "test.txt" + test_file.write_text("test content") + + output_dir = self.temp_dir / "output" + output_dir.mkdir() + + # Normal packaging should work fine + result = package_skill(str(skill_dir), str(output_dir)) + + # This should succeed - valid structure + self.assertIsNotNone(result, "Normal subdirectories should package successfully") + + def test_absolute_path_prevention(self): + """Test that absolute paths in arcname are rejected""" + # This is tested indirectly through the validation logic + # The code checks: if ".." in arcname.parts or arcname.is_absolute() + + skill_dir = self.create_test_skill("absolute-path-skill") + test_file = skill_dir / "normal.txt" + test_file.write_text("normal file") + + output_dir = self.temp_dir / "output" + output_dir.mkdir() + + # Should succeed with normal files + result = package_skill(str(skill_dir), str(output_dir)) + self.assertIsNotNone(result, "Normal files should package successfully") + + def test_nested_files_allowed(self): + """Test that properly nested files are allowed""" + skill_dir = self.create_test_skill("nested-skill") + + # Create nested directories with files + nested_dir = skill_dir / "lib" / "utils" / "helpers" + nested_dir.mkdir(parents=True, exist_ok=True) + + nested_file = nested_dir / "utility.py" + nested_file.write_text("def helper(): pass") + + output_dir = self.temp_dir / "output" + output_dir.mkdir() + + result = package_skill(str(skill_dir), str(output_dir)) + + self.assertIsNotNone(result, "Nested files should be allowed") + self.assertTrue((output_dir / "nested-skill.skill").exists()) + + # Verify nested file is in zip + with zipfile.ZipFile(output_dir / "nested-skill.skill", "r") as zipf: + names = zipf.namelist() + self.assertIn("nested-skill/lib/utils/helpers/utility.py", names) + + def test_multiple_files_with_symlink_mixed(self): + """Test that one symlink among many files causes entire packaging to fail""" + skill_dir = self.create_test_skill("mixed-skill") + + # Add multiple normal files + for i in range(5): + file = skill_dir / f"file_{i}.txt" + file.write_text(f"content {i}") + + # Add one symlink + external = self.temp_dir / "external.txt" + external.write_text("external") + + try: + (skill_dir / "symlinked").symlink_to(external) + except (OSError, NotImplementedError): + self.skipTest("Symlinks not supported on this system") + + output_dir = self.temp_dir / "output" + output_dir.mkdir() + + # Should fail due to symlink + result = package_skill(str(skill_dir), str(output_dir)) + + self.assertIsNone(result, "Packaging should fail if any symlink is present") + + def test_large_skill_with_many_files(self): + """Test packaging a skill with many files (no symlinks or traversal)""" + skill_dir = self.create_test_skill("large-skill") + + # Create many files + for i in range(100): + subdir = skill_dir / f"dir_{i // 10}" + subdir.mkdir(parents=True, exist_ok=True) + file = subdir / f"file_{i}.txt" + file.write_text(f"file {i}") + + output_dir = self.temp_dir / "output" + output_dir.mkdir() + + result = package_skill(str(skill_dir), str(output_dir)) + + self.assertIsNotNone(result, "Large skill should package successfully") + skill_file = output_dir / "large-skill.skill" + + with zipfile.ZipFile(skill_file, "r") as zipf: + # Should have SKILL.md + manifest.json + script.py + 100 files + self.assertGreaterEqual(len(zipf.namelist()), 100) + + +class TestPackageSkillValidation(TestCase): + """Test validation and error handling""" + + def setUp(self): + self.test_dir = tempfile.mkdtemp(prefix="test_validation_") + self.temp_dir = Path(self.test_dir) + + def tearDown(self): + import shutil + if self.temp_dir.exists(): + shutil.rmtree(self.temp_dir) + + def test_missing_skill_directory(self): + """Test error handling for missing skill directory""" + result = package_skill("/nonexistent/skill/path") + self.assertIsNone(result, "Should fail for missing directory") + + def test_file_instead_of_directory(self): + """Test error handling when path is a file, not directory""" + file_path = self.temp_dir / "file.txt" + file_path.write_text("not a directory") + + result = package_skill(str(file_path)) + self.assertIsNone(result, "Should fail when path is a file") + + def test_missing_skill_md(self): + """Test error handling for missing SKILL.md""" + skill_dir = self.temp_dir / "invalid-skill" + skill_dir.mkdir() + (skill_dir / "file.txt").write_text("content") + + result = package_skill(str(skill_dir)) + self.assertIsNone(result, "Should fail without SKILL.md") + + +if __name__ == "__main__": + main()
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
7- github.com/advisories/GHSA-r6h2-5gqq-v5v6ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-27485ghsaADVISORY
- github.com/openclaw/openclaw/commit/c275932aa4230fb7a8212fe1b9d2a18424874b3fghsax_refsource_MISCWEB
- github.com/openclaw/openclaw/commit/ee1d6427b544ccadd73e02b1630ea5c29ba9a9f0ghsax_refsource_MISCWEB
- github.com/openclaw/openclaw/pull/20796ghsax_refsource_MISCWEB
- github.com/openclaw/openclaw/releases/tag/v2026.2.19ghsax_refsource_MISCWEB
- github.com/openclaw/openclaw/security/advisories/GHSA-r6h2-5gqq-v5v6ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.