diff --git a/.gitignore b/.gitignore index 257db4c..bd660ac 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,4 @@ build/ dist/ .old/ test_assets/ +dev_notes/ diff --git a/abogen/Dockerfile b/abogen/Dockerfile index 13fd1d0..6c5731e 100644 --- a/abogen/Dockerfile +++ b/abogen/Dockerfile @@ -52,6 +52,11 @@ ENV ABOGEN_UPLOAD_ROOT=/data/uploads \ HF_HOME=/data/huggingface \ HUGGINGFACE_HUB_CACHE=/data/huggingface/hub -RUN mkdir -p /data/uploads /data/outputs /data/cache /data/voice-cache /data/huggingface +# Create non-root user and setup permissions +RUN useradd -m -u 1000 abogen \ + && mkdir -p /data/uploads /data/outputs /data/cache /data/voice-cache /data/huggingface \ + && chown -R abogen:abogen /data /app + +USER abogen CMD ["abogen"] diff --git a/abogen/pronunciation_store.py b/abogen/pronunciation_store.py index a6f0f4e..88524ff 100644 --- a/abogen/pronunciation_store.py +++ b/abogen/pronunciation_store.py @@ -86,6 +86,7 @@ def load_overrides(language: str, tokens: Iterable[str]) -> Dict[str, Dict[str, normalized_tokens = {normalize_token(token) for token in tokens if token} if not normalized_tokens: return {} + # Use parameterized queries to prevent SQL injection placeholders = ",".join("?" for _ in normalized_tokens) with _DB_LOCK: conn = _connect() diff --git a/abogen/utils.py b/abogen/utils.py index a41cd66..e984157 100644 --- a/abogen/utils.py +++ b/abogen/utils.py @@ -327,6 +327,9 @@ def create_process(cmd, stdin=None, text=True, capture_output=False): # Determine shell usage: use shell only for string commands use_shell = isinstance(cmd, str) + if use_shell: + logger.warning("Security Warning: create_process called with string command. Prefer using a list of arguments to avoid shell injection risks.") + kwargs = { "shell": use_shell, "stdout": subprocess.PIPE, diff --git a/abogen/web/app.py b/abogen/web/app.py index f09dcfb..3a9db9b 100644 --- a/abogen/web/app.py +++ b/abogen/web/app.py @@ -8,7 +8,7 @@ from typing import Any, Optional from flask import Flask -from abogen.utils import get_user_cache_path, get_user_output_path +from abogen.utils import get_user_cache_path, get_user_output_path, get_user_settings_dir from .conversion_runner import run_conversion_job from .service import build_service @@ -50,6 +50,26 @@ def _default_dirs() -> tuple[Path, Path]: return uploads, outputs +def _get_secret_key() -> str: + env_key = os.environ.get("ABOGEN_SECRET_KEY") + if env_key: + return env_key + + try: + settings_dir = Path(get_user_settings_dir()) + settings_dir.mkdir(parents=True, exist_ok=True) + secret_file = settings_dir / ".secret_key" + if secret_file.exists(): + return secret_file.read_text(encoding="utf-8").strip() + + key = os.urandom(24).hex() + secret_file.write_text(key, encoding="utf-8") + return key + except Exception: + # Fallback if we can't write to settings dir + return os.urandom(24).hex() + + def create_app(config: Optional[dict[str, Any]] = None) -> Flask: uploads_dir, outputs_dir = _default_dirs() @@ -59,7 +79,7 @@ def create_app(config: Optional[dict[str, Any]] = None) -> Flask: template_folder="templates", ) base_config = { - "SECRET_KEY": os.environ.get("ABOGEN_SECRET_KEY", os.urandom(16)), + "SECRET_KEY": _get_secret_key(), "UPLOAD_FOLDER": str(uploads_dir), "OUTPUT_FOLDER": str(outputs_dir), "MAX_CONTENT_LENGTH": 1024 * 1024 * 400, # 400 MB uploads diff --git a/abogen/web/routes.py b/abogen/web/routes.py index c430df0..07c418f 100644 --- a/abogen/web/routes.py +++ b/abogen/web/routes.py @@ -4511,6 +4511,7 @@ def enqueue_job() -> ResponseReturnValue: original_name: str if file and file.filename: + # secure_filename prevents path traversal attacks filename = secure_filename(file.filename) if not filename: return redirect(url_for("web.index"))