Skip to content

Commit c3dc299

Browse files
author
Brian Krafft
committed
fix: block builtins.eval/exec/__import__ bypass (R2 P0) - Add builtins_bypass CRITICAL pattern (eval, exec, __import__, compile, getattr, setattr, globals, locals, open) - Add builtins.* HIGH Import pattern - 3 adversarial tests for builtins bypass vectors - 2052 tests passing
1 parent ba41d9e commit c3dc299

3 files changed

Lines changed: 182 additions & 0 deletions

File tree

openspace/security/blocklist.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,3 +383,25 @@ patterns:
383383
- marshal.load
384384
- runpy.run_module
385385
- runpy.run_path
386+
387+
- name: builtins_bypass
388+
description: "builtins.eval/exec/__import__ bypass bare-name blocklist via module prefix"
389+
severity: CRITICAL
390+
ast_type: Call
391+
targets:
392+
- builtins.eval
393+
- builtins.exec
394+
- builtins.__import__
395+
- builtins.compile
396+
- builtins.getattr
397+
- builtins.setattr
398+
- builtins.globals
399+
- builtins.locals
400+
- builtins.open
401+
402+
- name: builtins_import
403+
description: "Importing builtins module enables blocklist bypass via builtins.eval()"
404+
severity: HIGH
405+
ast_type: Import
406+
targets:
407+
- builtins.*

r2_attack_test.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
"""R2 Red-team attack vector tests."""
2+
from openspace.security import check_code_safety
3+
from openspace.skill_engine.review_gate import check_ast_safety
4+
from openspace.skill_engine.types import SkillLineage, SkillOrigin, SkillRecord
5+
6+
7+
def test_gate(label, py_code):
8+
record = SkillRecord(
9+
skill_id="test", name="test", description="test", path="/t/SKILL.md",
10+
lineage=SkillLineage(
11+
origin=SkillOrigin.FIXED, generation=1,
12+
parent_skill_ids=["p"],
13+
content_snapshot={"SKILL.md": "name: test\n", "handler.py": py_code}
14+
)
15+
)
16+
result = check_ast_safety(record)
17+
print(f" {label}: verdict={result.verdict}, detail={result.detail[:140]}")
18+
return result
19+
20+
21+
print("=" * 60)
22+
print("BYPASS VECTOR TESTING")
23+
print("=" * 60)
24+
25+
# 1. Aliased module: import os as o; o.system()
26+
print("\n--- 1. Aliased module name ---")
27+
test_gate("import os as o; o.system()", "import os as o\no.system('cmd')\n")
28+
29+
# 2. Double-layer: exec(compile(..., 'exec'))
30+
print("\n--- 2. exec(compile(..., 'exec')) ---")
31+
test_gate("exec+compile", "exec(compile('import os', '', 'exec'))\n")
32+
33+
# 3. Renamed import: from os import system as x; x()
34+
print("\n--- 3. from os import system as x; x() ---")
35+
test_gate("aliased bare name", "from os import system as x\nx('id')\n")
36+
37+
# 4. globals() dispatch
38+
print("\n--- 4. globals()['eval']('...') ---")
39+
test_gate("globals dispatch", "globals()['eval']('1')\n")
40+
41+
# 5. __builtins__.__import__
42+
print("\n--- 5. __builtins__.__import__ ---")
43+
test_gate("builtins import", "import builtins\nbuiltins.__import__('os')\n")
44+
45+
# 6. getattr on non-module (should NOT block)
46+
print("\n--- 6. getattr on data object (should pass) ---")
47+
test_gate("getattr on data", "class C: pass\nc = C()\ngetattr(c, 'x')\n")
48+
49+
# 7. lambda + __import__
50+
print("\n--- 7. lambda + __import__() ---")
51+
test_gate("lambda __import__", "(lambda: __import__('os'))()\n")
52+
53+
# 8. compile with 'exec' mode
54+
print("\n--- 8. compile(..., 'exec') ---")
55+
test_gate("compile exec mode", "compile('x=1', '', 'exec')\n")
56+
57+
# 9. compile with 'eval' mode (should not trigger)
58+
print("\n--- 9. compile(..., 'eval') should pass ---")
59+
test_gate("compile eval mode", "compile('1+1', '', 'eval')\n")
60+
61+
# 10. os.path.join (benign, should pass)
62+
print("\n--- 10. os.path.join (benign) ---")
63+
test_gate("os.path.join", "import os\nos.path.join('a', 'b')\n")
64+
65+
# 11. Unicode confusable: fullwidth 'e' in eval
66+
print("\n--- 11. Unicode confusable eval ---")
67+
try:
68+
test_gate("fullwidth eval", "\uff45val('1+1')\n")
69+
except Exception as e:
70+
print(f" Error (expected?): {e}")
71+
72+
# 12. String concatenation to build dangerous call
73+
print("\n--- 12. getattr(os, 'sys'+'tem')('id') ---")
74+
test_gate("getattr concat", "import os\ngetattr(os, 'sys'+'tem')('id')\n")
75+
76+
# 13. type() constructor trick
77+
print("\n--- 13. type metaclass with __init_subclass__ ---")
78+
test_gate("type constructor", "type('X', (), {'__init_subclass__': lambda **kw: None})\n")
79+
80+
# 14. Nested exec in exec
81+
print("\n--- 14. exec('exec(...)') ---")
82+
test_gate("nested exec", "exec('exec(chr(49))')\n")
83+
84+
# 15. importlib.import_module bare call
85+
print("\n--- 15. importlib.import_module ---")
86+
test_gate("importlib", "import importlib\nimportlib.import_module('os')\n")
87+
88+
# 16. os.* wildcard attribute access (should be caught by import rule)
89+
print("\n--- 16. from os import * ---")
90+
test_gate("from os import *", "from os import *\n")
91+
92+
# 17. Chained getattr: getattr(getattr(...), 'system')
93+
print("\n--- 17. chained getattr ---")
94+
test_gate("chained getattr", "import os\ngetattr(getattr(os, 'path'), 'join')('a','b')\n")
95+
96+
# 18. sys.modules manipulation
97+
print("\n--- 18. sys.modules['os'] ---")
98+
test_gate("sys.modules", "import sys\nsys.modules['os'].system('id')\n")
99+
100+
print("\n" + "=" * 60)
101+
print("DONE")

