From 8f98399ea9d6d008683bef5555c4fe453690d9ed Mon Sep 17 00:00:00 2001 From: trivernis Date: Sat, 10 Apr 2021 11:22:49 +0200 Subject: [PATCH] Sanitize input of qalculate Signed-off-by: trivernis --- src/commands/misc/qalc.rs | 31 +++++++++++++++++-- src/providers/music/youtube_dl.rs | 2 +- src/providers/qalc.rs | 5 +-- src/utils/error.rs | 3 ++ src/utils/mod.rs | 28 ++--------------- src/utils/process.rs | 51 +++++++++++++++++++++++++++++++ 6 files changed, 89 insertions(+), 31 deletions(-) create mode 100644 src/utils/process.rs diff --git a/src/commands/misc/qalc.rs b/src/commands/misc/qalc.rs index e682764..788f3f7 100644 --- a/src/commands/misc/qalc.rs +++ b/src/commands/misc/qalc.rs @@ -1,9 +1,12 @@ use crate::providers::qalc; +use regex::Regex; use serenity::client::Context; use serenity::framework::standard::macros::command; use serenity::framework::standard::{Args, CommandResult}; use serenity::model::channel::Message; +static QALC_HELP: &[&str] = &["help", "--help", "-h", "h"]; + #[command] #[description("Calculates an expression")] #[min_args(1)] @@ -11,8 +14,32 @@ use serenity::model::channel::Message; #[example("1 * 1 + 1 / sqrt(2)")] async fn qalc(ctx: &Context, msg: &Message, args: Args) -> CommandResult { let expression = args.message(); - let result = qalc::qalc(expression).await?; - msg.reply(ctx, format!("`{}`", result)).await?; + lazy_static::lazy_static! { + static ref ERROR_REGEX: Regex = Regex::new(r"^error:.*").unwrap(); + } + + if QALC_HELP.contains(&expression) { + msg.channel_id.send_message(ctx, |f| { + f.embed(|e| e.title("Help").description("Read the [Qalculate! Manual](https://qalculate.github.io/manual/index.html)")) + }).await?; + } else { + let result = qalc::qalc(expression).await?; + let mut description = format!("`{}`", result); + + if ERROR_REGEX.is_match(&result) { + description += + "\nRead the [Qalculate! Manual](https://qalculate.github.io/manual/index.html)"; + } + + msg.channel_id + .send_message(ctx, |f| { + f.embed(|e| { + e.description(description) + .footer(|f| f.text("Powered by qalculate!")) + }) + }) + .await?; + } Ok(()) } diff --git a/src/providers/music/youtube_dl.rs b/src/providers/music/youtube_dl.rs index 1cd6299..a45b19e 100644 --- a/src/providers/music/youtube_dl.rs +++ b/src/providers/music/youtube_dl.rs @@ -1,7 +1,7 @@ use crate::providers::music::queue::Song; use crate::providers::music::responses::{PlaylistEntry, VideoInformation}; use crate::utils::error::BotResult; -use crate::utils::run_command_async; +use crate::utils::process::run_command_async; use futures::future::BoxFuture; use futures::FutureExt; use std::sync::atomic::{AtomicU8, Ordering}; diff --git a/src/providers/qalc.rs b/src/providers/qalc.rs index 52225b2..a184e31 100644 --- a/src/providers/qalc.rs +++ b/src/providers/qalc.rs @@ -1,8 +1,9 @@ use crate::utils::error::BotResult; -use crate::utils::run_command_async; +use crate::utils::process::{run_command_async, sanitize_argument}; /// Runs the qalc command with the given expression pub async fn qalc(expression: &str) -> BotResult { - let result = run_command_async("qalc", &[expression]).await?; + let expression = sanitize_argument(expression, true)?; + let result = run_command_async("qalc", &[&*expression]).await?; Ok(result) } diff --git a/src/utils/error.rs b/src/utils/error.rs index 8445d8e..1666014 100644 --- a/src/utils/error.rs +++ b/src/utils/error.rs @@ -28,6 +28,9 @@ pub enum BotError { #[error("Reqwest Error: {0}")] Reqwest(#[from] reqwest::Error), + #[error("Detected CLI injection attempt")] + CliInject, + #[error("{0}")] Msg(String), } diff --git a/src/utils/mod.rs b/src/utils/mod.rs index e6ef530..89ef7cb 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,15 +1,11 @@ -use std::collections::VecDeque; - use rand::Rng; -use std::io; -use std::process::Stdio; -use tokio::io::AsyncReadExt; -use tokio::process::Command; +use std::collections::VecDeque; pub(crate) mod context_data; pub(crate) mod error; pub(crate) mod logging; pub(crate) mod messages; +pub(crate) mod process; /// Fisher-Yates shuffle for VecDeque pub fn shuffle_vec_deque(deque: &mut VecDeque) { @@ -20,23 +16,3 @@ pub fn shuffle_vec_deque(deque: &mut VecDeque) { deque.swap(i, rng.gen_range(0..i + 1)) } } - -/// Asynchronously runs a given command and returns the output -pub async fn run_command_async(command: &str, args: &[&str]) -> io::Result { - log::trace!("Running command '{}' with args {:?}", command, args); - let cmd = Command::new(command) - .args(args) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn()?; - let mut stderr = String::new(); - let mut output = String::new(); - cmd.stderr.unwrap().read_to_string(&mut stderr).await?; - if stderr.len() != 0 { - log::debug!("STDERR of command {}: {}", command, stderr); - } - cmd.stdout.unwrap().read_to_string(&mut output).await?; - log::trace!("Command output is {}", output); - - Ok(output) -} diff --git a/src/utils/process.rs b/src/utils/process.rs new file mode 100644 index 0000000..750bb20 --- /dev/null +++ b/src/utils/process.rs @@ -0,0 +1,51 @@ +use crate::utils::error::{BotError, BotResult}; +use regex::Regex; +use std::io; +use std::process::Stdio; +use tokio::io::AsyncReadExt; +use tokio::process::Command; + +/// Asynchronously runs a given command and returns the output +pub async fn run_command_async(command: &str, args: &[&str]) -> io::Result { + log::trace!("Running command '{}' with args {:?}", command, args); + let cmd = Command::new(command) + .args(args) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn()?; + let mut stderr = String::new(); + let mut output = String::new(); + cmd.stderr.unwrap().read_to_string(&mut stderr).await?; + if stderr.len() != 0 { + log::debug!("STDERR of command {}: {}", command, stderr); + } + cmd.stdout.unwrap().read_to_string(&mut output).await?; + log::trace!("Command output is {}", output); + + Ok(output) +} + +/// Sanitizes a command line argument and throws an error +/// on a possible injection attempt +pub fn sanitize_argument(arg: &str, detect_help: bool) -> BotResult { + log::debug!("Sanitizing argument '{}'", arg); + lazy_static::lazy_static! { + static ref HELP_FLAG: Regex = Regex::new(r"^\s*(-*)h(elp)?\s*$").unwrap(); + static ref FLAG_REGEX: Regex = Regex::new(r"^\s*-\w*\s*$").unwrap(); + } + if FLAG_REGEX.is_match(arg) { + log::debug!("Detected STDIN injection"); + return Err(BotError::CliInject); + } + if detect_help && HELP_FLAG.is_match(arg) { + log::debug!("Detected help injection"); + return Err(BotError::CliInject); + } + let arg = arg.replace("--", "\\-\\-"); + if arg.is_empty() { + return Err(BotError::CliInject); + } + log::debug!("Sanitized argument is '{}'", arg); + + Ok(arg) +}