Added some initial work on checking safe files for malicious contents

This commit is contained in:
AyurooLee 2026-06-25 22:21:17 +02:00
parent 3080806d8c
commit 266b5b7e4f
6 changed files with 76 additions and 6 deletions

View File

@ -2,3 +2,4 @@ class_name SaveData
extends Resource extends Resource
@export_file("*.tscn", "*.scn") var current_world: String @export_file("*.tscn", "*.scn") var current_world: String
@export var com_hash: int = -1 # computer hash for checking, if this came from another source.

View File

@ -103,6 +103,8 @@ func update_world_state() -> void:
world_state.loaded_level_ids = get_loaded_level_ids() world_state.loaded_level_ids = get_loaded_level_ids()
world_state.com_hash = Game.get_computer_hash()
func get_level_ids_from_streamers(streamers: Array[LevelStreamer]) -> Array[StringName]: func get_level_ids_from_streamers(streamers: Array[LevelStreamer]) -> Array[StringName]:
var streamer_ids: Array[StringName] = [] var streamer_ids: Array[StringName] = []

View File

@ -5,3 +5,4 @@ extends Resource
@export_file("*.tscn", "*.scn") var world_path: String @export_file("*.tscn", "*.scn") var world_path: String
@export var loaded_level_ids: Array[StringName] = [] @export var loaded_level_ids: Array[StringName] = []
@export var instance_data: Dictionary[String, Variant] = {} @export var instance_data: Dictionary[String, Variant] = {}
@export var com_hash: int = -1 # computer hash for checking, if this came from another source.

View File

@ -22,7 +22,7 @@ var main_menu: MainMenu
var _os_dialog_window_result: int = -1 var _os_dialog_window_result: int = -1
@onready var world_holder: Node = %WorldHolder @onready var scene_holder: Node = %SceneHolder
@onready var loading_screen: LoadingScreen = %LoadingScreen @onready var loading_screen: LoadingScreen = %LoadingScreen
@ -34,6 +34,19 @@ static func debug_draw(draw_shape: String, args: Array) -> void:
const DD3D: StringName = &"DebugDraw3D" const DD3D: StringName = &"DebugDraw3D"
if is_debug and Engine.has_singleton(DD3D): if is_debug and Engine.has_singleton(DD3D):
Engine.get_singleton(DD3D).callv("draw_" + draw_shape, args) Engine.get_singleton(DD3D).callv("draw_" + draw_shape, args)
static func get_computer_hash() -> int:
var combined_str: String = str(
OS.get_processor_name(),
OS.get_processor_count(),
OS.get_model_name(),
OS.get_name(),
)
return combined_str.hash()
## Turns res://example.tscn into example.tres (and .scn -> .res) and uid://1234567 into 1234567.tres. ## Turns res://example.tscn into example.tres (and .scn -> .res) and uid://1234567 into 1234567.tres.
static func convert_path_to_resource_filename(path: String) -> String: static func convert_path_to_resource_filename(path: String) -> String:
path = ResourceUID.path_to_uid(path) path = ResourceUID.path_to_uid(path)
@ -50,12 +63,37 @@ func _ready() -> void:
register_commands() register_commands()
func check_computer_hash(input_hash: int) -> bool:
if input_hash == Game.get_computer_hash():
return true
_os_dialog_window_result = -1
var _on_dialog_interacted: Callable = func(button_id: int) -> void:
_os_dialog_window_result = button_id
DisplayServer.dialog_show(
"Save Hash Mismatch",
"The save you are trying to load seems to come from a different computer.\nWe are not responsible for any damages to your computer if the save is malicious.\n\nDo you want to load the save anyway?",
["Abort", "Continue"], _on_dialog_interacted)
get_tree().root.process_mode = Node.PROCESS_MODE_DISABLED
while _os_dialog_window_result == -1:
await get_tree().process_frame
get_tree().root.process_mode = Node.PROCESS_MODE_PAUSABLE
return _os_dialog_window_result != 0
func load_game(_save_data: SaveData, _save_slot: int) -> void: func load_game(_save_data: SaveData, _save_slot: int) -> void:
if not await check_computer_hash(save_data.com_hash): if not await check_computer_hash(_save_data.com_hash):
return return
save_data = _save_data.duplicate() save_data = _save_data.duplicate()
save_slot = _save_slot save_slot = _save_slot
InputManager.capture_mouse() InputManager.capture_mouse()
load_world(save_data.current_world, false) load_world(save_data.current_world, false)
@ -69,6 +107,8 @@ func save_game() -> Error:
if is_instance_valid(world): if is_instance_valid(world):
save_data.current_world = ResourceUID.path_to_uid(world.scene_file_path) save_data.current_world = ResourceUID.path_to_uid(world.scene_file_path)
save_data.com_hash = Game.get_computer_hash()
DirAccess.make_dir_recursive_absolute(SAVE_DIR % save_slot) DirAccess.make_dir_recursive_absolute(SAVE_DIR % save_slot)
return ResourceSaver.save(save_data, SAVE_PATH % [save_slot, file_name]) return ResourceSaver.save(save_data, SAVE_PATH % [save_slot, file_name])
@ -97,7 +137,7 @@ func load_world(world_path: String, save_previous: bool = true, load_from_save:
world.change_world_requested.connect(load_world) world.change_world_requested.connect(load_world)
world.world_unload_requested.connect(_on_unload_world_request) world.world_unload_requested.connect(_on_unload_world_request)
world_holder.add_child.call_deferred(world) scene_holder.add_child.call_deferred(world)
await world.loaded await world.loaded
loading_screen.fade_out() loading_screen.fade_out()
return OK return OK
@ -144,7 +184,7 @@ func load_main_menu() -> void:
_main_menu.load_game_request.connect(load_game) _main_menu.load_game_request.connect(load_game)
_main_menu.quit_request.connect(quit_game) _main_menu.quit_request.connect(quit_game)
main_menu = _main_menu main_menu = _main_menu
world_holder.add_child(main_menu) scene_holder.add_child(main_menu)
func load_startup() -> void: func load_startup() -> void:
@ -155,7 +195,7 @@ func load_startup() -> void:
load_main_menu() load_main_menu()
loading_screen.fade_out() loading_screen.fade_out()
) )
add_child(startup) scene_holder.add_child(startup)
func unload_main_menu() -> void: func unload_main_menu() -> void:
@ -170,12 +210,14 @@ func quit_game() -> void:
func register_commands() -> void: func register_commands() -> void:
var load_save_func: Callable = func(save_index: int) -> void: var load_save_func: Callable = func(save_index: int) -> void:
print("Loading Save index ", save_index)
var path: String = MainMenu.get_save_path(save_index) var path: String = MainMenu.get_save_path(save_index)
if ResourceLoader.exists(path): if ResourceLoader.exists(path):
load_game(load(path), save_index) load_game(load(path), save_index)
LimboConsole.register_command(load_save_func, "load_save", "Loads the save with the given save slot index.") LimboConsole.register_command(load_save_func, "load_save", "Loads the save with the given save slot index.")
LimboConsole.register_command(save_game, "save_game", "Saves the game based on the current save slot.") LimboConsole.register_command(save_game, "save_game", "Saves the game based on the current save slot.")
#DebugConsole.register_command("load_save", load_save_func, "Loads the save with the given save slot index.")
func _on_unload_world_request(do_save: bool) -> void: func _on_unload_world_request(do_save: bool) -> void:

