[하루한줄] CVE-2024-25106: OpenObserve의 Improper Access Control 취약점

URL

https://github.com/openobserve/openobserve/security/advisories/GHSA-3m5f-9m66-xgp7

Target

  • OpenObserve < 0.8.0

Explain

OpenObserve는 클라우드 기반 옵저버빌리티 플랫폼으로 데이터의 로깅, 분석, 추적에 특화되어 있습니다. OpenObserve는 보다 간단한 설정을 지원하고 사용자는 API를 통한 데이터의 수집, 검색 등이 가능합니다.

취약점은 이 API 중에서 유저를 삭제하는데 사용하는 /api/{org_id}/users/{email_id} 엔드포인트에 있습니다. /src/service/users.rs에 정의된 remove_user_from_org는 아래와 같이 DELETE 요청된 유저를 삭제하는 기능을 하는데 이 과정에서 request를 만든 사용자가 유저를 삭제할 권한을 가지고 있는지 확인하지 않습니다.

pub async fn remove_user_from_org(org_id: &str, email_id: &str) -> Result<HttpResponse, Error> {
    let ret_user = db::user::get_db_user(email_id).await;
    match ret_user {
        Ok(mut user) => {
            if !user.organizations.is_empty() {
                let mut orgs = user.clone().organizations;
                if orgs.len() == 1 {
                    let _ = db::user::delete(email_id).await;
                } else {
                    orgs.retain(|x| !x.name.eq(&org_id.to_string()));
                    user.organizations = orgs;
                    let resp = db::user::set(user).await;
                    // special case as we cache flattened user struct
                    if resp.is_ok() {
                        USERS.remove(&format!("{org_id}/{email_id}"));
                    }
                }
                Ok(HttpResponse::Ok().json(MetaHttpResponse::message(
                    http::StatusCode::OK.into(),
                    "User removed from organization".to_string(),
                )))
            } else {
                Ok(HttpResponse::NotFound().json(MetaHttpResponse::error(
                    StatusCode::NOT_FOUND.into(),
                    "User for the organization not found".to_string(),
                )))
            }
        }
        Err(_) => Ok(HttpResponse::NotFound().json(MetaHttpResponse::error(
            StatusCode::NOT_FOUND.into(),
            "User for the organization not found".to_string(),
        ))),
    }
}

따라서 권한이 없는 유저도 같은 organization에 속한 다른 유저를 삭제할 수 있고 Admin이나 Root처럼 더 높은 권한 수준을 가지는 유저 또한 삭제할 수 있습니다.

이 취약점의 패치는 취약한 함수인 remove_user_from_org 에서 아래와 같이 요청한 사용자의 권한을 확인하는 부분을 추가하는 것으로 이루어졌습니다.

pub async fn remove_user_from_org(
    org_id: &str,
    email_id: &str,
    initiator_id: &str,// 요청을 발생시킨 사용자 정보를 확인하기 위한 새로운 파라미터
) -> Result<HttpResponse, Error> {
    let initiating_user = if is_root_user(initiator_id) {
        ROOT_USER.get("root").unwrap().clone()
    } else {
        db::user::get(Some(org_id), initiator_id)
            .await
            .unwrap()
            .unwrap()
    };
    if initiating_user.role.eq(&UserRole::Root) || initiating_user.role.eq(&UserRole::Admin) {// 요청자가 Root 또는 Admin인지 검사
        let ret_user = db::user::get_db_user(email_id).await;
        match ret_user {
            Ok(mut user) => {
                if !user.organizations.is_empty() {
                    let mut orgs = user.clone().organizations;
                    if orgs.len() == 1 {
                        let _ = db::user::delete(email_id).await;
                    } else {
                        orgs.retain(|x| !x.name.eq(&org_id.to_string()));
                        user.organizations = orgs;
                        let resp = db::user::set(user).await;
                        // special case as we cache flattened user struct
                        if resp.is_ok() {
                            USERS.remove(&format!("{org_id}/{email_id}"));
                        }
                    }
                    Ok(HttpResponse::Ok().json(MetaHttpResponse::message(
                        http::StatusCode::OK.into(),
                        "User removed from organization".to_string(),
                    )))
                } else {
                    Ok(HttpResponse::NotFound().json(MetaHttpResponse::error(
                        StatusCode::NOT_FOUND.into(),
                        "User for the organization not found".to_string(),
                    )))
                }
            }
            Err(_) => Ok(HttpResponse::NotFound().json(MetaHttpResponse::error(
                StatusCode::NOT_FOUND.into(),
                "User for the organization not found".to_string(),
            ))),
        }
    } else {
        Ok(HttpResponse::Unauthorized().json(MetaHttpResponse::error(
            StatusCode::UNAUTHORIZED.into(),
            "Not Allowed".to_string(),
        )))
    }
}