{"record":{"id":"3c757ae12ea7eb86","repo":"harry0703/MoneyPrinterTurbo","slug":"custom-audio-file-must-be-task-local-or-an-existin","errorCode":null,"errorMessage":"custom audio file must be task-local or an existing server-side file","messagePattern":"custom audio file must be task-local or an existing server-side file","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"app/services/task.py","lineNumber":367,"sourceCode":"            requested_file,\n        )\n    except ValueError as exc:\n        task_dir_error = exc\n\n    server_audio_file = path.realpath(\n        requested_file\n        if path.isabs(requested_file)\n        else path.join(utils.root_dir(), requested_file)\n    )\n    if not path.isabs(requested_file):\n        project_root = path.realpath(utils.root_dir())\n        try:\n            if path.commonpath([project_root, server_audio_file]) != project_root:\n                raise ValueError(\n                    \"relative custom audio paths must stay within the project directory\"\n                )\n        except ValueError as exc:\n            raise ValueError(\n                \"custom audio file must be task-local or an existing server-side file\"\n            ) from exc\n\n    if not path.isfile(server_audio_file):\n        raise ValueError(\n            \"custom audio file does not exist or is not a file\"\n        ) from task_dir_error\n\n    return server_audio_file\n\n\ndef _resolve_reusable_voice_preview(\n    task_id: str,\n    params,\n    video_script: str,\n    voice_preview: dict | None,\n) -> tuple[str, float, object] | None:\n    \"\"\"","sourceCodeStart":349,"sourceCodeEnd":385,"githubUrl":"https://github.com/harry0703/MoneyPrinterTurbo/blob/1f9f19c2021a68d04df228f33e9099a0c947f6f8/app/services/task.py#L349-L385","documentation":"Raised when resolving a custom audio file: the value was not task-local (file_security.resolve_path_within_directory already failed), so the code fell back to treating it as a server-relative path, and realpath+commonpath proved it resolves outside the project root (or commonpath itself raised, for example a path on a different drive on Windows). This is a path-containment guard: relative custom audio must stay inside the project directory.","triggerScenarios":"custom_audio_file containing parent-directory traversal segments (for example targeting /etc/passwd or /home/user/x.mp3); a symlink whose realpath escapes the project root; on Windows, a relative-looking path that commonpath evaluates onto another drive; a task-dir lookup that failed for an unrelated reason (task directory missing) pushing a benign relative path into the server-path branch where the guard then trips.","commonSituations":"Clients sending user-typed file paths verbatim; symlinked uploads pointing to system paths; malformed path strings containing backslashes or drive letters; scanner or pen-test probes attempting traversal.","solutions":["Fix the client to send either a task-local filename (file uploaded into this task's directory) or a relative path that stays under the project root.","Audit the sent path for traversal segments, absolute paths, and symlinks; resolve symlinks before submitting.","If the intended file legitimately lives outside the project, copy it into the task directory and reference the copy.","Note the nested-try quirk: any ValueError raised inside the commonpath block (including the explicit containment raise and cross-drive errors) is re-wrapped with this message, so check the __cause__ chain to tell traversal apart from other commonpath failures."],"exampleFix":"# before\ncustom_audio_file = \"../../shared/bgm.mp3\"\n# after (copy into the task and reference the task-local name)\ncustom_audio_file = \"bgm.mp3\"  # uploaded to this task's directory","handlingStrategy":"validation","validationCode":"from pathlib import Path\nimport app.utils.utils as utils\n\ndef custom_audio_is_safe(custom_audio_file: str) -> bool:\n    root = Path(utils.root_dir()).resolve()\n    p = Path(custom_audio_file)\n    if p.is_absolute():\n        return True  # server-managed path; existence checked separately\n    resolved = (root / p).resolve()\n    return resolved == root or root in resolved.parents","typeGuard":null,"tryCatchPattern":"try:\n    audio = resolve_custom_audio_file(task_id, custom_audio_file)\nexcept ValueError as exc:\n    if \"must be task-local or an existing server-side file\" in str(exc):\n        # containment violation: reject the request, never coerce or normalize the path\n        raise HTTPException(400, str(exc)) from exc\n    raise","preventionTips":["Only accept task-local filenames from end users; server-side paths should come from a curated picker, not free text.","Never normalize or strip parent-directory segments to fix this error; it is a security guard, so fix the source of the path instead.","On Windows deployments, remember cross-drive relative paths also land here via commonpath's ValueError."],"tags":["path-traversal","security","validation","custom-audio"],"backgroundTag":null,"analyzedSha":"1f9f19c2021a68d04df228f33e9099a0c947f6f8","analyzedAt":"2026-08-14T19:41:05.568Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}