WIP checkpoint: Research and design GDScript sandboxing approach

This commit is contained in:
2026-08-17 21:30:27 +00:00
parent 0b00b01502
commit a576c25124
3 changed files with 43 additions and 98 deletions
+36 -97
View File
@@ -1,7 +1,7 @@
extends Node extends Node
func _ready(): func _ready():
print("=== SANDBOX PROBE 5: Corrected Sandboxing Mechanisms (Godot 4.6.2) ===") print("=== SANDBOX PROBE 5b: Corrected Sandboxing Mechanisms (Godot 4.6.2) ===")
probe_script_loading() probe_script_loading()
probe_os_apis() probe_os_apis()
probe_file_access() probe_file_access()
@@ -11,15 +11,13 @@ func _ready():
probe_engine_singletons() probe_engine_singletons()
probe_worker_threads() probe_worker_threads()
probe_project_settings() probe_project_settings()
probe_scriptserver()
probe_script_restrictions() probe_script_restrictions()
probe_allowlist() probe_allowlist()
print("=== SANDBOX PROBE 5 END ===") print("=== SANDBOX PROBE 5b END ===")
get_tree().quit() get_tree().quit()
func probe_script_loading(): func probe_script_loading():
print("--- Script loading ---") print("--- Script loading ---")
# Can we load a GDScript from a string at runtime?
var gd = GDScript.new() var gd = GDScript.new()
gd.source_code = "extends RefCounted\nfunc hello():\n\treturn 42\n" gd.source_code = "extends RefCounted\nfunc hello():\n\treturn 42\n"
var err = gd.reload() var err = gd.reload()
@@ -27,63 +25,51 @@ func probe_script_loading():
if err == OK: if err == OK:
var inst = gd.new() var inst = gd.new()
print(" instantiated, hello()=", inst.call("hello")) print(" instantiated, hello()=", inst.call("hello"))
# Can we load a script resource from a path?
var res = load("res://test_script.gd") var res = load("res://test_script.gd")
print("load('res://test_script.gd') = ", res) print("load('res://test_script.gd') = ", res)
# Can we get the source code back from a loaded script?
if res: if res:
print(" get_source_code() available: ", res.has_method("get_source_code")) print(" get_source_code() available: ", res.has_method("get_source_code"))
# Can we get the AST / parse tree? (instance methods)
print(" GDScript instance has 'get_ast': ", gd.has_method("get_ast")) 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_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")) 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")) print(" GDScript instance has 'get_script_property_list': ", gd.has_method("get_script_property_list"))
# Can we get the script's base class?
print(" GDScript instance has 'get_instance_base_type': ", gd.has_method("get_instance_base_type")) print(" GDScript instance has 'get_instance_base_type': ", gd.has_method("get_instance_base_type"))
# Can we get the script's resource path? # Can we get the list of static methods on GDScript class?
print(" GDScript instance has 'get_path': ", gd.has_method("get_path")) 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(): func probe_os_apis():
print("--- OS APIs ---") print("--- OS APIs ---")
# Can we execute shell commands?
var out = [] var out = []
var exit_code = OS.execute("echo", ["hello"], out) var exit_code = OS.execute("echo", ["hello"], out)
print("OS.execute('echo') exit=", exit_code, " out=", out) print("OS.execute('echo') exit=", exit_code, " out=", out)
# Environment access
print("OS.get_environment('HOME') = ", OS.get_environment("HOME")) print("OS.get_environment('HOME') = ", OS.get_environment("HOME"))
# Process spawning
print("OS has 'create_process': ", OS.has_method("create_process")) 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()) 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()) 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()) print("OS.get_executable_path() = ", OS.get_executable_path())
func probe_file_access(): func probe_file_access():
print("--- File access ---") print("--- File access ---")
# Can we read arbitrary files?
var f = FileAccess.open("/etc/hostname", FileAccess.READ) var f = FileAccess.open("/etc/hostname", FileAccess.READ)
print("FileAccess.open('/etc/hostname') = ", f) print("FileAccess.open('/etc/hostname') = ", f)
if f: if f:
print(" content: ", f.get_as_text()) print(" content: ", f.get_as_text())
f.close() f.close()
# Can we write outside the project?
var w = FileAccess.open("/tmp/godot_probe_write.txt", FileAccess.WRITE) var w = FileAccess.open("/tmp/godot_probe_write.txt", FileAccess.WRITE)
print("FileAccess.open('/tmp/...', WRITE) = ", w) print("FileAccess.open('/tmp/...', WRITE) = ", w)
if w: if w:
w.store_string("probe") w.store_string("probe")
w.close() w.close()
print(" wrote to /tmp OK") print(" wrote to /tmp OK")
# Can we list directories?
var dir = DirAccess.open("/") var dir = DirAccess.open("/")
print("DirAccess.open('/') = ", dir) print("DirAccess.open('/') = ", dir)
if dir: if dir:
var names = dir.get_files() var names = dir.get_files()
print(" root files count: ", names.size()) print(" root files count: ", names.size())
# Can we access res:// and user:// ?
var res_dir = DirAccess.open("res://") var res_dir = DirAccess.open("res://")
print("DirAccess.open('res://') = ", res_dir) print("DirAccess.open('res://') = ", res_dir)
var user_dir = DirAccess.open("user://") var user_dir = DirAccess.open("user://")
@@ -91,83 +77,63 @@ func probe_file_access():
func probe_network(): func probe_network():
print("--- Network ---") print("--- Network ---")
# Can we open raw sockets?
var tcp = TCPServer.new() var tcp = TCPServer.new()
var bind_err = tcp.listen(0) var bind_err = tcp.listen(0)
print("TCPServer.listen(0) err=", bind_err) print("TCPServer.listen(0) err=", bind_err)
# HTTP client
var http = HTTPClient.new() var http = HTTPClient.new()
print("HTTPClient instantiated: ", http != null) print("HTTPClient instantiated: ", http != null)
# Can we make HTTP requests?
var http_req = HTTPRequest.new() var http_req = HTTPRequest.new()
print("HTTPRequest instantiated: ", http_req != null) print("HTTPRequest instantiated: ", http_req != null)
# PacketPeerUDP
var udp = PacketPeerUDP.new() var udp = PacketPeerUDP.new()
print("PacketPeerUDP instantiated: ", udp != null) print("PacketPeerUDP instantiated: ", udp != null)
# WebSocket
var ws = WebSocketPeer.new() var ws = WebSocketPeer.new()
print("WebSocketPeer instantiated: ", ws != null) print("WebSocketPeer instantiated: ", ws != null)
func probe_classdb(): func probe_classdb():
print("--- ClassDB ---") print("--- ClassDB ---")
# ClassDB is the registry of all engine classes
print("ClassDB.class_exists('OS'): ", ClassDB.class_exists("OS")) print("ClassDB.class_exists('OS'): ", ClassDB.class_exists("OS"))
print("ClassDB.class_exists('FileAccess'): ", ClassDB.class_exists("FileAccess")) print("ClassDB.class_exists('FileAccess'): ", ClassDB.class_exists("FileAccess"))
print("ClassDB.class_exists('TCPServer'): ", ClassDB.class_exists("TCPServer")) print("ClassDB.class_exists('TCPServer'): ", ClassDB.class_exists("TCPServer"))
print("ClassDB.class_exists('HTTPClient'): ", ClassDB.class_exists("HTTPClient")) print("ClassDB.class_exists('HTTPClient'): ", ClassDB.class_exists("HTTPClient"))
# Can we enumerate all classes?
var class_list = ClassDB.get_class_list() var class_list = ClassDB.get_class_list()
print("ClassDB.get_class_list() count: ", class_list.size()) 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") var os_methods = ClassDB.class_get_method_list("OS")
print("OS method count: ", os_methods.size()) 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") var inst = ClassDB.instantiate("Node")
print("ClassDB.instantiate('Node') = ", inst) print("ClassDB.instantiate('Node') = ", inst)
# Can we check if a class is a singleton? (static method on ClassDB) # Can we enumerate ClassDB methods to see what's available?
print("ClassDB has 'class_get_singleton': ", ClassDB.has_method("class_get_singleton")) var classdb_methods = ClassDB.get_method_list()
# Can we get the singleton instance of OS? var classdb_method_names = []
var os_singleton = ClassDB.class_get_singleton("OS") for m in classdb_methods:
print("ClassDB.class_get_singleton('OS') = ", os_singleton) classdb_method_names.append(String(m.name))
# Can we check what methods are available on the singleton? print(" ClassDB methods: ", classdb_method_names)
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(): func probe_expression_engine():
print("--- Expression engine ---") print("--- Expression engine ---")
# Godot has an Expression class for evaluating expressions safely
var expr = Expression.new() var expr = Expression.new()
var err = expr.parse("1 + 2") var err = expr.parse("1 + 2")
print("Expression.parse('1 + 2') err=", err) print("Expression.parse('1 + 2') err=", err)
if err == OK: if err == OK:
var result = expr.execute() var result = expr.execute()
print(" Expression.execute() = ", result) print(" Expression.execute() = ", result)
# Can Expression access OS?
var expr2 = Expression.new() var expr2 = Expression.new()
err = expr2.parse("OS.get_environment('HOME')") err = expr2.parse("OS.get_environment('HOME')")
print("Expression.parse('OS.get_environment') err=", err) print("Expression.parse('OS.get_environment') err=", err)
if err == OK: if err == OK:
var result2 = expr2.execute() var result2 = expr2.execute()
print(" Expression.execute() = ", result2) print(" Expression.execute() = ", result2)
# Expression with base_instance
var expr3 = Expression.new() var expr3 = Expression.new()
err = expr3.parse("get_environment('HOME')", ["OS"]) err = expr3.parse("get_environment('HOME')", ["OS"])
print("Expression.parse with base_instance err=", err) print("Expression.parse with base_instance err=", err)
if err == OK: if err == OK:
var result3 = expr3.execute([], OS) var result3 = expr3.execute([], OS)
print(" Expression.execute with OS base = ", result3) print(" Expression.execute with OS base = ", result3)
# Can Expression call arbitrary methods?
var expr4 = Expression.new() var expr4 = Expression.new()
err = expr4.parse("FileAccess.open('/etc/hostname', 1)") err = expr4.parse("FileAccess.open('/etc/hostname', 1)")
print("Expression.parse('FileAccess.open') err=", err) print("Expression.parse('FileAccess.open') err=", err)
if err == OK: if err == OK:
var result4 = expr4.execute() var result4 = expr4.execute()
print(" Expression.execute() = ", result4) print(" Expression.execute() = ", result4)
# Can Expression access the scene tree?
var expr5 = Expression.new() var expr5 = Expression.new()
err = expr5.parse("get_tree().quit()") err = expr5.parse("get_tree().quit()")
print("Expression.parse('get_tree().quit()') err=", err) print("Expression.parse('get_tree().quit()') err=", err)
@@ -177,33 +143,24 @@ func probe_expression_engine():
func probe_engine_singletons(): func probe_engine_singletons():
print("--- Engine singletons ---") print("--- Engine singletons ---")
# Engine singletons are the global objects like OS, Engine, etc.
# get_singletons() is a static method on Engine
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 'remove_singleton': ", Engine.has_method("remove_singleton"))
print("Engine has 'add_singleton': ", Engine.has_method("add_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()) print("Engine.get_main_loop() = ", Engine.get_main_loop())
# Can we get the physics server? # Can we enumerate singletons via Engine instance methods?
print("Engine.get_physics_server() = ", Engine.get_physics_server()) 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(): func probe_worker_threads():
print("--- Worker threads ---") print("--- Worker threads ---")
# Can we run scripts in worker threads?
var thread = Thread.new() var thread = Thread.new()
print(" Thread.new() = ", thread) print(" Thread.new() = ", thread)
# WorkerThreadPool
print(" WorkerThreadPool has 'add_task': ", WorkerThreadPool.has_method("add_task")) 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(): func probe_project_settings():
print("--- Project settings ---") print("--- Project settings ---")
# Check for any sandbox-related project settings
var settings = ProjectSettings.get_property_list() var settings = ProjectSettings.get_property_list()
var relevant = [] var relevant = []
for s in settings: for s in settings:
@@ -211,31 +168,14 @@ func probe_project_settings():
if "sandbox" in name.to_lower() or "restrict" in name.to_lower() or "security" in name.to_lower(): if "sandbox" in name.to_lower() or "restrict" in name.to_lower() or "security" in name.to_lower():
relevant.append(name) relevant.append(name)
print(" Sandbox/security project settings: ", relevant) 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_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_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 '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 'set_setting': ", ProjectSettings.has_method("set_setting"))
print(" ProjectSettings has 'save': ", ProjectSettings.has_method("save")) print(" ProjectSettings has 'save': ", ProjectSettings.has_method("save"))
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(): func probe_script_restrictions():
print("--- 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() var gd = GDScript.new()
gd.source_code = "extends RefCounted\nfunc evil():\n\treturn OS.get_environment('HOME')\n" gd.source_code = "extends RefCounted\nfunc evil():\n\treturn OS.get_environment('HOME')\n"
var err = gd.reload() var err = gd.reload()
@@ -243,14 +183,15 @@ func probe_script_restrictions():
if err == OK: if err == OK:
var inst = gd.new() var inst = gd.new()
print(" evil() result: ", inst.call("evil")) 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")) 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")) 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(): func probe_allowlist():
print("--- Allowlist / static analysis ---") print("--- Allowlist / static analysis ---")
# The allowlist approach: parse GDScript source and reject disallowed patterns
# Test: can we detect dangerous API calls via string analysis?
var dangerous_patterns = [ var dangerous_patterns = [
"OS.execute", "OS.create_process", "OS.get_environment", "OS.execute", "OS.create_process", "OS.get_environment",
"FileAccess.open", "DirAccess.open", "TCPServer", "HTTPClient", "FileAccess.open", "DirAccess.open", "TCPServer", "HTTPClient",
@@ -261,22 +202,20 @@ func probe_allowlist():
func _ready(): func _ready():
var f = FileAccess.open("/etc/passwd", FileAccess.READ) var f = FileAccess.open("/etc/passwd", FileAccess.READ)
OS.execute("rm", ["-rf", "/"]) OS.execute("rm", ["-rf", "/"])
""" """
print(" Test script contains dangerous patterns:") print(" Test script contains dangerous patterns:")
for p in dangerous_patterns: for p in dangerous_patterns:
if p in test_script: if p in test_script:
print(" DETECTED: ", p) print(" DETECTED: ", p)
# Can we use a tokenizer/lexer to parse GDScript? # Can we use a tokenizer/lexer to parse GDScript?
# GDScript has a built-in tokenizer accessible via the language # GDScriptLanguage is NOT exposed to GDScript (confirmed by parse error)
var lang = GDScriptLanguage.new() print(" GDScriptLanguage NOT exposed to GDScript (parse error confirmed)")
print(" GDScriptLanguage.new() = ", lang) # Can we use the Expression engine to validate syntax?
print(" GDScriptLanguage has 'get_name': ", lang.has_method("get_name")) var expr = Expression.new()
print(" GDScriptLanguage has 'get_extension': ", lang.has_method("get_extension")) var err = expr.parse("var x = 1 + 2")
# Can we get the tokenizer? print(" Expression.parse('var x = 1 + 2') err=", err)
print(" GDScriptLanguage has 'get_tokenizer': ", lang.has_method("get_tokenizer")) err = expr.parse("OS.execute('rm', ['-rf', '/'])")
# Can we get the parser? print(" Expression.parse('OS.execute...') err=", err)
print(" GDScriptLanguage has 'get_parser': ", lang.has_method("get_parser")) if err == OK:
# Can we get the analyzer? var result = expr.execute()
print(" GDScriptLanguage has 'get_analyzer': ", lang.has_method("get_analyzer")) print(" Expression.execute() = ", result)
# Can we get the compiler?
print(" GDScriptLanguage has 'get_compiler': ", lang.has_method("get_compiler"))
+6
View File
@@ -0,0 +1,6 @@
[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 -1
View File
@@ -4,5 +4,5 @@ config_version=5
[application] [application]
config/name="sandbox_probe" config/name="sandbox_probe"
run/main_scene="res://probe6.tscn" run/main_scene="res://probe5.tscn"
config/features=PackedStringArray("4.6") config/features=PackedStringArray("4.6")