fix(security): harden guard JSON shape handling (#128)

This commit is contained in:
Ayobami Adegoke
2026-07-11 22:04:54 +02:00
committed by GitHub
parent 543d1a733e
commit 569b1df371
2 changed files with 45 additions and 2 deletions
+26
View File
@@ -92,6 +92,20 @@ class PermissionGuardTests(GuardRepoFixture):
self.assertEqual(result.returncode, 1) self.assertEqual(result.returncode, 1)
self.assertIn("invalid JSON", result.stdout) self.assertIn("invalid JSON", result.stdout)
def test_malformed_settings_shape_fails_cleanly(self):
for data, message in [
([], "top-level JSON value must be an object"),
({"permissions": []}, "permissions must be an object"),
({"permissions": {"allow": "Bash(*)"}}, "permissions.allow must be a list of strings"),
({"permissions": {"allow": [1]}}, "permissions.allow must be a list of strings"),
]:
with self.subTest(data=data):
self.settings.write_text(json.dumps(data))
result = run_guards(self.root)
self.assertEqual(result.returncode, 1)
self.assertIn(message, result.stdout)
self.assertNotIn("Traceback", result.stderr)
class GitignoreGuardTests(GuardRepoFixture): class GitignoreGuardTests(GuardRepoFixture):
def test_each_missing_personal_data_rule_fails(self): def test_each_missing_personal_data_rule_fails(self):
@@ -134,6 +148,18 @@ class ManifestGuardTests(GuardRepoFixture):
self.assertEqual(result.returncode, 1) self.assertEqual(result.returncode, 1)
self.assertIn("trustedDependencies", result.stdout) self.assertIn("trustedDependencies", result.stdout)
def test_malformed_manifest_shape_fails_cleanly(self):
for data, message in [
([], "top-level JSON value must be an object"),
({"name": "example-cli", "scripts": []}, "scripts must be an object"),
]:
with self.subTest(data=data):
self.write_manifest(data)
result = run_guards(self.root)
self.assertEqual(result.returncode, 1)
self.assertIn(message, result.stdout)
self.assertNotIn("Traceback", result.stderr)
def test_benign_scripts_pass(self): def test_benign_scripts_pass(self):
self.write_manifest( self.write_manifest(
{"name": "example-cli", "scripts": {"start": "bun run src/cli.ts", "test": "bun test", "typecheck": "tsc --noEmit"}} {"name": "example-cli", "scripts": {"start": "bun run src/cli.ts", "test": "bun test", "typecheck": "tsc --noEmit"}}
+19 -2
View File
@@ -65,7 +65,17 @@ def check_permissions() -> None:
except (OSError, json.JSONDecodeError) as exc: except (OSError, json.JSONDecodeError) as exc:
errors.append(f".claude/settings.json: unreadable or invalid JSON: {exc}") errors.append(f".claude/settings.json: unreadable or invalid JSON: {exc}")
return return
allow = data.get("permissions", {}).get("allow", []) if not isinstance(data, dict):
errors.append(".claude/settings.json: top-level JSON value must be an object")
return
permissions = data.get("permissions", {})
if not isinstance(permissions, dict):
errors.append(".claude/settings.json: permissions must be an object")
return
allow = permissions.get("allow", [])
if not isinstance(allow, list) or not all(isinstance(entry, str) for entry in allow):
errors.append(".claude/settings.json: permissions.allow must be a list of strings")
return
for entry in allow: for entry in allow:
if entry not in ALLOWED_PERMISSIONS: if entry not in ALLOWED_PERMISSIONS:
errors.append( errors.append(
@@ -110,7 +120,14 @@ def check_package_manifests() -> None:
except (OSError, json.JSONDecodeError) as exc: except (OSError, json.JSONDecodeError) as exc:
errors.append(f"{relpath}: unreadable or invalid JSON: {exc}") errors.append(f"{relpath}: unreadable or invalid JSON: {exc}")
continue continue
bad = FORBIDDEN_SCRIPTS & set(data.get("scripts", {})) if not isinstance(data, dict):
errors.append(f"{relpath}: top-level JSON value must be an object")
continue
scripts = data.get("scripts", {})
if not isinstance(scripts, dict):
errors.append(f"{relpath}: scripts must be an object")
continue
bad = FORBIDDEN_SCRIPTS & set(scripts)
if bad: if bad:
errors.append( errors.append(
f"{relpath}: lifecycle script(s) {sorted(bad)} are forbidden - they execute " f"{relpath}: lifecycle script(s) {sorted(bad)} are forbidden - they execute "