allow only whitelisted folders to be used in ffpapi

This commit is contained in:
jb-alvarado 2024-05-27 11:59:49 +02:00
parent 8bb2e96edb
commit 3729d37a78
7 changed files with 53 additions and 18 deletions

9
Cargo.lock generated
View File

@ -1292,7 +1292,7 @@ checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a"
[[package]]
name = "ffplayout"
version = "0.22.3"
version = "0.23.0"
dependencies = [
"chrono",
"clap",
@ -1314,7 +1314,7 @@ dependencies = [
[[package]]
name = "ffplayout-api"
version = "0.22.3"
version = "0.23.0"
dependencies = [
"actix-files",
"actix-multipart",
@ -1330,6 +1330,7 @@ dependencies = [
"faccess",
"ffplayout-lib",
"futures-util",
"home",
"jsonwebtoken",
"lazy_static",
"lexical-sort",
@ -1357,7 +1358,7 @@ dependencies = [
[[package]]
name = "ffplayout-lib"
version = "0.22.3"
version = "0.23.0"
dependencies = [
"chrono",
"crossbeam-channel",
@ -3587,7 +3588,7 @@ dependencies = [
[[package]]
name = "tests"
version = "0.22.3"
version = "0.23.0"
dependencies = [
"chrono",
"crossbeam-channel",

View File

@ -4,7 +4,7 @@ default-members = ["ffplayout-api", "ffplayout-engine", "tests"]
resolver = "2"
[workspace.package]
version = "0.22.3"
version = "0.23.0"
license = "GPL-3.0"
repository = "https://github.com/ffplayout/ffplayout"
authors = ["Jonathan Baecker <jonbae77@gmail.com>"]

View File

@ -27,6 +27,7 @@ clap = { version = "4.3", features = ["derive"] }
derive_more = "0.99"
faccess = "0.2"
futures-util = { version = "0.3", default-features = false, features = ["std"] }
home = "0.5"
jsonwebtoken = "9"
lazy_static = "1.4"
lexical-sort = "0.3"

View File

@ -855,7 +855,7 @@ pub async fn gen_playlist(
let mut path_list = vec![];
for path in paths {
let (p, _, _) = norm_abs_path(&config.storage.path, path);
let (p, _, _) = norm_abs_path(&config.storage.path, path)?;
path_list.push(p);
}
@ -1024,7 +1024,7 @@ async fn get_file(
let (config, _) = playout_config(&pool.into_inner(), &id).await?;
let storage_path = config.storage.path;
let file_path = req.match_info().query("filename");
let (path, _, _) = norm_abs_path(&storage_path, file_path);
let (path, _, _) = norm_abs_path(&storage_path, file_path)?;
let file = actix_files::NamedFile::open(path)?;
Ok(file

View File

@ -12,6 +12,9 @@ pub enum ServiceError {
#[display(fmt = "Conflict: {_0}")]
Conflict(String),
#[display(fmt = "Forbidden: {_0}")]
Forbidden(String),
#[display(fmt = "Unauthorized: {_0}")]
Unauthorized(String),
@ -31,6 +34,7 @@ impl ResponseError for ServiceError {
}
ServiceError::BadRequest(ref message) => HttpResponse::BadRequest().json(message),
ServiceError::Conflict(ref message) => HttpResponse::Conflict().json(message),
ServiceError::Forbidden(ref message) => HttpResponse::Forbidden().json(message),
ServiceError::Unauthorized(ref message) => HttpResponse::Unauthorized().json(message),
ServiceError::NoContent(ref message) => HttpResponse::NoContent().json(message),
ServiceError::ServiceUnavailable(ref message) => {

View File

@ -6,6 +6,7 @@ use std::{
use actix_multipart::Multipart;
use actix_web::{web, HttpResponse};
use futures_util::TryStreamExt as _;
use lazy_static::lazy_static;
use lexical_sort::{natural_lexical_cmp, PathSort};
use rand::{distributions::Alphanumeric, Rng};
use relative_path::RelativePath;
@ -54,11 +55,30 @@ pub struct VideoFile {
duration: f64,
}
lazy_static! {
pub static ref HOME_DIR: String = home::home_dir()
.unwrap_or("/home/h1wl3n2og".into()) // any random not existing folder
.as_os_str()
.to_string_lossy()
.to_string();
}
const FOLDER_WHITELIST: &[&str; 6] = &[
"/media",
"/mnt",
"/playlists",
"/tv-media",
"/usr/share/ffplayout",
"/var/lib/ffplayout",
];
/// Normalize absolut path
///
/// This function takes care, that it is not possible to break out from root_path.
/// It also gives always a relative path back.
pub fn norm_abs_path(root_path: &Path, input_path: &str) -> (PathBuf, String, String) {
pub fn norm_abs_path(
root_path: &Path,
input_path: &str,
) -> Result<(PathBuf, String, String), ServiceError> {
let path_relative = RelativePath::new(&root_path.to_string_lossy())
.normalize()
.to_string()
@ -91,7 +111,15 @@ pub fn norm_abs_path(root_path: &Path, input_path: &str) -> (PathBuf, String, St
let path = &root_path.join(&source_relative);
(path.to_path_buf(), path_suffix, source_relative)
if !FOLDER_WHITELIST.iter().any(|f| path.starts_with(f))
&& !path.starts_with(&HOME_DIR.to_string())
{
return Err(ServiceError::Forbidden(
"Access forbidden: Folder cannot be opened.".to_string(),
));
}
Ok((path.to_path_buf(), path_suffix, source_relative))
}
/// File Browser
@ -114,7 +142,7 @@ pub async fn browser(
let mut extensions = config.storage.extensions;
extensions.append(&mut channel_extensions);
let (path, parent, path_component) = norm_abs_path(&config.storage.path, &path_obj.source);
let (path, parent, path_component) = norm_abs_path(&config.storage.path, &path_obj.source)?;
let parent_path = if !path_component.is_empty() {
path.parent().unwrap()
@ -212,7 +240,7 @@ pub async fn create_directory(
path_obj: &PathObject,
) -> Result<HttpResponse, ServiceError> {
let (config, _) = playout_config(conn, &id).await?;
let (path, _, _) = norm_abs_path(&config.storage.path, &path_obj.source);
let (path, _, _) = norm_abs_path(&config.storage.path, &path_obj.source)?;
if let Err(e) = fs::create_dir_all(&path).await {
return Err(ServiceError::BadRequest(e.to_string()));
@ -283,8 +311,8 @@ pub async fn rename_file(
move_object: &MoveObject,
) -> Result<MoveObject, ServiceError> {
let (config, _) = playout_config(conn, &id).await?;
let (source_path, _, _) = norm_abs_path(&config.storage.path, &move_object.source);
let (mut target_path, _, _) = norm_abs_path(&config.storage.path, &move_object.target);
let (source_path, _, _) = norm_abs_path(&config.storage.path, &move_object.source)?;
let (mut target_path, _, _) = norm_abs_path(&config.storage.path, &move_object.target)?;
if !source_path.exists() {
return Err(ServiceError::BadRequest("Source file not exist!".into()));
@ -318,7 +346,7 @@ pub async fn remove_file_or_folder(
source_path: &str,
) -> Result<(), ServiceError> {
let (config, _) = playout_config(conn, &id).await?;
let (source, _, _) = norm_abs_path(&config.storage.path, source_path);
let (source, _, _) = norm_abs_path(&config.storage.path, source_path)?;
if !source.exists() {
return Err(ServiceError::BadRequest("Source does not exists!".into()));
@ -351,7 +379,7 @@ pub async fn remove_file_or_folder(
async fn valid_path(conn: &Pool<Sqlite>, id: i32, path: &str) -> Result<PathBuf, ServiceError> {
let (config, _) = playout_config(conn, &id).await?;
let (test_path, _, _) = norm_abs_path(&config.storage.path, path);
let (test_path, _, _) = norm_abs_path(&config.storage.path, path)?;
if !test_path.is_dir() {
return Err(ServiceError::BadRequest("Target folder not exists!".into()));

View File

@ -14,7 +14,8 @@ pub async fn read_playlist(
date: String,
) -> Result<JsonPlaylist, ServiceError> {
let (config, _) = playout_config(conn, &id).await?;
let mut playlist_path = PathBuf::from(&config.playlist.path);
let (path, _, _) = norm_abs_path(&config.playlist.path, "")?;
let mut playlist_path = path;
let d: Vec<&str> = date.split('-').collect();
playlist_path = playlist_path
.join(d[0])
@ -96,7 +97,7 @@ pub async fn generate_playlist(
for path in &source.paths {
let (safe_path, _, _) =
norm_abs_path(&config.storage.path, &path.to_string_lossy());
norm_abs_path(&config.storage.path, &path.to_string_lossy())?;
paths.push(safe_path);
}