feat: require plugin permissions confirmation

This commit is contained in:
Alexander Medvedev 2026-05-02 18:24:52 +02:00
parent bae072254f
commit f683de2bf8
7 changed files with 167 additions and 24 deletions

1
Cargo.lock generated
View File

@ -2793,6 +2793,7 @@ dependencies = [
"base64 0.22.1",
"bitflags 2.11.1",
"bytes",
"colored",
"console-subscriber",
"crossbeam",
"dashmap",

View File

@ -52,6 +52,8 @@ serde.workspace = true
serde_repr.workspace = true
serde_json.workspace = true
colored.workspace = true
bytes.workspace = true
rand.workspace = true

View File

@ -220,21 +220,6 @@ impl PumpkinServer {
let rcon = server.advanced_config.networking.rcon.clone();
if server.advanced_config.commands.use_console
&& let Some((wrapper, _, _)) = LOGGER_IMPL.wait()
{
if let Some(rl) = wrapper.take_readline() {
setup_console(rl, server.clone());
} else {
if server.advanced_config.commands.use_tty {
warn!(
"The input is not a TTY; falling back to simple logger and ignoring `use_tty` setting"
);
}
setup_stdin_console(server.clone());
}
}
if rcon.enabled {
warn!(
"RCON is enabled, but it's highly insecure as it transmits passwords and commands in plain text. This makes it vulnerable to interception and exploitation by anyone on the network"
@ -324,7 +309,7 @@ impl PumpkinServer {
}
}
pub async fn init_plugins(&self) {
pub async fn init_plugins(&self) -> std::time::Duration {
self.server
.plugin_manager
.set_self_ref(self.server.plugin_manager.clone())
@ -333,8 +318,12 @@ impl PumpkinServer {
.plugin_manager
.set_server(self.server.clone())
.await;
if let Err(err) = self.server.plugin_manager.load_plugins().await {
match self.server.plugin_manager.load_plugins().await {
Ok(duration) => duration,
Err(err) => {
error!("{err}");
std::time::Duration::ZERO
}
}
}
@ -347,6 +336,21 @@ impl PumpkinServer {
}
pub async fn start(&self) {
if self.server.advanced_config.commands.use_console
&& let Some((wrapper, _, _)) = LOGGER_IMPL.wait()
{
if let Some(rl) = wrapper.take_readline() {
setup_console(rl, self.server.clone());
} else {
if self.server.advanced_config.commands.use_tty {
warn!(
"The input is not a TTY; falling back to simple logger and ignoring `use_tty` setting"
);
}
setup_stdin_console(self.server.clone());
}
}
let tasks = Arc::new(TaskTracker::new());
let mut master_client_id: u64 = 0;
let bedrock_clients = Arc::new(Mutex::new(HashMap::new()));

View File

@ -325,7 +325,7 @@ impl ReadlineLogWrapper {
// This isn't really dead code. It is just only used by the lib and not the bin for this
// crate, and as such creates a compiler warning.
#[allow(dead_code)]
pub(crate) fn return_readline(&self, rl: Editor<PumpkinCommandCompleter, FileHistory>) {
pub fn return_readline(&self, rl: Editor<PumpkinCommandCompleter, FileHistory>) {
if let Ok(mut result) = self.readline.lock() {
let _ = result.insert(rl);
}

View File

@ -121,11 +121,13 @@ async fn main() {
});
let pumpkin_server = PumpkinServer::new(basic_config, advanced_config, vanilla_data).await;
pumpkin_server.init_plugins().await;
let plugin_wait_time = pumpkin_server.init_plugins().await;
let time_elapsed = time.elapsed().saturating_sub(plugin_wait_time);
info!(
"Started server; took {}",
TextComponent::text(format!("{}ms", time.elapsed().as_millis()))
TextComponent::text(format!("{}ms", time_elapsed.as_millis()))
.color_named(NamedColor::Gold)
.to_pretty_console()
);

View File

@ -466,6 +466,70 @@ impl PluginManager {
Ok(sorted)
}
/// Ask the server owner if they allow the permissions requested by a plugin
#[expect(clippy::print_stdout)]
fn ask_permission_confirmation(metadata: &PluginMetadata) -> (bool, std::time::Duration) {
use colored::Colorize;
use rustyline::DefaultEditor;
if metadata.permissions.is_empty() {
return (true, std::time::Duration::ZERO);
}
let start_time = std::time::Instant::now();
println!(
"\n{} \"{}\" ({}) requests the following permissions:",
"Plugin".bold(),
metadata.name.cyan(),
metadata.version.green()
);
for permission in &metadata.permissions {
if let Some(description) = permissions::get_permission_description(permission) {
println!(
" - {}: {}",
permission.yellow().bold(),
description.italic()
);
} else {
println!(" - {}", permission.yellow().bold());
}
}
let prompt = format!(
"\n{} [Y/n]: ",
"Do you want to allow these permissions and load the plugin?".bold()
);
let mut rl_taken = if let Some(logger_option) = crate::LOGGER_IMPL.get()
&& let Some((wrapper, _, _)) = logger_option
&& let Some(rl) = wrapper.take_readline()
{
Some((wrapper, rl))
} else {
None
};
let result = if let Some((_, ref mut rl)) = rl_taken {
rl.readline(&prompt).is_ok_and(|line| {
let input = line.trim().to_lowercase();
input == "y" || input == "yes"
})
} else {
let mut rl = DefaultEditor::new().expect("Failed to create rustyline editor");
rl.readline(&prompt).is_ok_and(|line| {
let input = line.trim().to_lowercase();
input == "y" || input == "yes"
})
};
if let Some((wrapper, rl)) = rl_taken {
wrapper.return_readline(rl);
}
(result, start_time.elapsed())
}
/// Spawn initialization for a single plugin
#[expect(clippy::too_many_lines)]
async fn spawn_plugin_initialization(
@ -599,12 +663,12 @@ impl PluginManager {
}
/// Load all plugins from the plugin directory
pub async fn load_plugins(&self) -> Result<(), ManagerError> {
pub async fn load_plugins(&self) -> Result<std::time::Duration, ManagerError> {
let path = Path::new(PLUGIN_DIR);
if !path.exists() {
std::fs::create_dir(path)?;
return Ok(());
return Ok(std::time::Duration::ZERO);
}
let mut prepared_plugins = Vec::new();
@ -677,9 +741,22 @@ impl PluginManager {
.map(|(i, m, d, l, p)| (m.name.clone(), (i, m, d, l, p)))
.collect();
let mut total_wait_time = std::time::Duration::ZERO;
for name in sorted_names {
if let Some((instance, metadata, loader_data, loader, path)) = plugins_map.remove(&name)
{
let (allowed, wait_time) = Self::ask_permission_confirmation(&metadata);
total_wait_time += wait_time;
if !allowed {
warn!(
"Permission denied for plugin \"{}\", skipping loading.",
metadata.name
);
continue;
}
match self
.spawn_plugin_initialization(instance, metadata, loader_data, loader, path)
.await
@ -695,7 +772,7 @@ impl PluginManager {
}
}
Ok(())
Ok(total_wait_time)
}
/// Start loading a plugin asynchronously
@ -706,6 +783,18 @@ impl PluginManager {
for loader in self.loaders.read().await.iter() {
if loader.can_load(path) {
let (instance, metadata, loader_data) = loader.load(path).await?;
let (allowed, _wait_time) = Self::ask_permission_confirmation(&metadata);
if !allowed {
warn!(
"Permission denied for plugin \"{}\", skipping loading.",
metadata.name
);
return Err(ManagerError::LoaderError(LoaderError::RuntimeError(
"Permission denied".to_string(),
)));
}
return self
.spawn_plugin_initialization(
instance,

View File

@ -64,3 +64,48 @@ pub const SYS_INFO_RAM: &str = "sys.info.ram";
/// Allows the plugin to read OS information.
pub const SYS_INFO_OS: &str = "sys.info.os";
#[must_use]
pub fn get_permission_description(permission: &str) -> Option<&'static str> {
match permission {
NETWORK_DNS => Some(
"Allows the plugin to perform DNS resolution (resolving hostnames to IP addresses).",
),
NETWORK_TCP => Some("Allows the plugin to use TCP sockets."),
NETWORK_UDP => Some("Allows the plugin to use UDP sockets."),
NETWORK_TCP_CONNECT => Some("Allows the plugin to initiate TCP connections."),
NETWORK_TCP_BIND => {
Some("Allows the plugin to bind TCP listeners (accept inbound connections).")
}
NETWORK_UDP_CONNECT => {
Some("Allows the plugin to send and receive UDP packets to specific destinations.")
}
NETWORK_UDP_BIND => Some("Allows the plugin to bind UDP sockets to local ports."),
NETWORK_UDP_OUTGOING_DATAGRAM => {
Some("Allows the plugin to send datagram on non-connected UDP socket.")
}
NETWORK_LOOPBACK => {
Some("Restricts all networking permissions to loopback addresses (localhost) only.")
}
NETWORK_OUTBOUND => {
Some("Allows the plugin to make outbound TCP/UDP connections. (POWERFUL)")
}
FS_READ => Some(
"Allows the plugin to read files from the server's file system outside of its data folder.",
),
FS_WRITE => Some(
"Allows the plugin to write files to the server's file system outside of its data folder.",
),
FS_READ_DATA => Some("Allows the plugin to read files within its own data folder."),
FS_WRITE_DATA => Some("Allows the plugin to write files within its own data folder."),
SYS_ENV => Some("Allows the plugin to read all environment variables."),
SYS_INFO => Some("Allows the plugin to read system information (CPU, Memory, OS)."),
SYS_INFO_CPU => Some("Allows the plugin to read CPU information."),
SYS_INFO_RAM => Some("Allows the plugin to read RAM information."),
SYS_INFO_OS => Some("Allows the plugin to read OS information."),
p if p.starts_with(SYS_ENV_PREFIX) => {
Some("Allows the plugin to read specific environment variables.")
}
_ => None,
}
}