View File

@ -7,7 +7,7 @@
script = ExtResource("1_7uq6d") script = ExtResource("1_7uq6d")
metadata/_custom_type_script = "uid://cl1u038dbrou2" metadata/_custom_type_script = "uid://cl1u038dbrou2"
[node name="WorldHolder" type="Node" parent="." unique_id=1835125942] [node name="SceneHolder" type="Node" parent="." unique_id=1835125942]
unique_name_in_owner = true unique_name_in_owner = true
[node name="CanvasLayer" type="CanvasLayer" parent="." unique_id=919699501] [node name="CanvasLayer" type="CanvasLayer" parent="." unique_id=919699501]

View File

@ -49,6 +49,16 @@ static func get_save_path(slot_index: int) -> String:
return SAVE_DATA_PATH % slot_index return SAVE_DATA_PATH % slot_index
static func does_save_contents_contain_code(save_path: String) -> bool:
var file := FileAccess.open(save_path, FileAccess.READ)
var contents: String = file.get_as_text()
if contents.containsn("func") or contents.containsn("script/source") or contents.containsn("_init"):
OS.alert("The save you are trying to load contains malicious code.\nAborting...", "Save File Warning")
return true
return false
func _ready() -> void: func _ready() -> void:
continue_button.pressed.connect(_on_continue_pressed) continue_button.pressed.connect(_on_continue_pressed)
load_button.pressed.connect(_on_load_pressed) load_button.pressed.connect(_on_load_pressed)
@ -98,6 +108,9 @@ func _on_continue_pressed() -> void:
_on_new_game_pressed() _on_new_game_pressed()
return return
if does_save_contents_contain_code(path):
return
config.set_value("save", "last_slot", slot) config.set_value("save", "last_slot", slot)
config.save(Game.USER_SETTINGS_PATH) config.save(Game.USER_SETTINGS_PATH)
@ -112,6 +125,8 @@ func _on_load_pressed() -> void:
func _on_new_game_pressed() -> void: func _on_new_game_pressed() -> void:
var save_data: SaveData = Game.INITIAL_SAVE_DATA.duplicate() var save_data: SaveData = Game.INITIAL_SAVE_DATA.duplicate()
save_data.com_hash = Game.get_computer_hash()
print("Save hash: ", save_data.com_hash, " Computer hash: ", Game.get_computer_hash())
var slot_index: int = get_unique_save_slot_index() var slot_index: int = get_unique_save_slot_index()
load_game_request.emit(save_data, slot_index) load_game_request.emit(save_data, slot_index)
@ -139,6 +154,15 @@ func _on_save_entry_pressed(save_slot: int) -> void:
_on_new_game_pressed() _on_new_game_pressed()
return return
# TODO: Check for hash before loading the file via 'load'
if does_save_contents_contain_code(save_path):
return
#var file := FileAccess.open(save_path, FileAccess.READ)
#var contents: String = file.get_as_text()
#var index: int = contents.find("com_hash = ")
#var hash_str: String = contents.get_slice("\n", 1)
#print("Hash: ", hash_str, " Index: ", index, "\nContents: ", contents)
var save_data: SaveData = load(save_path) var save_data: SaveData = load(save_path)
load_game_request.emit(save_data, save_slot) load_game_request.emit(save_data, save_slot)