mirror of
https://github.com/MadsLorentzen/ai-job-search.git
synced 2026-09-17 00:26:26 +00:00
fix(security): harden guard JSON shape handling (#128)
This commit is contained in:
@@ -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"}}
|
||||||
|
|||||||
@@ -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 "
|
||||||
|
|||||||
Reference in New Issue
Block a user