Skip to content

Commit 7c94a0b

Browse files
mjcarsongabaker
authored andcommitted
Fix(api): Fixed several vulnerabilities in Thorium
None of these issues allow for RCE or privilege escalation. Result File Path Normalization The API was not validating that uploaded result file paths are not absolute paths and do not contain any '..' components. This was not exploitable due to the fact that: - Some s3 servers (Minio and CEPH were tested) do not allow .. in paths - The agent panics when downloading files with an absolute path - Thorctl nests the absolute path in its relative path and returns an error Regardless this has been resolved, and Thorium will now validate and reject any absolute paths or paths where any component contains only '.'s. LDAP Injection Thorium was not escaping user controlled strings that it sent to LDAP. This would allow attackers to perform LDAP injection if they can add metagroups to groups. In order to perform this attack, an attacker would already have the permissions to modify group permissions at will. Thorium now properly escapes user controlled strings in ldap. Spam Verification Emails For Unverified Users Thorium was not limiting how often verification emails could be resent to unverified users in systems that have email verification configured. This means that if an attacker knew a user's username and that user had not yet verified their email, they could spam them with emails. Only the verification email would sent this does not allow an attacker to send arbitrary emails. Thorium now allows admins to set a rate limit value that currently defaults to only allowing an email to be resent every 10 minutes. Token Not Rotating When Resetting Passwords Thorium was generating a new token but not saving it when updating a users password. This meant that if a user was updating their password due to a password or token being leaked, Thorium did not properly remove all prior access. This is only relevant to LDAP enabled Thorium clusters. Thorium now saves the new token on password updates. Disabled TLS Verification To Elasticsearch Thorium was not allowing users to configure how they want to validate the certificate used by elastic search and was defaulting to not verifying it. This option is now configurable. Divide By Zero When Getting Streams If a user set a split of 0 when getting streams, that request would panic due to a divide by zero error. This has been resolved by requiring a NonZeroU64 instead of a u64. Thanks to OpenAI Security Research for bringing these issues to our attention.
1 parent d24f038 commit 7c94a0b

23 files changed

Lines changed: 444 additions & 338 deletions

File tree

Cargo.lock

Lines changed: 341 additions & 303 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ members = [
1616
]
1717

1818
[workspace.package]
19-
version = "1.1.0"
19+
version = "1.1.1"
2020
authors = ["mcarson <mcarson@sandia.gov>", "gmbaker <gmbaker@sandia.gov>", "jehamza <jehamza@sandia.gov>"]
2121
edition = "2024"
2222

agent/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,14 @@ infer = { version = "0.19.0", default-features = false, features = ["std"] }
3636

3737
# enable cgroups support for linux
3838
[target.'cfg(target_os = "linux")'.dependencies]
39-
thorium = { version = "1.1.0", path="../api", default-features = false, features = ["client", "cgroups", "crossbeam-err", "trace"]}
39+
thorium = { version = "1.1.1", path="../api", default-features = false, features = ["client", "cgroups", "crossbeam-err", "trace"]}
4040
cgroups-rs = "0.3"
4141
controlgroup = "0.3"
4242

4343

4444
# disable cgroups support when on windows
4545
[target.'cfg(any(target_os = "windows", target_os = "macos"))'.dependencies]
46-
thorium = { version = "1.1.0", path="../api", default-features = false, features = ["client", "crossbeam-err", "trace"]}
46+
thorium = { version = "1.1.1", path="../api", default-features = false, features = ["client", "crossbeam-err", "trace"]}
4747

4848

4949
[features]

agent/src/args.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use clap::Parser;
22
use serde_derive::Deserialize;
33
use thorium::models::ImageScaler;
44
use thorium::{Error, Thorium};
5-
use tracing::{event, instrument, Level};
5+
use tracing::{Level, event, instrument};
66

77
use crate::libs::Target;
88

@@ -37,6 +37,9 @@ pub struct Args {
3737
/// The keys to use when authenticating to Thorium
3838
#[clap(short, long, default_value = "keys.yml")]
3939
pub keys: String,
40+
/// How long should this agent sit limbo before exiting without a job to work on
41+
#[clap(short, long, default_value = "30")]
42+
pub limbo: usize,
4043
}
4144

4245
impl Args {
@@ -103,7 +106,7 @@ impl Args {
103106
return Err(Error::new(format!(
104107
"Failed to get hostname with {:#?}",
105108
err
106-
)))
109+
)));
107110
}
108111
}
109112
}

agent/src/libs/worker.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
use futures::{poll, task::Poll};
22
use std::time::Duration;
3-
use thorium::models::{StageLogsAdd, WorkerStatus};
43
use thorium::Error;
54
use thorium::Thorium;
6-
use tracing::{event, instrument, span, Level};
5+
use thorium::models::{StageLogsAdd, WorkerStatus};
6+
use tracing::{Level, event, instrument, span};
77

88
use super::agents::{self, Agent};
99
use super::{CurrentTarget, Lifetime, Target};

api/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ once_cell = { version = "1.21.3", optional = true }
134134
utoipa = { version = "5", features = ["axum_extras", "chrono", "uuid", "time", "url"], optional = true }
135135
utoipa-swagger-ui = { version = "9", features = ["axum"], optional = true }
136136
lettre = { version = "0.11", features = ["tokio1", "tokio1-rustls-tls", "builder", "smtp-transport"], default-features = false, optional = true }
137-
thorium-derive = { path = "../thorium-derive", version = "1.1.0", optional = true}
137+
thorium-derive = { path = "../thorium-derive", version = "1.1.1", optional = true}
138138

