diff options
author | metamuffin <metamuffin@disroot.org> | 2023-12-11 01:19:51 +0100 |
---|---|---|
committer | metamuffin <metamuffin@disroot.org> | 2023-12-11 01:19:51 +0100 |
commit | 36d7fb2790774c53415c96f8c6955be42bad952f (patch) | |
tree | 4481dac53a6d0896e90ff72b9b68665e59e159db /server/src/routes | |
parent | 767d6c4c7b8518198b0343781128027051b94ae5 (diff) | |
download | jellything-36d7fb2790774c53415c96f8c6955be42bad952f.tar jellything-36d7fb2790774c53415c96f8c6955be42bad952f.tar.bz2 jellything-36d7fb2790774c53415c96f8c6955be42bad952f.tar.zst |
(partially) fix security problem with federated session
Diffstat (limited to 'server/src/routes')
-rw-r--r-- | server/src/routes/api/mod.rs | 28 | ||||
-rw-r--r-- | server/src/routes/stream.rs | 17 | ||||
-rw-r--r-- | server/src/routes/ui/account/mod.rs | 28 | ||||
-rw-r--r-- | server/src/routes/ui/account/session/token.rs | 18 | ||||
-rw-r--r-- | server/src/routes/ui/account/settings.rs | 8 |
5 files changed, 69 insertions, 30 deletions
diff --git a/server/src/routes/api/mod.rs b/server/src/routes/api/mod.rs index 615c836..87ed0e9 100644 --- a/server/src/routes/api/mod.rs +++ b/server/src/routes/api/mod.rs @@ -4,12 +4,12 @@ Copyright (C) 2023 metamuffin <metamuffin.org> */ use super::ui::{ - account::{login_logic, session::AdminSession, LoginForm}, + account::{login_logic, session::AdminSession}, error::MyResult, }; use crate::database::Database; use anyhow::{anyhow, Context}; -use jellycommon::Node; +use jellycommon::{user::UserPermission, Node}; use rocket::{ get, http::MediaType, @@ -20,8 +20,9 @@ use rocket::{ serde::json::Json, Request, State, }; +use serde::Deserialize; use serde_json::{json, Value}; -use std::ops::Deref; +use std::{collections::HashSet, ops::Deref}; #[get("/api")] pub fn r_api_root() -> Redirect { @@ -33,9 +34,26 @@ pub fn r_api_version() -> &'static str { "2" } +#[derive(Deserialize)] +pub struct CreateSessionParams { + username: String, + password: String, + expire: Option<i64>, + drop_permissions: Option<HashSet<UserPermission>>, +} + #[post("/api/create_session", data = "<data>")] -pub fn r_api_account_login(database: &State<Database>, data: Json<LoginForm>) -> MyResult<Value> { - let token = login_logic(database, &data.username, &data.password)?; +pub fn r_api_account_login( + database: &State<Database>, + data: Json<CreateSessionParams>, +) -> MyResult<Value> { + let token = login_logic( + database, + &data.username, + &data.password, + data.expire, + data.drop_permissions.clone(), + )?; Ok(json!(token)) } diff --git a/server/src/routes/stream.rs b/server/src/routes/stream.rs index 5944ace..279a621 100644 --- a/server/src/routes/stream.rs +++ b/server/src/routes/stream.rs @@ -10,6 +10,7 @@ use jellybase::{ permission::{NodePermissionExt, PermissionSetExt}, CONF, }; +use jellyclient::LoginDetails; use jellycommon::{stream::StreamSpec, user::UserPermission, MediaSource}; use log::{info, warn}; use rocket::{ @@ -19,7 +20,7 @@ use rocket::{ response::{self, Redirect, Responder}, Either, Request, Response, State, }; -use std::{ops::Range, time::Duration}; +use std::{collections::HashSet, ops::Range}; use tokio::io::{duplex, DuplexStream}; #[head("/n/<_id>/stream?<spec>")] @@ -71,11 +72,15 @@ pub async fn r_stream( info!("creating session on {host}"); let instance = federation.get_instance(&host)?.to_owned(); let session = instance - .login( - username.to_owned(), - password.to_owned(), - Duration::from_secs(60), - ) + .login(LoginDetails { + username: username.to_owned(), + password: password.to_owned(), + expire: Some(60), + drop_permissions: Some(HashSet::from_iter([ + UserPermission::ManageSelf, + UserPermission::Admin, // in case somebody federated the admin :))) + ])), + }) .await?; let uri = session.stream(&remote_id, &spec); diff --git a/server/src/routes/ui/account/mod.rs b/server/src/routes/ui/account/mod.rs index 88f6f45..1c5b19a 100644 --- a/server/src/routes/ui/account/mod.rs +++ b/server/src/routes/ui/account/mod.rs @@ -6,6 +6,8 @@ pub mod session; pub mod settings; +use std::collections::HashSet; + use super::{error::MyError, layout::LayoutPage}; use crate::{ database::Database, @@ -16,7 +18,7 @@ use anyhow::anyhow; use argon2::{password_hash::Salt, Argon2, PasswordHasher}; use chrono::Duration; use jellybase::CONF; -use jellycommon::user::{PermissionSet, Theme, User}; +use jellycommon::user::{PermissionSet, Theme, User, UserPermission}; use rocket::{ form::{Contextual, Form}, get, @@ -162,7 +164,7 @@ pub fn r_account_login_post( jar.add( Cookie::build( "session", - login_logic(database, &form.username, &form.password)?, + login_logic(database, &form.username, &form.password, None, None)?, ) .permanent() .finish(), @@ -177,11 +179,17 @@ pub fn r_account_logout_post(jar: &CookieJar) -> MyResult<Redirect> { Ok(Redirect::found(rocket::uri!(r_home()))) } -pub fn login_logic(database: &Database, username: &str, password: &str) -> MyResult<String> { +pub fn login_logic( + database: &Database, + username: &str, + password: &str, + expire: Option<i64>, + drop_permissions: Option<HashSet<UserPermission>>, +) -> MyResult<String> { // hashing the password regardless if the accounts exists to prevent timing attacks let password = hash_password(username, password); - let user = database + let mut user = database .user .get(&username.to_string())? .ok_or(anyhow!("invalid password"))?; @@ -190,9 +198,17 @@ pub fn login_logic(database: &Database, username: &str, password: &str) -> MyRes Err(anyhow!("invalid password"))? } + if let Some(ep) = drop_permissions { + // remove all grant perms that are in `ep` + user.permissions + .0 + .retain(|p, val| if *val { !ep.contains(p) } else { true }) + } + Ok(session::token::create( - &user, - Duration::days(CONF.login_expire), + user.name, + user.permissions, + Duration::days(CONF.login_expire.min(expire.unwrap_or(i64::MAX))), )) } diff --git a/server/src/routes/ui/account/session/token.rs b/server/src/routes/ui/account/session/token.rs index b6c22f7..969207d 100644 --- a/server/src/routes/ui/account/session/token.rs +++ b/server/src/routes/ui/account/session/token.rs @@ -12,7 +12,7 @@ use anyhow::anyhow; use base64::Engine; use chrono::{Duration, Utc}; use jellybase::CONF; -use jellycommon::user::User; +use jellycommon::user::PermissionSet; use log::warn; use std::sync::LazyLock; @@ -29,11 +29,11 @@ static SESSION_KEY: LazyLock<[u8; 32]> = LazyLock::new(|| { } }); -pub fn create(user: &User, expire: Duration) -> String { +pub fn create(username: String, permissions: PermissionSet, expire: Duration) -> String { let session_data = SessionData { expire: Utc::now() + expire, - username: user.name.to_owned(), - permissions: user.permissions.clone(), + username: username.to_owned(), + permissions, }; let mut plaintext = bincode::serde::encode_to_vec(&session_data, bincode::config::standard()).unwrap(); @@ -73,14 +73,8 @@ pub fn validate(token: &str) -> anyhow::Result<String> { #[test] fn test() { let tok = create( - &User { - name: "blub".to_string(), - display_name: "blub".to_owned(), - password: vec![], - admin: false, - permissions: jellycommon::user::PermissionSet::default(), - theme: jellycommon::user::Theme::Dark, - }, + "blub".to_string(), + jellycommon::user::PermissionSet::default(), Duration::days(1), ); validate(&tok).unwrap(); diff --git a/server/src/routes/ui/account/settings.rs b/server/src/routes/ui/account/settings.rs index 2192d43..90dcf37 100644 --- a/server/src/routes/ui/account/settings.rs +++ b/server/src/routes/ui/account/settings.rs @@ -13,7 +13,8 @@ use crate::{ }, uri, }; -use jellycommon::user::Theme; +use jellybase::permission::PermissionSetExt; +use jellycommon::user::{Theme, UserPermission}; use rocket::{ form::{self, validate::len, Contextual, Form}, get, @@ -95,6 +96,11 @@ pub fn r_account_settings_post( database: &State<Database>, form: Form<Contextual<SettingsForm>>, ) -> MyResult<DynLayoutPage<'static>> { + session + .user + .permissions + .assert(&UserPermission::ManageSelf)?; + let form = match &form.value { Some(v) => v, None => return Ok(settings_page(session, Some(Err(format_form_error(form))))), |