WIP checkpoint: Address review feedback: Research and design GDScript sandboxing approach
This commit is contained in:
@@ -1,76 +0,0 @@
|
|||||||
extends Node
|
|
||||||
|
|
||||||
func _ready():
|
|
||||||
print("=== SANDBOX PROBE START ===")
|
|
||||||
probe_script_loading()
|
|
||||||
probe_os_apis()
|
|
||||||
probe_file_access()
|
|
||||||
probe_network()
|
|
||||||
probe_engine_apis()
|
|
||||||
print("=== SANDBOX PROBE END ===")
|
|
||||||
get_tree().quit()
|
|
||||||
|
|
||||||
func probe_script_loading():
|
|
||||||
print("--- Script loading ---")
|
|
||||||
# Can we load a GDScript from a string at runtime?
|
|
||||||
var gd = GDScript.new()
|
|
||||||
gd.source_code = "extends RefCounted\nfunc hello():\n\treturn 42\n"
|
|
||||||
var err = gd.reload()
|
|
||||||
print("GDScript.reload() from source_code err=", err)
|
|
||||||
if err == OK:
|
|
||||||
var inst = gd.new()
|
|
||||||
print(" instantiated, hello()=", inst.call("hello"))
|
|
||||||
# Can we load a script resource from a path?
|
|
||||||
var res = load("res://test_script.gd")
|
|
||||||
print("load('res://test_script.gd') = ", res)
|
|
||||||
|
|
||||||
func probe_os_apis():
|
|
||||||
print("--- OS APIs ---")
|
|
||||||
# Can we execute shell commands?
|
|
||||||
var out = []
|
|
||||||
var exit_code = OS.execute("echo", ["hello"], out)
|
|
||||||
print("OS.execute('echo') exit=", exit_code, " out=", out)
|
|
||||||
# Environment access
|
|
||||||
print("OS.get_environment('HOME') = ", OS.get_environment("HOME"))
|
|
||||||
# Process spawning
|
|
||||||
print("OS.create_process available: ", OS.has_method("create_process"))
|
|
||||||
|
|
||||||
func probe_file_access():
|
|
||||||
print("--- File access ---")
|
|
||||||
# Can we read arbitrary files?
|
|
||||||
var f = FileAccess.open("/etc/hostname", FileAccess.READ)
|
|
||||||
print("FileAccess.open('/etc/hostname') = ", f)
|
|
||||||
if f:
|
|
||||||
print(" content: ", f.get_as_text())
|
|
||||||
f.close()
|
|
||||||
# Can we write outside the project?
|
|
||||||
var w = FileAccess.open("/tmp/godot_probe_write.txt", FileAccess.WRITE)
|
|
||||||
print("FileAccess.open('/tmp/...', WRITE) = ", w)
|
|
||||||
if w:
|
|
||||||
w.store_string("probe")
|
|
||||||
w.close()
|
|
||||||
print(" wrote to /tmp OK")
|
|
||||||
|
|
||||||
func probe_network():
|
|
||||||
print("--- Network ---")
|
|
||||||
# Can we open raw sockets?
|
|
||||||
var tcp = TCPServer.new()
|
|
||||||
var bind_err = tcp.listen(0)
|
|
||||||
print("TCPServer.listen(0) err=", bind_err)
|
|
||||||
# HTTP client
|
|
||||||
var http = HTTPClient.new()
|
|
||||||
print("HTTPClient instantiated: ", http != null)
|
|
||||||
|
|
||||||
func probe_engine_apis():
|
|
||||||
print("--- Engine APIs ---")
|
|
||||||
# Can we access the scene tree / add nodes?
|
|
||||||
var node = Node.new()
|
|
||||||
print("Node.new() = ", node)
|
|
||||||
# Can we access ProjectSettings?
|
|
||||||
print("ProjectSettings.get_setting('application/config/name') = ", ProjectSettings.get_setting("application/config/name"))
|
|
||||||
# Can we access the filesystem directory listing?
|
|
||||||
var dir = DirAccess.open("/")
|
|
||||||
print("DirAccess.open('/') = ", dir)
|
|
||||||
if dir:
|
|
||||||
var names = dir.get_files()
|
|
||||||
print(" root files count: ", names.size())
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
[gd_scene load_steps=2 format=3]
|
|
||||||
|
|
||||||
[ext_resource type="Script" path="res://probe.gd" id="1"]
|
|
||||||
|
|
||||||
[node name="Probe" type="Node"]
|
|
||||||
script = ExtResource("1")
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
extends Node
|
|
||||||
|
|
||||||
func _ready():
|
|
||||||
print("=== SANDBOX PROBE 2: Sandboxing Mechanisms ===")
|
|
||||||
probe_scriptserver()
|
|
||||||
probe_script_restrictions()
|
|
||||||
probe_subviewport_isolation()
|
|
||||||
probe_engine_restrictions()
|
|
||||||
probe_allowlist_approach()
|
|
||||||
print("=== SANDBOX PROBE 2 END ===")
|
|
||||||
get_tree().quit()
|
|
||||||
|
|
||||||
func probe_scriptserver():
|
|
||||||
print("--- ScriptServer ---")
|
|
||||||
# Does ScriptServer exist and what can it do?
|
|
||||||
print("ScriptServer has_method('get_language_count'): ", ScriptServer.has_method("get_language_count"))
|
|
||||||
print("ScriptServer has_method('get_language'): ", ScriptServer.has_method("get_language"))
|
|
||||||
# Can we enumerate registered script languages?
|
|
||||||
if ScriptServer.has_method("get_language_count"):
|
|
||||||
var count = ScriptServer.get_language_count()
|
|
||||||
print(" language count: ", count)
|
|
||||||
for i in range(count):
|
|
||||||
var lang = ScriptServer.get_language(i)
|
|
||||||
print(" language[", i, "] = ", lang)
|
|
||||||
|
|
||||||
func probe_script_restrictions():
|
|
||||||
print("--- Script restrictions ---")
|
|
||||||
# Can we restrict which classes/globals a script can access?
|
|
||||||
# Test: does GDScript have any built-in allowlist mechanism?
|
|
||||||
var gd = GDScript.new()
|
|
||||||
gd.source_code = "extends RefCounted\nfunc evil():\n\treturn OS.get_environment('HOME')\n"
|
|
||||||
var err = gd.reload()
|
|
||||||
print(" GDScript with OS access reload err=", err)
|
|
||||||
if err == OK:
|
|
||||||
var inst = gd.new()
|
|
||||||
print(" evil() result: ", inst.call("evil"))
|
|
||||||
# Can we block specific global classes?
|
|
||||||
print(" GDScript has method to restrict globals: ", gd.has_method("set_global_scope"))
|
|
||||||
|
|
||||||
func probe_subviewport_isolation():
|
|
||||||
print("--- SubViewport isolation ---")
|
|
||||||
# SubViewports isolate rendering, but do they isolate script execution?
|
|
||||||
var vp = SubViewport.new()
|
|
||||||
print(" SubViewport.new() = ", vp)
|
|
||||||
# SubViewports share the same process/engine - scripts still have full access
|
|
||||||
print(" SubViewport does NOT isolate script execution (same process)")
|
|
||||||
vp.free()
|
|
||||||
|
|
||||||
func probe_engine_restrictions():
|
|
||||||
print("--- Engine restrictions ---")
|
|
||||||
# Is there a way to disable OS/File/Network access globally?
|
|
||||||
print(" Engine.has_singleton('OS'): ", Engine.has_singleton("OS"))
|
|
||||||
print(" Engine.has_singleton('FileAccess'): ", Engine.has_singleton("FileAccess"))
|
|
||||||
# Check if there's a '--disable-*' flag or project setting
|
|
||||||
print(" ProjectSettings has 'application/run/disable_*': ", ProjectSettings.has_setting("application/run/disable_os"))
|
|
||||||
# Check for any sandbox-related project settings
|
|
||||||
var settings = ProjectSettings.get_property_list()
|
|
||||||
var sandbox_settings = []
|
|
||||||
for s in settings:
|
|
||||||
if "sandbox" in String(s.name).to_lower() or "restrict" in String(s.name).to_lower():
|
|
||||||
sandbox_settings.append(s.name)
|
|
||||||
print(" Sandbox-related project settings: ", sandbox_settings)
|
|
||||||
|
|
||||||
func probe_allowlist_approach():
|
|
||||||
print("--- Allowlist approach ---")
|
|
||||||
# The allowlist approach: parse GDScript source and reject disallowed patterns
|
|
||||||
# Test: can we detect dangerous API calls via string analysis?
|
|
||||||
var dangerous_patterns = [
|
|
||||||
"OS.execute", "OS.create_process", "OS.get_environment",
|
|
||||||
"FileAccess.open", "DirAccess.open", "TCPServer", "HTTPClient",
|
|
||||||
"load(", "preload(", "ClassDB", "Engine.get_main_loop",
|
|
||||||
"get_tree().quit", "ProjectSettings"
|
|
||||||
]
|
|
||||||
var test_script = """
|
|
||||||
extends Node
|
|
||||||
func _ready():
|
|
||||||
var f = FileAccess.open("/etc/passwd", FileAccess.READ)
|
|
||||||
OS.execute("rm", ["-rf", "/"])
|
|
||||||
"""
|
|
||||||
print(" Test script contains dangerous patterns:")
|
|
||||||
for p in dangerous_patterns:
|
|
||||||
if p in test_script:
|
|
||||||
print(" DETECTED: ", p)
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
extends Node
|
|
||||||
|
|
||||||
func _ready():
|
|
||||||
print("=== SANDBOX PROBE 3: Deep Sandboxing Mechanisms ===")
|
|
||||||
probe_global_scope_restriction()
|
|
||||||
probe_classdb_restriction()
|
|
||||||
probe_engine_singletons()
|
|
||||||
probe_script_validation()
|
|
||||||
probe_expression_engine()
|
|
||||||
probe_worker_threads()
|
|
||||||
probe_os_restriction_flags()
|
|
||||||
print("=== SANDBOX PROBE 3 END ===")
|
|
||||||
get_tree().quit()
|
|
||||||
|
|
||||||
func probe_global_scope_restriction():
|
|
||||||
print("--- Global scope restriction ---")
|
|
||||||
# Can we enumerate what's in the global scope?
|
|
||||||
var globals = []
|
|
||||||
for g in GDScript.get_global_class_list():
|
|
||||||
globals.append(g)
|
|
||||||
print(" GDScript.get_global_class_list() count: ", globals.size())
|
|
||||||
# Can we check if a global is available before using it?
|
|
||||||
print(" GDScript has 'get_global_class_list': ", GDScript.has_method("get_global_class_list"))
|
|
||||||
# Is there any way to restrict which globals a script sees?
|
|
||||||
print(" GDScript has 'set_global_scope': ", GDScript.has_method("set_global_scope"))
|
|
||||||
print(" GDScript has 'restrict_globals': ", GDScript.has_method("restrict_globals"))
|
|
||||||
# Check the GDScript class methods
|
|
||||||
var methods = GDScript.get_method_list()
|
|
||||||
var method_names = []
|
|
||||||
for m in methods:
|
|
||||||
method_names.append(m.name)
|
|
||||||
print(" GDScript methods: ", method_names)
|
|
||||||
|
|
||||||
func probe_classdb_restriction():
|
|
||||||
print("--- ClassDB restriction ---")
|
|
||||||
# ClassDB is the registry of all engine classes. Can we restrict it?
|
|
||||||
print(" ClassDB has 'get_class_list': ", ClassDB.has_method("get_class_list"))
|
|
||||||
print(" ClassDB has 'class_exists': ", ClassDB.has_method("class_exists"))
|
|
||||||
# Can we check if a specific class is available?
|
|
||||||
print(" ClassDB.class_exists('OS'): ", ClassDB.class_exists("OS"))
|
|
||||||
print(" ClassDB.class_exists('FileAccess'): ", ClassDB.class_exists("FileAccess"))
|
|
||||||
print(" ClassDB.class_exists('TCPServer'): ", ClassDB.class_exists("TCPServer"))
|
|
||||||
# Is there a way to hide classes from scripts?
|
|
||||||
print(" ClassDB has 'set_class_enabled': ", ClassDB.has_method("set_class_enabled"))
|
|
||||||
print(" ClassDB has 'disable_class': ", ClassDB.has_method("disable_class"))
|
|
||||||
|
|
||||||
func probe_engine_singletons():
|
|
||||||
print("--- Engine singletons ---")
|
|
||||||
# Engine singletons are the global objects like OS, Engine, etc.
|
|
||||||
# Can we enumerate them?
|
|
||||||
var singletons = Engine.get_singletons()
|
|
||||||
print(" Engine.get_singletons() count: ", singletons.size())
|
|
||||||
for s in singletons:
|
|
||||||
print(" singleton: ", s)
|
|
||||||
# Can we remove a singleton?
|
|
||||||
print(" Engine has 'remove_singleton': ", Engine.has_method("remove_singleton"))
|
|
||||||
print(" Engine has 'add_singleton': ", Engine.has_method("add_singleton"))
|
|
||||||
|
|
||||||
func probe_script_validation():
|
|
||||||
print("--- Script validation ---")
|
|
||||||
# Can we validate a script before running it?
|
|
||||||
var gd = GDScript.new()
|
|
||||||
gd.source_code = "extends RefCounted\nfunc evil():\n\treturn OS.get_environment('HOME')\n"
|
|
||||||
var err = gd.reload()
|
|
||||||
print(" GDScript.reload() err=", err)
|
|
||||||
# Can we get the list of identifiers used in a script?
|
|
||||||
print(" GDScript has 'get_script_method_list': ", gd.has_method("get_script_method_list"))
|
|
||||||
# Can we get the source code back?
|
|
||||||
print(" GDScript has 'get_source_code': ", gd.has_method("get_source_code"))
|
|
||||||
# Can we check what identifiers a script references?
|
|
||||||
print(" GDScript has 'get_script_constant_map': ", gd.has_method("get_script_constant_map"))
|
|
||||||
# Check if there's a way to get the AST
|
|
||||||
print(" GDScript has 'get_ast': ", gd.has_method("get_ast"))
|
|
||||||
print(" GDScript has 'get_parser': ", gd.has_method("get_parser"))
|
|
||||||
|
|
||||||
func probe_expression_engine():
|
|
||||||
print("--- Expression engine ---")
|
|
||||||
# Godot has an Expression class for evaluating expressions safely
|
|
||||||
var expr = Expression.new()
|
|
||||||
var err = expr.parse("1 + 2")
|
|
||||||
print(" Expression.parse('1 + 2') err=", err)
|
|
||||||
if err == OK:
|
|
||||||
var result = expr.execute()
|
|
||||||
print(" Expression.execute() = ", result)
|
|
||||||
# Can Expression access OS?
|
|
||||||
var expr2 = Expression.new()
|
|
||||||
err = expr2.parse("OS.get_environment('HOME')")
|
|
||||||
print(" Expression.parse('OS.get_environment') err=", err)
|
|
||||||
if err == OK:
|
|
||||||
var result2 = expr2.execute()
|
|
||||||
print(" Expression.execute() = ", result2)
|
|
||||||
# Expression with base_instance
|
|
||||||
var expr3 = Expression.new()
|
|
||||||
err = expr3.parse("get_environment('HOME')", ["OS"])
|
|
||||||
print(" Expression.parse with base_instance err=", err)
|
|
||||||
if err == OK:
|
|
||||||
var result3 = expr3.execute([], OS)
|
|
||||||
print(" Expression.execute with OS base = ", result3)
|
|
||||||
|
|
||||||
func probe_worker_threads():
|
|
||||||
print("--- Worker threads ---")
|
|
||||||
# Can we run scripts in worker threads?
|
|
||||||
print(" Thread has 'new': ", Thread.has_method("new"))
|
|
||||||
# WorkerThreadPool
|
|
||||||
print(" WorkerThreadPool has 'add_task': ", WorkerThreadPool.has_method("add_task"))
|
|
||||||
# Can we isolate a script in a thread?
|
|
||||||
print(" Thread available: ", Thread != null)
|
|
||||||
|
|
||||||
func probe_os_restriction_flags():
|
|
||||||
print("--- OS restriction flags ---")
|
|
||||||
# Check if there are any OS-level sandbox flags
|
|
||||||
print(" OS has 'get_environment': ", OS.has_method("get_environment"))
|
|
||||||
print(" OS has 'execute': ", OS.has_method("execute"))
|
|
||||||
print(" OS has 'create_process': ", OS.has_method("create_process"))
|
|
||||||
# Check project settings for sandbox-related options
|
|
||||||
var settings = ProjectSettings.get_property_list()
|
|
||||||
var relevant = []
|
|
||||||
for s in settings:
|
|
||||||
var name = String(s.name)
|
|
||||||
if "sandbox" in name.to_lower() or "restrict" in name.to_lower() or "security" in name.to_lower():
|
|
||||||
relevant.append(name)
|
|
||||||
print(" Sandbox/security project settings: ", relevant)
|
|
||||||
# Check if there's a way to disable specific features
|
|
||||||
print(" ProjectSettings has 'application/run/disable_os': ", ProjectSettings.has_setting("application/run/disable_os"))
|
|
||||||
print(" ProjectSettings has 'application/run/disable_file_access': ", ProjectSettings.has_setting("application/run/disable_file_access"))
|
|
||||||
print(" ProjectSettings has 'application/run/disable_network': ", ProjectSettings.has_setting("application/run/disable_network"))
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
[gd_scene load_steps=2 format=3]
|
|
||||||
|
|
||||||
[ext_resource type="Script" path="res://probe3.gd" id="1"]
|
|
||||||
|
|
||||||
[node name="Probe3" type="Node"]
|
|
||||||
script = ExtResource("1")
|
|
||||||
@@ -1,232 +0,0 @@
|
|||||||
extends Node
|
|
||||||
|
|
||||||
func _ready():
|
|
||||||
print("=== SANDBOX PROBE 4: Godot 4.6.2 Sandboxing Mechanisms ===")
|
|
||||||
probe_script_loading()
|
|
||||||
probe_os_apis()
|
|
||||||
probe_file_access()
|
|
||||||
probe_network()
|
|
||||||
probe_classdb()
|
|
||||||
probe_expression_engine()
|
|
||||||
probe_engine_singletons()
|
|
||||||
probe_global_scope()
|
|
||||||
probe_worker_threads()
|
|
||||||
probe_project_settings()
|
|
||||||
print("=== SANDBOX PROBE 4 END ===")
|
|
||||||
get_tree().quit()
|
|
||||||
|
|
||||||
func probe_script_loading():
|
|
||||||
print("--- Script loading ---")
|
|
||||||
# Can we load a GDScript from a string at runtime?
|
|
||||||
var gd = GDScript.new()
|
|
||||||
gd.source_code = "extends RefCounted\nfunc hello():\n\treturn 42\n"
|
|
||||||
var err = gd.reload()
|
|
||||||
print("GDScript.reload() from source_code err=", err)
|
|
||||||
if err == OK:
|
|
||||||
var inst = gd.new()
|
|
||||||
print(" instantiated, hello()=", inst.call("hello"))
|
|
||||||
# Can we load a script resource from a path?
|
|
||||||
var res = load("res://test_script.gd")
|
|
||||||
print("load('res://test_script.gd') = ", res)
|
|
||||||
# Can we get the source code back from a loaded script?
|
|
||||||
if res:
|
|
||||||
print(" get_source_code() available: ", res.has_method("get_source_code"))
|
|
||||||
# Can we get the AST / parse tree?
|
|
||||||
print(" GDScript instance has 'get_ast': ", gd.has_method("get_ast"))
|
|
||||||
print(" GDScript instance has 'get_parser': ", gd.has_method("get_parser"))
|
|
||||||
# Can we get the list of methods?
|
|
||||||
print(" GDScript instance has 'get_script_method_list': ", gd.has_method("get_script_method_list"))
|
|
||||||
# Can we get the list of member variables?
|
|
||||||
print(" GDScript instance has 'get_script_property_list': ", gd.has_method("get_script_property_list"))
|
|
||||||
|
|
||||||
func probe_os_apis():
|
|
||||||
print("--- OS APIs ---")
|
|
||||||
# Can we execute shell commands?
|
|
||||||
var out = []
|
|
||||||
var exit_code = OS.execute("echo", ["hello"], out)
|
|
||||||
print("OS.execute('echo') exit=", exit_code, " out=", out)
|
|
||||||
# Environment access
|
|
||||||
print("OS.get_environment('HOME') = ", OS.get_environment("HOME"))
|
|
||||||
# Process spawning
|
|
||||||
print("OS has 'create_process': ", OS.has_method("create_process"))
|
|
||||||
# Can we get the command line?
|
|
||||||
print("OS.get_cmdline_args() = ", OS.get_cmdline_args())
|
|
||||||
# Can we get user data dir?
|
|
||||||
print("OS.get_user_data_dir() = ", OS.get_user_data_dir())
|
|
||||||
# Can we get executable path?
|
|
||||||
print("OS.get_executable_path() = ", OS.get_executable_path())
|
|
||||||
|
|
||||||
func probe_file_access():
|
|
||||||
print("--- File access ---")
|
|
||||||
# Can we read arbitrary files?
|
|
||||||
var f = FileAccess.open("/etc/hostname", FileAccess.READ)
|
|
||||||
print("FileAccess.open('/etc/hostname') = ", f)
|
|
||||||
if f:
|
|
||||||
print(" content: ", f.get_as_text())
|
|
||||||
f.close()
|
|
||||||
# Can we write outside the project?
|
|
||||||
var w = FileAccess.open("/tmp/godot_probe_write.txt", FileAccess.WRITE)
|
|
||||||
print("FileAccess.open('/tmp/...', WRITE) = ", w)
|
|
||||||
if w:
|
|
||||||
w.store_string("probe")
|
|
||||||
w.close()
|
|
||||||
print(" wrote to /tmp OK")
|
|
||||||
# Can we list directories?
|
|
||||||
var dir = DirAccess.open("/")
|
|
||||||
print("DirAccess.open('/') = ", dir)
|
|
||||||
if dir:
|
|
||||||
var names = dir.get_files()
|
|
||||||
print(" root files count: ", names.size())
|
|
||||||
# Can we access res:// and user:// ?
|
|
||||||
var res_dir = DirAccess.open("res://")
|
|
||||||
print("DirAccess.open('res://') = ", res_dir)
|
|
||||||
var user_dir = DirAccess.open("user://")
|
|
||||||
print("DirAccess.open('user://') = ", user_dir)
|
|
||||||
|
|
||||||
func probe_network():
|
|
||||||
print("--- Network ---")
|
|
||||||
# Can we open raw sockets?
|
|
||||||
var tcp = TCPServer.new()
|
|
||||||
var bind_err = tcp.listen(0)
|
|
||||||
print("TCPServer.listen(0) err=", bind_err)
|
|
||||||
# HTTP client
|
|
||||||
var http = HTTPClient.new()
|
|
||||||
print("HTTPClient instantiated: ", http != null)
|
|
||||||
# Can we make HTTP requests?
|
|
||||||
var http_req = HTTPRequest.new()
|
|
||||||
print("HTTPRequest instantiated: ", http_req != null)
|
|
||||||
# PacketPeerUDP
|
|
||||||
var udp = PacketPeerUDP.new()
|
|
||||||
print("PacketPeerUDP instantiated: ", udp != null)
|
|
||||||
# WebSocket
|
|
||||||
var ws = WebSocketPeer.new()
|
|
||||||
print("WebSocketPeer instantiated: ", ws != null)
|
|
||||||
|
|
||||||
func probe_classdb():
|
|
||||||
print("--- ClassDB ---")
|
|
||||||
# ClassDB is the registry of all engine classes
|
|
||||||
print("ClassDB.class_exists('OS'): ", ClassDB.class_exists("OS"))
|
|
||||||
print("ClassDB.class_exists('FileAccess'): ", ClassDB.class_exists("FileAccess"))
|
|
||||||
print("ClassDB.class_exists('TCPServer'): ", ClassDB.class_exists("TCPServer"))
|
|
||||||
print("ClassDB.class_exists('HTTPClient'): ", ClassDB.class_exists("HTTPClient"))
|
|
||||||
# Can we enumerate all classes?
|
|
||||||
var class_list = ClassDB.get_class_list()
|
|
||||||
print("ClassDB.get_class_list() count: ", class_list.size())
|
|
||||||
# Can we get methods of a class?
|
|
||||||
var os_methods = ClassDB.class_get_method_list("OS")
|
|
||||||
print("OS method count: ", os_methods.size())
|
|
||||||
# Can we get properties of a class?
|
|
||||||
var os_props = ClassDB.class_get_property_list("OS")
|
|
||||||
print("OS property count: ", os_props.size())
|
|
||||||
# Can we instantiate a class by name?
|
|
||||||
var inst = ClassDB.instantiate("Node")
|
|
||||||
print("ClassDB.instantiate('Node') = ", inst)
|
|
||||||
# Can we check if a class is a singleton?
|
|
||||||
print("ClassDB has 'class_get_singleton': ", ClassDB.has_method("class_get_singleton"))
|
|
||||||
# Can we get the singleton instance of OS?
|
|
||||||
var os_singleton = ClassDB.class_get_singleton("OS")
|
|
||||||
print("ClassDB.class_get_singleton('OS') = ", os_singleton)
|
|
||||||
# Can we check what methods are available on the singleton?
|
|
||||||
if os_singleton:
|
|
||||||
print(" OS singleton has 'execute': ", os_singleton.has_method("execute"))
|
|
||||||
print(" OS singleton has 'get_environment': ", os_singleton.has_method("get_environment"))
|
|
||||||
|
|
||||||
func probe_expression_engine():
|
|
||||||
print("--- Expression engine ---")
|
|
||||||
# Godot has an Expression class for evaluating expressions safely
|
|
||||||
var expr = Expression.new()
|
|
||||||
var err = expr.parse("1 + 2")
|
|
||||||
print("Expression.parse('1 + 2') err=", err)
|
|
||||||
if err == OK:
|
|
||||||
var result = expr.execute()
|
|
||||||
print(" Expression.execute() = ", result)
|
|
||||||
# Can Expression access OS?
|
|
||||||
var expr2 = Expression.new()
|
|
||||||
err = expr2.parse("OS.get_environment('HOME')")
|
|
||||||
print("Expression.parse('OS.get_environment') err=", err)
|
|
||||||
if err == OK:
|
|
||||||
var result2 = expr2.execute()
|
|
||||||
print(" Expression.execute() = ", result2)
|
|
||||||
# Expression with base_instance
|
|
||||||
var expr3 = Expression.new()
|
|
||||||
err = expr3.parse("get_environment('HOME')", ["OS"])
|
|
||||||
print("Expression.parse with base_instance err=", err)
|
|
||||||
if err == OK:
|
|
||||||
var result3 = expr3.execute([], OS)
|
|
||||||
print(" Expression.execute with OS base = ", result3)
|
|
||||||
# Can Expression call arbitrary methods?
|
|
||||||
var expr4 = Expression.new()
|
|
||||||
err = expr4.parse("FileAccess.open('/etc/hostname', 1)")
|
|
||||||
print("Expression.parse('FileAccess.open') err=", err)
|
|
||||||
if err == OK:
|
|
||||||
var result4 = expr4.execute()
|
|
||||||
print(" Expression.execute() = ", result4)
|
|
||||||
# Can Expression access the scene tree?
|
|
||||||
var expr5 = Expression.new()
|
|
||||||
err = expr5.parse("get_tree().quit()")
|
|
||||||
print("Expression.parse('get_tree().quit()') err=", err)
|
|
||||||
if err == OK:
|
|
||||||
var result5 = expr5.execute()
|
|
||||||
print(" Expression.execute() = ", result5)
|
|
||||||
|
|
||||||
func probe_engine_singletons():
|
|
||||||
print("--- Engine singletons ---")
|
|
||||||
# Engine singletons are the global objects like OS, Engine, etc.
|
|
||||||
var singletons = Engine.get_singletons()
|
|
||||||
print("Engine.get_singletons() count: ", singletons.size())
|
|
||||||
for s in singletons:
|
|
||||||
print(" singleton: ", s)
|
|
||||||
# Can we remove a singleton?
|
|
||||||
print("Engine has 'remove_singleton': ", Engine.has_method("remove_singleton"))
|
|
||||||
print("Engine has 'add_singleton': ", Engine.has_method("add_singleton"))
|
|
||||||
# Can we get the main loop?
|
|
||||||
print("Engine.get_main_loop() = ", Engine.get_main_loop())
|
|
||||||
# Can we get the physics server?
|
|
||||||
print("Engine.get_physics_server() = ", Engine.get_physics_server())
|
|
||||||
|
|
||||||
func probe_global_scope():
|
|
||||||
print("--- Global scope ---")
|
|
||||||
# Can we enumerate what's in the global scope?
|
|
||||||
var globals = []
|
|
||||||
for g in GDScript.get_global_class_list():
|
|
||||||
globals.append(g)
|
|
||||||
print("GDScript.get_global_class_list() count: ", globals.size())
|
|
||||||
# Can we check if a global is available before using it?
|
|
||||||
print("GDScript has 'get_global_class_list': ", GDScript.has_method("get_global_class_list"))
|
|
||||||
# Is there any way to restrict which globals a script sees?
|
|
||||||
print("GDScript has 'set_global_scope': ", GDScript.has_method("set_global_scope"))
|
|
||||||
print("GDScript has 'restrict_globals': ", GDScript.has_method("restrict_globals"))
|
|
||||||
# Check the GDScript class methods
|
|
||||||
var methods = GDScript.get_method_list()
|
|
||||||
var method_names = []
|
|
||||||
for m in methods:
|
|
||||||
method_names.append(m.name)
|
|
||||||
print(" GDScript methods: ", method_names)
|
|
||||||
|
|
||||||
func probe_worker_threads():
|
|
||||||
print("--- Worker threads ---")
|
|
||||||
# Can we run scripts in worker threads?
|
|
||||||
var thread = Thread.new()
|
|
||||||
print(" Thread.new() = ", thread)
|
|
||||||
# WorkerThreadPool
|
|
||||||
print(" WorkerThreadPool has 'add_task': ", WorkerThreadPool.has_method("add_task"))
|
|
||||||
# Can we isolate a script in a thread?
|
|
||||||
print(" Thread available: ", Thread != null)
|
|
||||||
|
|
||||||
func probe_project_settings():
|
|
||||||
print("--- Project settings ---")
|
|
||||||
# Check for any sandbox-related project settings
|
|
||||||
var settings = ProjectSettings.get_property_list()
|
|
||||||
var relevant = []
|
|
||||||
for s in settings:
|
|
||||||
var name = String(s.name)
|
|
||||||
if "sandbox" in name.to_lower() or "restrict" in name.to_lower() or "security" in name.to_lower():
|
|
||||||
relevant.append(name)
|
|
||||||
print(" Sandbox/security project settings: ", relevant)
|
|
||||||
# Check if there's a way to disable specific features
|
|
||||||
print(" ProjectSettings has 'application/run/disable_os': ", ProjectSettings.has_setting("application/run/disable_os"))
|
|
||||||
print(" ProjectSettings has 'application/run/disable_file_access': ", ProjectSettings.has_setting("application/run/disable_file_access"))
|
|
||||||
print(" ProjectSettings has 'application/run/disable_network': ", ProjectSettings.has_setting("application/run/disable_network"))
|
|
||||||
# Can we set project settings at runtime?
|
|
||||||
print(" ProjectSettings has 'set_setting': ", ProjectSettings.has_method("set_setting"))
|
|
||||||
print(" ProjectSettings has 'save': ", ProjectSettings.has_method("save"))
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
[gd_scene load_steps=2 format=3]
|
|
||||||
|
|
||||||
[ext_resource type="Script" path="res://probe4.gd" id="1"]
|
|
||||||
|
|
||||||
[node name="Probe4" type="Node"]
|
|
||||||
script = ExtResource("1")
|
|
||||||
@@ -1,221 +0,0 @@
|
|||||||
extends Node
|
|
||||||
|
|
||||||
func _ready():
|
|
||||||
print("=== SANDBOX PROBE 5b: Corrected Sandboxing Mechanisms (Godot 4.6.2) ===")
|
|
||||||
probe_script_loading()
|
|
||||||
probe_os_apis()
|
|
||||||
probe_file_access()
|
|
||||||
probe_network()
|
|
||||||
probe_classdb()
|
|
||||||
probe_expression_engine()
|
|
||||||
probe_engine_singletons()
|
|
||||||
probe_worker_threads()
|
|
||||||
probe_project_settings()
|
|
||||||
probe_script_restrictions()
|
|
||||||
probe_allowlist()
|
|
||||||
print("=== SANDBOX PROBE 5b END ===")
|
|
||||||
get_tree().quit()
|
|
||||||
|
|
||||||
func probe_script_loading():
|
|
||||||
print("--- Script loading ---")
|
|
||||||
var gd = GDScript.new()
|
|
||||||
gd.source_code = "extends RefCounted\nfunc hello():\n\treturn 42\n"
|
|
||||||
var err = gd.reload()
|
|
||||||
print("GDScript.reload() from source_code err=", err)
|
|
||||||
if err == OK:
|
|
||||||
var inst = gd.new()
|
|
||||||
print(" instantiated, hello()=", inst.call("hello"))
|
|
||||||
var res = load("res://test_script.gd")
|
|
||||||
print("load('res://test_script.gd') = ", res)
|
|
||||||
if res:
|
|
||||||
print(" get_source_code() available: ", res.has_method("get_source_code"))
|
|
||||||
print(" GDScript instance has 'get_ast': ", gd.has_method("get_ast"))
|
|
||||||
print(" GDScript instance has 'get_parser': ", gd.has_method("get_parser"))
|
|
||||||
print(" GDScript instance has 'get_script_method_list': ", gd.has_method("get_script_method_list"))
|
|
||||||
print(" GDScript instance has 'get_script_property_list': ", gd.has_method("get_script_property_list"))
|
|
||||||
print(" GDScript instance has 'get_instance_base_type': ", gd.has_method("get_instance_base_type"))
|
|
||||||
# Can we get the list of static methods on GDScript class?
|
|
||||||
var gd_methods = GDScript.get_method_list()
|
|
||||||
var gd_method_names = []
|
|
||||||
for m in gd_methods:
|
|
||||||
gd_method_names.append(String(m.name))
|
|
||||||
print(" GDScript static methods: ", gd_method_names)
|
|
||||||
|
|
||||||
func probe_os_apis():
|
|
||||||
print("--- OS APIs ---")
|
|
||||||
var out = []
|
|
||||||
var exit_code = OS.execute("echo", ["hello"], out)
|
|
||||||
print("OS.execute('echo') exit=", exit_code, " out=", out)
|
|
||||||
print("OS.get_environment('HOME') = ", OS.get_environment("HOME"))
|
|
||||||
print("OS has 'create_process': ", OS.has_method("create_process"))
|
|
||||||
print("OS.get_cmdline_args() = ", OS.get_cmdline_args())
|
|
||||||
print("OS.get_user_data_dir() = ", OS.get_user_data_dir())
|
|
||||||
print("OS.get_executable_path() = ", OS.get_executable_path())
|
|
||||||
|
|
||||||
func probe_file_access():
|
|
||||||
print("--- File access ---")
|
|
||||||
var f = FileAccess.open("/etc/hostname", FileAccess.READ)
|
|
||||||
print("FileAccess.open('/etc/hostname') = ", f)
|
|
||||||
if f:
|
|
||||||
print(" content: ", f.get_as_text())
|
|
||||||
f.close()
|
|
||||||
var w = FileAccess.open("/tmp/godot_probe_write.txt", FileAccess.WRITE)
|
|
||||||
print("FileAccess.open('/tmp/...', WRITE) = ", w)
|
|
||||||
if w:
|
|
||||||
w.store_string("probe")
|
|
||||||
w.close()
|
|
||||||
print(" wrote to /tmp OK")
|
|
||||||
var dir = DirAccess.open("/")
|
|
||||||
print("DirAccess.open('/') = ", dir)
|
|
||||||
if dir:
|
|
||||||
var names = dir.get_files()
|
|
||||||
print(" root files count: ", names.size())
|
|
||||||
var res_dir = DirAccess.open("res://")
|
|
||||||
print("DirAccess.open('res://') = ", res_dir)
|
|
||||||
var user_dir = DirAccess.open("user://")
|
|
||||||
print("DirAccess.open('user://') = ", user_dir)
|
|
||||||
|
|
||||||
func probe_network():
|
|
||||||
print("--- Network ---")
|
|
||||||
var tcp = TCPServer.new()
|
|
||||||
var bind_err = tcp.listen(0)
|
|
||||||
print("TCPServer.listen(0) err=", bind_err)
|
|
||||||
var http = HTTPClient.new()
|
|
||||||
print("HTTPClient instantiated: ", http != null)
|
|
||||||
var http_req = HTTPRequest.new()
|
|
||||||
print("HTTPRequest instantiated: ", http_req != null)
|
|
||||||
var udp = PacketPeerUDP.new()
|
|
||||||
print("PacketPeerUDP instantiated: ", udp != null)
|
|
||||||
var ws = WebSocketPeer.new()
|
|
||||||
print("WebSocketPeer instantiated: ", ws != null)
|
|
||||||
|
|
||||||
func probe_classdb():
|
|
||||||
print("--- ClassDB ---")
|
|
||||||
print("ClassDB.class_exists('OS'): ", ClassDB.class_exists("OS"))
|
|
||||||
print("ClassDB.class_exists('FileAccess'): ", ClassDB.class_exists("FileAccess"))
|
|
||||||
print("ClassDB.class_exists('TCPServer'): ", ClassDB.class_exists("TCPServer"))
|
|
||||||
print("ClassDB.class_exists('HTTPClient'): ", ClassDB.class_exists("HTTPClient"))
|
|
||||||
var class_list = ClassDB.get_class_list()
|
|
||||||
print("ClassDB.get_class_list() count: ", class_list.size())
|
|
||||||
var os_methods = ClassDB.class_get_method_list("OS")
|
|
||||||
print("OS method count: ", os_methods.size())
|
|
||||||
var inst = ClassDB.instantiate("Node")
|
|
||||||
print("ClassDB.instantiate('Node') = ", inst)
|
|
||||||
# Can we enumerate ClassDB methods to see what's available?
|
|
||||||
var classdb_methods = ClassDB.get_method_list()
|
|
||||||
var classdb_method_names = []
|
|
||||||
for m in classdb_methods:
|
|
||||||
classdb_method_names.append(String(m.name))
|
|
||||||
print(" ClassDB methods: ", classdb_method_names)
|
|
||||||
|
|
||||||
func probe_expression_engine():
|
|
||||||
print("--- Expression engine ---")
|
|
||||||
var expr = Expression.new()
|
|
||||||
var err = expr.parse("1 + 2")
|
|
||||||
print("Expression.parse('1 + 2') err=", err)
|
|
||||||
if err == OK:
|
|
||||||
var result = expr.execute()
|
|
||||||
print(" Expression.execute() = ", result)
|
|
||||||
var expr2 = Expression.new()
|
|
||||||
err = expr2.parse("OS.get_environment('HOME')")
|
|
||||||
print("Expression.parse('OS.get_environment') err=", err)
|
|
||||||
if err == OK:
|
|
||||||
var result2 = expr2.execute()
|
|
||||||
print(" Expression.execute() = ", result2)
|
|
||||||
var expr3 = Expression.new()
|
|
||||||
err = expr3.parse("get_environment('HOME')", ["OS"])
|
|
||||||
print("Expression.parse with base_instance err=", err)
|
|
||||||
if err == OK:
|
|
||||||
var result3 = expr3.execute([], OS)
|
|
||||||
print(" Expression.execute with OS base = ", result3)
|
|
||||||
var expr4 = Expression.new()
|
|
||||||
err = expr4.parse("FileAccess.open('/etc/hostname', 1)")
|
|
||||||
print("Expression.parse('FileAccess.open') err=", err)
|
|
||||||
if err == OK:
|
|
||||||
var result4 = expr4.execute()
|
|
||||||
print(" Expression.execute() = ", result4)
|
|
||||||
var expr5 = Expression.new()
|
|
||||||
err = expr5.parse("get_tree().quit()")
|
|
||||||
print("Expression.parse('get_tree().quit()') err=", err)
|
|
||||||
if err == OK:
|
|
||||||
var result5 = expr5.execute()
|
|
||||||
print(" Expression.execute() = ", result5)
|
|
||||||
|
|
||||||
func probe_engine_singletons():
|
|
||||||
print("--- Engine singletons ---")
|
|
||||||
print("Engine has 'remove_singleton': ", Engine.has_method("remove_singleton"))
|
|
||||||
print("Engine has 'add_singleton': ", Engine.has_method("add_singleton"))
|
|
||||||
print("Engine.get_main_loop() = ", Engine.get_main_loop())
|
|
||||||
# Can we enumerate singletons via Engine instance methods?
|
|
||||||
var engine_methods = Engine.get_method_list()
|
|
||||||
var engine_method_names = []
|
|
||||||
for m in engine_methods:
|
|
||||||
engine_method_names.append(String(m.name))
|
|
||||||
print(" Engine instance methods: ", engine_method_names)
|
|
||||||
|
|
||||||
func probe_worker_threads():
|
|
||||||
print("--- Worker threads ---")
|
|
||||||
var thread = Thread.new()
|
|
||||||
print(" Thread.new() = ", thread)
|
|
||||||
print(" WorkerThreadPool has 'add_task': ", WorkerThreadPool.has_method("add_task"))
|
|
||||||
|
|
||||||
func probe_project_settings():
|
|
||||||
print("--- Project settings ---")
|
|
||||||
var settings = ProjectSettings.get_property_list()
|
|
||||||
var relevant = []
|
|
||||||
for s in settings:
|
|
||||||
var name = String(s.name)
|
|
||||||
if "sandbox" in name.to_lower() or "restrict" in name.to_lower() or "security" in name.to_lower():
|
|
||||||
relevant.append(name)
|
|
||||||
print(" Sandbox/security project settings: ", relevant)
|
|
||||||
print(" ProjectSettings has 'application/run/disable_os': ", ProjectSettings.has_setting("application/run/disable_os"))
|
|
||||||
print(" ProjectSettings has 'application/run/disable_file_access': ", ProjectSettings.has_setting("application/run/disable_file_access"))
|
|
||||||
print(" ProjectSettings has 'application/run/disable_network': ", ProjectSettings.has_setting("application/run/disable_network"))
|
|
||||||
print(" ProjectSettings has 'set_setting': ", ProjectSettings.has_method("set_setting"))
|
|
||||||
print(" ProjectSettings has 'save': ", ProjectSettings.has_method("save"))
|
|
||||||
|
|
||||||
func probe_script_restrictions():
|
|
||||||
print("--- Script restrictions ---")
|
|
||||||
var gd = GDScript.new()
|
|
||||||
gd.source_code = "extends RefCounted\nfunc evil():\n\treturn OS.get_environment('HOME')\n"
|
|
||||||
var err = gd.reload()
|
|
||||||
print(" GDScript with OS access reload err=", err)
|
|
||||||
if err == OK:
|
|
||||||
var inst = gd.new()
|
|
||||||
print(" evil() result: ", inst.call("evil"))
|
|
||||||
print(" GDScript has method to restrict globals: ", gd.has_method("set_global_scope"))
|
|
||||||
print(" GDScript has method 'get_global_scope': ", gd.has_method("get_global_scope"))
|
|
||||||
# Can we check what identifiers a script references via the analyzer?
|
|
||||||
print(" GDScript has 'get_script_constant_map': ", gd.has_method("get_script_constant_map"))
|
|
||||||
# Can we get the list of member variables?
|
|
||||||
print(" GDScript has 'get_script_property_list': ", gd.has_method("get_script_property_list"))
|
|
||||||
|
|
||||||
func probe_allowlist():
|
|
||||||
print("--- Allowlist / static analysis ---")
|
|
||||||
var dangerous_patterns = [
|
|
||||||
"OS.execute", "OS.create_process", "OS.get_environment",
|
|
||||||
"FileAccess.open", "DirAccess.open", "TCPServer", "HTTPClient",
|
|
||||||
"load(", "preload(", "ClassDB", "Engine.get_main_loop",
|
|
||||||
"get_tree().quit", "ProjectSettings"
|
|
||||||
]
|
|
||||||
var test_script = """extends Node
|
|
||||||
func _ready():
|
|
||||||
var f = FileAccess.open("/etc/passwd", FileAccess.READ)
|
|
||||||
OS.execute("rm", ["-rf", "/"])
|
|
||||||
"""
|
|
||||||
print(" Test script contains dangerous patterns:")
|
|
||||||
for p in dangerous_patterns:
|
|
||||||
if p in test_script:
|
|
||||||
print(" DETECTED: ", p)
|
|
||||||
# Can we use a tokenizer/lexer to parse GDScript?
|
|
||||||
# GDScriptLanguage is NOT exposed to GDScript (confirmed by parse error)
|
|
||||||
print(" GDScriptLanguage NOT exposed to GDScript (parse error confirmed)")
|
|
||||||
# Can we use the Expression engine to validate syntax?
|
|
||||||
var expr = Expression.new()
|
|
||||||
var err = expr.parse("var x = 1 + 2")
|
|
||||||
print(" Expression.parse('var x = 1 + 2') err=", err)
|
|
||||||
err = expr.parse("OS.execute('rm', ['-rf', '/'])")
|
|
||||||
print(" Expression.parse('OS.execute...') err=", err)
|
|
||||||
if err == OK:
|
|
||||||
var result = expr.execute()
|
|
||||||
print(" Expression.execute() = ", result)
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
[gd_scene load_steps=2 format=3]
|
|
||||||
|
|
||||||
[ext_resource type="Script" path="res://probe5.gd" id="1"]
|
|
||||||
|
|
||||||
[node name="Probe5" type="Node"]
|
|
||||||
script = ExtResource("1")
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
extends SceneTree
|
|
||||||
func _init():
|
|
||||||
print("ScriptServer class exists: ", ClassDB.class_exists("ScriptServer"))
|
|
||||||
var sv = SubViewport.new()
|
|
||||||
print("SubViewport instantiated: ", sv != null)
|
|
||||||
quit()
|
|
||||||
@@ -4,5 +4,5 @@ config_version=5
|
|||||||
[application]
|
[application]
|
||||||
|
|
||||||
config/name="sandbox_probe"
|
config/name="sandbox_probe"
|
||||||
run/main_scene="res://probe5.tscn"
|
run/main_scene="res://probe6.tscn"
|
||||||
config/features=PackedStringArray("4.6")
|
config/features=PackedStringArray("4.6")
|
||||||
|
|||||||
Reference in New Issue
Block a user