Merge pull request #666 from jb-alvarado/master

allow only whitelisted folders to be used in ffpapi
This commit is contained in:
jb-alvarado 2024-05-27 10:22:01 +00:00 committed by GitHub
commit dc88d541f4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 58 additions and 83 deletions

10
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,13 +1358,14 @@ dependencies = [
[[package]]
name = "ffplayout-lib"
version = "0.22.3"
version = "0.23.0"
dependencies = [
"chrono",
"crossbeam-channel",
"derive_more",
"ffprobe",
"file-rotate",
"home",
"lazy_static",
"lettre",
"lexical-sort",
@ -3587,7 +3589,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);
}

@ -1 +1 @@
Subproject commit 390544ae3ad4494667a2c6c2f3385412e1e7eac0
Subproject commit 8d63cc4f85f3cbd530d509d74494b6fefbb9bf2c

View File

@ -14,6 +14,7 @@ crossbeam-channel = "0.5"
derive_more = "0.99"
ffprobe = "0.4"
file-rotate = "0.7"
home = "0.5"
lazy_static = "1.4"
lettre = { version = "0.11", features = ["builder", "rustls-tls", "smtp-transport"], default-features = false }
lexical-sort = "0.3"

View File

@ -15,7 +15,7 @@ use shlex::split;
use crate::AdvancedConfig;
use super::vec_strings;
use crate::utils::{free_tcp_socket, home_dir, time_to_sec, OutputMode::*};
use crate::utils::{free_tcp_socket, time_to_sec, OutputMode::*};
pub const DUMMY_LEN: f64 = 60.0;
pub const IMAGE_FORMAT: [&str; 21] = [
@ -406,7 +406,7 @@ impl PlayoutConfig {
config.general.config_path = config_path.to_string_lossy().to_string();
config.general.stat_file = home_dir()
config.general.stat_file = home::home_dir()
.unwrap_or_else(env::temp_dir)
.join(if config.general.stat_file.is_empty() {
".ffp_status"

View File

@ -10,9 +10,6 @@ use std::{
sync::{Arc, Mutex},
};
#[cfg(not(windows))]
use std::env;
use chrono::{prelude::*, TimeDelta};
use ffprobe::{ffprobe, Stream as FFStream};
use rand::prelude::*;
@ -33,9 +30,6 @@ pub mod json_serializer;
mod json_validate;
mod logging;
#[cfg(windows)]
mod windows;
pub use config::{
self as playout_config,
OutputMode::{self, *},
@ -988,19 +982,6 @@ pub fn custom_format<T: fmt::Display>(template: &str, args: &[T]) -> String {
filled_template
}
pub fn home_dir() -> Option<PathBuf> {
home_dir_inner()
}
#[cfg(windows)]
use windows::home_dir_inner;
#[cfg(any(unix, target_os = "redox"))]
fn home_dir_inner() -> Option<PathBuf> {
#[allow(deprecated)]
env::home_dir()
}
/// Get system time, in non test/debug case.
#[cfg(not(any(test, debug_assertions)))]
pub fn time_now() -> DateTime<Local> {

View File

@ -1,43 +0,0 @@
use std::{env, ffi::OsString, os::windows::ffi::OsStringExt, path::PathBuf, ptr};
use winapi::shared::minwindef::MAX_PATH;
use winapi::shared::winerror::S_OK;
use winapi::um::shlobj::{SHGetFolderPathW, CSIDL_PROFILE};
pub fn home_dir_inner() -> Option<PathBuf> {
env::var_os("USERPROFILE")
.filter(|s| !s.is_empty())
.map(PathBuf::from)
.or_else(home_dir_crt)
}
#[cfg(not(target_vendor = "uwp"))]
fn home_dir_crt() -> Option<PathBuf> {
unsafe {
let mut path: Vec<u16> = Vec::with_capacity(MAX_PATH);
match SHGetFolderPathW(
ptr::null_mut(),
CSIDL_PROFILE,
ptr::null_mut(),
0,
path.as_mut_ptr(),
) {
S_OK => {
let len = wcslen(path.as_ptr());
path.set_len(len);
let s = OsString::from_wide(&path);
Some(PathBuf::from(s))
}
_ => None,
}
}
}
#[cfg(target_vendor = "uwp")]
fn home_dir_crt() -> Option<PathBuf> {
None
}
extern "C" {
fn wcslen(buf: *const u16) -> usize;
}