Fix unbounded resource usage in crypto and rand SQL functions (#94)

This commit is contained in:
Finn Bear 2022-09-02 08:19:01 -07:00 committed by GitHub
parent 93dedd4869
commit 3d83f086a6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 126 additions and 34 deletions

View file

@ -77,7 +77,7 @@ pub enum Error {
message: String, message: String,
}, },
/// The wrong number of arguments was given for the specified function /// The wrong quantity or magnitude of arguments was given for the specified function
#[error("Incorrect arguments for function {name}(). {message}")] #[error("Incorrect arguments for function {name}(). {message}")]
InvalidArguments { InvalidArguments {
name: String, name: String,

View file

@ -39,23 +39,72 @@ pub fn sha512(_: &Context, mut args: Vec<Value>) -> Result<Value, Error> {
Ok(val.into()) Ok(val.into())
} }
/// Allowed to cost this much more than default setting for each hash function.
const COST_ALLOWANCE: u32 = 4;
/// Like verify_password, but takes a closure to determine whether the cost of performing the
/// operation is not too high.
macro_rules! bounded_verify_password {
($algo: ident, $instance: expr, $password: expr, $hash: expr, $bound: expr) => {
if let (Some(salt), Some(expected_output)) = (&$hash.salt, &$hash.hash) {
if let Some(params) =
<$algo as PasswordHasher>::Params::try_from($hash).ok().filter($bound)
{
if let Ok(computed_hash) = $instance.hash_password_customized(
$password.as_ref(),
Some($hash.algorithm),
$hash.version,
params,
*salt,
) {
if let Some(computed_output) = &computed_hash.hash {
expected_output == computed_output
} else {
false
}
} else {
false
}
} else {
false
}
} else {
false
}
};
($algo: ident, $password: expr, $hash: expr, $bound: expr) => {
bounded_verify_password!($algo, $algo::default(), $password, $hash, $bound)
};
}
pub mod argon2 { pub mod argon2 {
use super::COST_ALLOWANCE;
use crate::ctx::Context; use crate::ctx::Context;
use crate::err::Error; use crate::err::Error;
use crate::sql::value::Value; use crate::sql::value::Value;
use argon2::{ use argon2::{
password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString}, password_hash::{PasswordHash, PasswordHasher, SaltString},
Argon2, Argon2,
}; };
use rand::rngs::OsRng; use rand::rngs::OsRng;
pub fn cmp(_: &Context, mut args: Vec<Value>) -> Result<Value, Error> { pub fn cmp(_: &Context, args: Vec<Value>) -> Result<Value, Error> {
let algo = Argon2::default(); let args: [Value; 2] = args.try_into().unwrap();
let hash = args.remove(0).as_string(); let [hash, pass] = args.map(Value::as_string);
let pass = args.remove(0).as_string(); type Params<'a> = <Argon2<'a> as PasswordHasher>::Params;
let test = PasswordHash::new(&hash).unwrap(); Ok(PasswordHash::new(&hash)
Ok(algo.verify_password(pass.as_ref(), &test).is_ok().into()) .ok()
.filter(|test| {
bounded_verify_password!(Argon2, pass, test, |params: &Params| {
params.m_cost() <= Params::DEFAULT_M_COST.saturating_mul(COST_ALLOWANCE)
&& params.t_cost() <= Params::DEFAULT_T_COST.saturating_mul(COST_ALLOWANCE)
&& params.p_cost() <= Params::DEFAULT_P_COST.saturating_mul(COST_ALLOWANCE)
})
})
.is_some()
.into())
} }
pub fn gen(_: &Context, mut args: Vec<Value>) -> Result<Value, Error> { pub fn gen(_: &Context, mut args: Vec<Value>) -> Result<Value, Error> {
@ -69,20 +118,33 @@ pub mod argon2 {
pub mod pbkdf2 { pub mod pbkdf2 {
use super::COST_ALLOWANCE;
use crate::ctx::Context; use crate::ctx::Context;
use crate::err::Error; use crate::err::Error;
use crate::sql::value::Value; use crate::sql::value::Value;
use pbkdf2::{ use pbkdf2::{
password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString}, password_hash::{PasswordHash, PasswordHasher, SaltString},
Pbkdf2, Pbkdf2,
}; };
use rand::rngs::OsRng; use rand::rngs::OsRng;
pub fn cmp(_: &Context, mut args: Vec<Value>) -> Result<Value, Error> { pub fn cmp(_: &Context, args: Vec<Value>) -> Result<Value, Error> {
let hash = args.remove(0).as_string(); let args: [Value; 2] = args.try_into().unwrap();
let pass = args.remove(0).as_string(); let [hash, pass] = args.map(Value::as_string);
let test = PasswordHash::new(&hash).unwrap(); type Params = <Pbkdf2 as PasswordHasher>::Params;
Ok(Pbkdf2.verify_password(pass.as_ref(), &test).is_ok().into()) Ok(PasswordHash::new(&hash)
.ok()
.filter(|test| {
bounded_verify_password!(Pbkdf2, Pbkdf2, pass, test, |params: &Params| {
params.rounds <= Params::default().rounds.saturating_mul(COST_ALLOWANCE)
&& params.output_length
<= Params::default()
.output_length
.saturating_mul(COST_ALLOWANCE as usize)
})
})
.is_some()
.into())
} }
pub fn gen(_: &Context, mut args: Vec<Value>) -> Result<Value, Error> { pub fn gen(_: &Context, mut args: Vec<Value>) -> Result<Value, Error> {
@ -100,15 +162,26 @@ pub mod scrypt {
use crate::sql::value::Value; use crate::sql::value::Value;
use rand::rngs::OsRng; use rand::rngs::OsRng;
use scrypt::{ use scrypt::{
password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString}, password_hash::{PasswordHash, PasswordHasher, SaltString},
Scrypt, Scrypt,
}; };
pub fn cmp(_: &Context, mut args: Vec<Value>) -> Result<Value, Error> { pub fn cmp(_: &Context, args: Vec<Value>) -> Result<Value, Error> {
let hash = args.remove(0).as_string(); let args: [Value; 2] = args.try_into().unwrap();
let pass = args.remove(0).as_string(); let [hash, pass] = args.map(Value::as_string);
let test = PasswordHash::new(&hash).unwrap(); type Params = <Scrypt as PasswordHasher>::Params;
Ok(Scrypt.verify_password(pass.as_ref(), &test).is_ok().into()) Ok(PasswordHash::new(&hash)
.ok()
.filter(|test| {
bounded_verify_password!(Scrypt, Scrypt, pass, test, |params: &Params| {
// Scrypt is slow, use lower cost allowance.
params.log_n() <= Params::default().log_n().saturating_add(2)
&& params.r() <= Params::default().r().saturating_mul(2)
&& params.p() <= Params::default().p().saturating_mul(4)
})
})
.is_some()
.into())
} }
pub fn gen(_: &Context, mut args: Vec<Value>) -> Result<Value, Error> { pub fn gen(_: &Context, mut args: Vec<Value>) -> Result<Value, Error> {

View file

@ -45,9 +45,18 @@ pub fn float(_: &Context, mut args: Vec<Value>) -> Result<Value, Error> {
pub fn guid(_: &Context, mut args: Vec<Value>) -> Result<Value, Error> { pub fn guid(_: &Context, mut args: Vec<Value>) -> Result<Value, Error> {
match args.len() { match args.len() {
1 => { 1 => {
// Only need 53 to uniquely identify all atoms in observable universe.
const LIMIT: usize = 64;
let len = args.remove(0).as_int() as usize; let len = args.remove(0).as_int() as usize;
if len > LIMIT {
Err(Error::InvalidArguments {
name: String::from("rand::guid"),
message: format!("The maximum length of a GUID is {}.", LIMIT),
})
} else {
Ok(nanoid!(len, &ID_CHARS).into()) Ok(nanoid!(len, &ID_CHARS).into())
} }
}
0 => Ok(nanoid!(20, &ID_CHARS).into()), 0 => Ok(nanoid!(20, &ID_CHARS).into()),
_ => unreachable!(), _ => unreachable!(),
} }
@ -68,33 +77,35 @@ pub fn int(_: &Context, mut args: Vec<Value>) -> Result<Value, Error> {
} }
pub fn string(_: &Context, mut args: Vec<Value>) -> Result<Value, Error> { pub fn string(_: &Context, mut args: Vec<Value>) -> Result<Value, Error> {
// Limit how much time and bandwidth is spent.
const LIMIT: i64 = 2i64.pow(16);
match args.len() { match args.len() {
2 => match args.remove(0).as_int() { 2 => match args.remove(0).as_int() {
min if min >= 0 => match args.remove(0).as_int() { min if (0..=LIMIT).contains(&min) => match args.remove(0).as_int() {
max if max >= 0 && max < min => Ok(rand::thread_rng() max if min <= max && max <= LIMIT => Ok(rand::thread_rng()
.sample_iter(&Alphanumeric)
.take(rand::thread_rng().gen_range(max as usize..=min as usize))
.map(char::from)
.collect::<String>()
.into()),
max if max >= 0 => Ok(rand::thread_rng()
.sample_iter(&Alphanumeric) .sample_iter(&Alphanumeric)
.take(rand::thread_rng().gen_range(min as usize..=max as usize)) .take(rand::thread_rng().gen_range(min as usize..=max as usize))
.map(char::from) .map(char::from)
.collect::<String>() .collect::<String>()
.into()), .into()),
max if max >= 0 && max <= min => Ok(rand::thread_rng()
.sample_iter(&Alphanumeric)
.take(rand::thread_rng().gen_range(max as usize..=min as usize))
.map(char::from)
.collect::<String>()
.into()),
_ => Err(Error::InvalidArguments { _ => Err(Error::InvalidArguments {
name: String::from("rand::string"), name: String::from("rand::string"),
message: String::from("To generate a string of between X and Y characters in length, the 2 arguments must be positive numbers."), message: format!("To generate a string of between X and Y characters in length, the 2 arguments must be positive numbers and no higher than {}.", LIMIT),
}), }),
}, },
_ => Err(Error::InvalidArguments { _ => Err(Error::InvalidArguments {
name: String::from("rand::string"), name: String::from("rand::string"),
message: String::from("To generate a string of between X and Y characters in length, the 2 arguments must be positive numbers."), message: format!("To generate a string of between X and Y characters in length, the 2 arguments must be positive numbers and no higher than {}.", LIMIT),
}), }),
}, },
1 => match args.remove(0).as_int() { 1 => match args.remove(0).as_int() {
x if x >= 0 => Ok(rand::thread_rng() x if (0..=LIMIT).contains(&x) => Ok(rand::thread_rng()
.sample_iter(&Alphanumeric) .sample_iter(&Alphanumeric)
.take(x as usize) .take(x as usize)
.map(char::from) .map(char::from)
@ -102,7 +113,7 @@ pub fn string(_: &Context, mut args: Vec<Value>) -> Result<Value, Error> {
.into()), .into()),
_ => Err(Error::InvalidArguments { _ => Err(Error::InvalidArguments {
name: String::from("rand::string"), name: String::from("rand::string"),
message: String::from("To generate a string of X characters in length, the argument must be a positive number."), message: format!("To generate a string of X characters in length, the argument must be a positive number and no higher than {}.", LIMIT),
}), }),
}, },
0 => Ok(rand::thread_rng() 0 => Ok(rand::thread_rng()

View file

@ -36,7 +36,15 @@ pub fn repeat(_: &Context, args: Vec<Value>) -> Result<Value, Error> {
let [val_arg, num_arg]: [Value; 2] = args.try_into().unwrap(); let [val_arg, num_arg]: [Value; 2] = args.try_into().unwrap();
let val = val_arg.as_string(); let val = val_arg.as_string();
let num = num_arg.as_int() as usize; let num = num_arg.as_int() as usize;
const LIMIT: usize = 2usize.pow(20);
if val.len().saturating_mul(num) > LIMIT {
Err(Error::InvalidArguments {
name: String::from("string::repeat"),
message: format!("Output must not exceed {} bytes.", LIMIT),
})
} else {
Ok(val.repeat(num).into()) Ok(val.repeat(num).into())
}
} }
pub fn replace(_: &Context, args: Vec<Value>) -> Result<Value, Error> { pub fn replace(_: &Context, args: Vec<Value>) -> Result<Value, Error> {