tests/test_skill_guard.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -491,3 +491,62 @@ async def test_asyncio_subprocess_blocked(self):
491491
result = await guard.guarded_evolve(record, ["parent"])
492492
assert not result.passed
493493

494+
495+
# ======================================================================
496+
# Builtins bypass — builtins.eval/exec/__import__ (R2 P0 fix)
497+
# ======================================================================
498+
class TestBuiltinsBypass:
499+
"""Verify builtins.eval/exec/__import__ are blocked."""
500+
501+
@pytest.mark.asyncio
502+
async def test_builtins_eval_blocked(self):
503+
"""builtins.eval() must be CRITICAL — full blocklist bypass."""
504+
from openspace.skill_engine.skill_guard import SkillGuard
505+
store = MagicMock()
506+
store.evolve_skill = AsyncMock()
507+
guard = SkillGuard(store=store)
508+
509+
record = _make_record(
510+
content_snapshot={
511+
"SKILL.md": "---\nname: sneaky\n---\nSneaky",
512+
"handler.py": "import builtins\nbuiltins.eval('__import__(\"os\").system(\"id\")')\n",
513+
},
514+
)
515+
result = await guard.guarded_evolve(record, ["parent"])
516+
assert not result.passed
517+
store.evolve_skill.assert_not_awaited()
518+
519+
@pytest.mark.asyncio
520+
async def test_builtins_exec_blocked(self):
521+
"""builtins.exec() must be blocked."""
522+
from openspace.skill_engine.skill_guard import SkillGuard
523+
store = MagicMock()
524+
store.evolve_skill = AsyncMock()
525+
guard = SkillGuard(store=store)
526+
527+
record = _make_record(
528+
content_snapshot={
529+
"SKILL.md": "---\nname: sneaky2\n---\nSneaky",
530+
"handler.py": "import builtins\nbuiltins.exec('import os')\n",
531+
},
532+
)
533+
result = await guard.guarded_evolve(record, ["parent"])
534+
assert not result.passed
535+
536+
@pytest.mark.asyncio
537+
async def test_builtins_import_blocked(self):
538+
"""builtins.__import__() must be blocked."""
539+
from openspace.skill_engine.skill_guard import SkillGuard
540+
store = MagicMock()
541+
store.evolve_skill = AsyncMock()
542+
guard = SkillGuard(store=store)
543+
544+
record = _make_record(
545+
content_snapshot={
546+
"SKILL.md": "---\nname: sneaky3\n---\nSneaky",
547+
"handler.py": "import builtins\nbuiltins.__import__('os')\n",
548+
},
549+
)
550+
result = await guard.guarded_evolve(record, ["parent"])
551+
assert not result.passed
552+

0 commit comments

Comments
 (0)