feat(dtmm): Guard certain Lua libraries behind a setting
All checks were successful
lint/clippy Checking for common mistakes and opportunities for code improvement
build/msvc Build for the target platform: msvc
build/linux Build for the target platform: linux

Libraries like `io`, `os` and `ffi` allow practically unrestricted
access to the system's files and running arbitrary operations.
The base game removes them for this reason, and while we don't want to
disable them permanently, very few mods should ever have a need for
them.

So we hide them behind a setting, worded so that people only enable it
when absolutely needed.

Closes #112.
This commit is contained in:
Lucas Schwiderski 2023-04-24 16:45:49 +02:00
parent af6c2e9f82
commit 707a3ead8b
Signed by: lucas
GPG key ID: AA12679AAA6DF4D8
9 changed files with 85 additions and 14 deletions

1
Cargo.lock generated
View file

@ -855,6 +855,7 @@ dependencies = [
"sdk",
"serde",
"serde_sjson",
"string_template",
"strip-ansi-escapes",
"time",
"tokio",

View file

@ -32,3 +32,4 @@ colors-transform = "0.2.11"
usvg = "0.25.0"
druid-widget-nursery = "0.1"
ansi-parser = "0.8.0"
string_template = "0.2.1"

View file

@ -107,19 +107,35 @@ log("mod_main", "Initializing mods...")
local require_store = {}
-- This token is treated as a string template and filled by DTMM during deployment.
-- This allows hiding unsafe I/O functions behind a setting.
-- It's also a valid table definition, thereby degrading gracefully when not replaced.
local is_io_enabled = {{is_io_enabled}} -- luacheck: ignore 113
local lua_libs = {
debug = debug,
os = {
date = os.date,
time = os.time,
clock = os.clock,
getenv = os.getenv,
difftime = os.difftime,
},
load = load,
loadfile = loadfile,
loadstring = loadstring,
}
if is_io_enabled then
lua_libs.io = io
lua_libs.os = os
lua_libs.ffi = ffi
end
Mods = {
-- Keep a backup of certain system libraries before
-- Fatshark's code scrubs them.
-- The loader can then decide to pass them on to mods, or ignore them
lua = setmetatable({}, {
io = io,
debug = debug,
ffi = ffi,
os = os,
load = load,
loadfile = loadfile,
loadstring = loadstring,
}),
lua = setmetatable({}, { __index = lua_libs }),
require_store = require_store
}

View file

@ -1,3 +1,4 @@
use std::collections::HashMap;
use std::io::{self, Cursor, ErrorKind};
use std::path::{Path, PathBuf};
use std::str::FromStr;
@ -15,6 +16,7 @@ use sdk::{
Bundle, BundleDatabase, BundleFile, BundleFileType, BundleFileVariant, FromBinary, ToBinary,
};
use serde::{Deserialize, Serialize};
use string_template::Template;
use time::OffsetDateTime;
use tokio::fs;
use tokio::io::AsyncWriteExt;
@ -452,7 +454,13 @@ async fn patch_boot_bundle(state: Arc<ActionState>) -> Result<Vec<Bundle>> {
let span = tracing::debug_span!("Importing mod main script");
let _enter = span.enter();
let lua = include_str!("../../assets/mod_main.lua");
let is_io_enabled = format!("{}", state.is_io_enabled);
let mut data = HashMap::new();
data.insert("is_io_enabled", is_io_enabled.as_str());
let tmpl = include_str!("../../assets/mod_main.lua");
let lua = Template::new(tmpl).render(&data);
tracing::trace!("Main script rendered:\n===========\n{}\n=============", lua);
let file =
lua::compile(MOD_BOOT_SCRIPT, lua).wrap_err("Failed to compile mod main Lua file")?;

View file

@ -153,6 +153,7 @@ pub(crate) struct State {
pub is_save_in_progress: bool,
pub is_next_save_pending: bool,
pub is_update_in_progress: bool,
pub is_io_enabled: bool,
pub game_dir: Arc<PathBuf>,
pub data_dir: Arc<PathBuf>,
pub nexus_api_key: Arc<String>,
@ -189,6 +190,7 @@ impl State {
is_save_in_progress: false,
is_next_save_pending: false,
is_update_in_progress: false,
is_io_enabled: false,
config_path: Arc::new(PathBuf::new()),
game_dir: Arc::new(PathBuf::new()),
data_dir: Arc::new(PathBuf::new()),

View file

@ -1,4 +1,5 @@
use std::{path::PathBuf, sync::Arc};
use std::path::PathBuf;
use std::sync::Arc;
use color_eyre::Report;
use druid::im::Vector;
@ -8,8 +9,9 @@ use druid::{
};
use tokio::sync::mpsc::UnboundedSender;
use crate::ui::window;
use crate::util::ansi::ansi_to_rich_text;
use crate::{ui::window, util::config::Config};
use crate::util::config::Config;
use super::{ModInfo, State};
@ -68,6 +70,7 @@ pub(crate) struct ActionState {
pub config_path: Arc<PathBuf>,
pub ctx: Arc<sdk::Context>,
pub nexus_api_key: Arc<String>,
pub is_io_enabled: bool,
}
impl From<State> for ActionState {
@ -80,6 +83,7 @@ impl From<State> for ActionState {
config_path: state.config_path,
ctx: state.ctx,
nexus_api_key: state.nexus_api_key,
is_io_enabled: state.is_io_enabled,
}
}
}
@ -407,6 +411,7 @@ impl AppDelegate<State> for Delegate {
state.config_path = Arc::new(config.path);
state.data_dir = Arc::new(config.data_dir);
state.game_dir = Arc::new(config.game_dir.unwrap_or_default());
state.is_io_enabled = config.unsafe_io;
}
state.loading = false;

View file

@ -80,11 +80,19 @@ impl<W: Widget<State>> Controller<State, W> for DirtyStateController {
) {
// Only start tracking changes after the initial load has finished
if old_data.loading == data.loading {
if compare_state_fields!(old_data, data, mods, game_dir, data_dir, nexus_api_key) {
if compare_state_fields!(
old_data,
data,
mods,
game_dir,
data_dir,
nexus_api_key,
is_io_enabled
) {
ctx.submit_command(ACTION_START_SAVE_SETTINGS);
}
if compare_state_fields!(old_data, data, mods, game_dir) {
if compare_state_fields!(old_data, data, mods, game_dir, is_io_enabled) {
ctx.submit_command(ACTION_SET_DIRTY);
}
}

View file

@ -425,6 +425,29 @@ fn build_view_settings() -> impl Widget<State> {
.with_flex_child(TextBox::new().expand_width().lens(State::nexus_api_key), 1.)
.expand_width();
let io_setting = Flex::row()
.must_fill_main_axis(true)
.main_axis_alignment(MainAxisAlignment::Start)
.with_child(Label::new("Enable unsafe I/O:"))
.with_default_spacer()
.with_child(Checkbox::from_label(Label::dynamic(
|enabled: &bool, _: &Env| {
if *enabled {
"Enabled".into()
} else {
"Disabled".into()
}
},
)))
.lens(State::is_io_enabled)
.tooltip(|_: &State, _: &Env| {
"Enabling this gives ANY mod full access to your files \
and the ability to load arbitrary software libraries.\n\
Only enable this if it is crucial for a mod's functionality, \
and you are sure none of the ones you have installed are malicious."
})
.expand_width();
let content = Flex::column()
.must_fill_main_axis(true)
.cross_axis_alignment(CrossAxisAlignment::Start)
@ -432,6 +455,8 @@ fn build_view_settings() -> impl Widget<State> {
.with_default_spacer()
.with_child(game_dir_setting)
.with_default_spacer()
.with_child(io_setting)
.with_default_spacer()
.with_child(nexus_apy_key_setting);
SizedBox::new(content)

View file

@ -30,6 +30,7 @@ pub(crate) struct ConfigSerialize<'a> {
data_dir: &'a Path,
nexus_api_key: &'a String,
mod_order: Vec<LoadOrderEntrySerialize<'a>>,
unsafe_io: bool,
}
impl<'a> From<&'a ActionState> for ConfigSerialize<'a> {
@ -38,6 +39,7 @@ impl<'a> From<&'a ActionState> for ConfigSerialize<'a> {
game_dir: &state.game_dir,
data_dir: &state.data_dir,
nexus_api_key: &state.nexus_api_key,
unsafe_io: state.is_io_enabled,
mod_order: state
.mods
.iter()
@ -61,6 +63,8 @@ pub(crate) struct Config {
#[serde(default = "get_default_data_dir")]
pub data_dir: PathBuf,
pub game_dir: Option<PathBuf>,
#[serde(default)]
pub unsafe_io: bool,
pub nexus_api_key: Option<String>,
#[serde(default)]
pub mod_order: Vec<LoadOrderEntry>,
@ -144,6 +148,7 @@ where
game_dir: None,
nexus_api_key: None,
mod_order: Vec::new(),
unsafe_io: false,
};
{