139139
# rkyv dependencies
140140
rkyv = { version = "=0.7.43", features = ["arbitrary_enum_discriminant", "uuid", "validation"], optional = true }

api/src/conf.rs

Lines changed: 51 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -278,17 +278,9 @@ pub struct Ldap {
278278
pub credentials: Option<LdapCreds>,
279279
}
280280

281-
/// Helps serde default the default user token expiration to 90
282-
fn default_token_expire() -> u32 {
283-
90
284-
}
285-
286-
/// Helps serde default the local user/group ids to a sane default
287-
fn default_local_user_ids() -> UnixInfo {
288-
UnixInfo {
289-
user: 1_879_048_192,
290-
group: 1_879_048_192,
291-
}
281+
/// Help serde default the email rate limit to 10 minutes
282+
fn default_email_rate_limit() -> u64 {
283+
600
292284
}
293285

294286
/// The email settings to use for verification emails
@@ -304,6 +296,54 @@ pub struct EmailVerification {
304296
pub password: String,
305297
/// The email regexes to restrict users too
306298
pub approved_emails: Vec<String>,
299+
/// The time to require between verification emails in seconds
300+
#[serde(default = "default_email_rate_limit")]
301+
pub rate_limit: u64,
302+
}
303+
304+
impl EmailVerification {
305+
/// Check if a user has recently sent a verification email
306+
///
307+
/// This will return an error if we can't send a verification email yet.
308+
///
309+
/// # Arguments
310+
///
311+
/// * `user` - The user that we might send a verification email too
312+
#[cfg(feature = "api")]
313+
pub fn can_send_verification(
314+
&self,
315+
user: &crate::models::User,
316+
) -> Result<(), crate::utils::ApiError> {
317+
// get the last time an email was sent if it was
318+
if let Some(sent) = user.verification_sent {
319+
// calculate the time at which our rate limit no longer applies
320+
let limit_ts = chrono::Utc::now() - chrono::Duration::seconds(self.rate_limit as i64);
321+
// a previous verification email was sent so check our rate limit
322+
if sent > limit_ts {
323+
// calculate when we can next send a verification email
324+
let wait_time = sent - limit_ts;
325+
// return a 429
326+
return Err(crate::too_many_requests!(format!(
327+
"Cannot send verificaiton for another {} seconds",
328+
wait_time.num_seconds()
329+
)));
330+
}
331+
}
332+
Ok(())
333+
}
334+
}
335+
336+
/// Helps serde default the default user token expiration to 90
337+
fn default_token_expire() -> u32 {
338+
90
339+
}
340+
341+
/// Helps serde default the local user/group ids to a sane default
342+
fn default_local_user_ids() -> UnixInfo {
343+
UnixInfo {
344+
user: 1_879_048_192,
345+
group: 1_879_048_192,
346+
}
307347
}
308348

309349
/// Authentication settings

api/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ async fn initial_settings_consistency_scan(
105105
settings: crate::models::UserSettings::default(),
106106
verified: bool::default(),
107107
verification_token: None,
108+
verification_sent: None,
108109
};
109110
// do a scan for consistency according to current settings
110111
settings.consistency_scan(&fake_admin, &shared).await?;
@@ -130,7 +131,7 @@ fn build_app(
130131
use std::time::Duration;
131132
use tower_http::set_header::SetResponseHeaderLayer;
132133
use tower_http::trace::{DefaultMakeSpan, TraceLayer};
133-
use tracing::{event, Level, Span};
134+
use tracing::{Level, Span, event};
134135

135136
use crate::utils::trace;
136137

api/src/models/backends/db/users.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use bb8_redis::redis::cmd;
2+
use chrono::prelude::*;
23
use std::collections::{HashMap, HashSet};
34
use tracing::instrument;
45

@@ -116,6 +117,7 @@ pub(super) fn cast(
116117
settings: deserialize_ext!(raw, "settings", UserSettings::default()),
117118
verified: helpers::extract_bool_default(&mut raw, "verified", true)?,
118119
verification_token: helpers::extract_opt(&mut raw, "verification_token"),
120+
verification_sent: deserialize_opt!(&mut raw, "verification_sent"),
119121
};
120122
Ok(user)
121123
}
@@ -426,7 +428,8 @@ pub async fn set_verification_token(username: &str, verification_token: &str, sh
426428
// build a redis pipeline
427429
let mut pipe = redis::pipe();
428430
// set our updated verification token
429-
pipe.cmd("hset").arg(&data_key).arg("verification_token").arg(verification_token);
431+
pipe.cmd("hset").arg(&data_key).arg("verification_token").arg(verification_token)
432+
.cmd("hset").arg(&data_key).arg("verification_sent").arg(serialize!(&Utc::now()));
430433
// save user into redis
431434
let _: () = pipe.atomic()
432435
.query_async(conn!(shared))

api/src/models/backends/groups.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1004,7 +1004,7 @@ impl
10041004
macro_rules! build_filter {
10051005
($arr:expr, $key:expr, $target:expr) => {
10061006
for item in $arr.iter() {
1007-
$target.insert(format!("({}={})", $key, item));
1007+
$target.insert(format!("({}={})", $key, ldap3::ldap_escape(item)));
10081008
}
10091009
};
10101010
}

0 commit comments

Comments
 (0)