{"record":{"id":"61288b2cbf9142c3","repo":"mudler/LocalAI","slug":"output-path-is-outside-the-allowed-directory","errorCode":null,"errorMessage":"Output path is outside the allowed directory","messagePattern":"Output path is outside the allowed directory","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"backend/python/trl/backend.py","lineNumber":697,"sourceCode":"                )\n\n                checkpoints.append(backend_pb2.CheckpointInfo(\n                    path=ckpt_path,\n                    step=step,\n                    epoch=float(epoch),\n                    loss=float(loss),\n                    created_at=created_at,\n                ))\n\n        return backend_pb2.ListCheckpointsResponse(checkpoints=checkpoints)\n\n    def ExportModel(self, request, context):\n        export_format = request.export_format or \"lora\"\n        output_path = request.output_path\n        _allowed_output_dir = os.path.realpath(os.path.abspath(os.environ.get(\"LOCALAI_OUTPUT_DIR\", os.getcwd())))\n        _real_output_path = os.path.realpath(os.path.abspath(output_path))\n        if not (_real_output_path == _allowed_output_dir or _real_output_path.startswith(_allowed_output_dir + os.sep)):\n            raise ValueError(\"Output path is outside the allowed directory\")\n        output_path = _real_output_path\n        checkpoint_path = request.checkpoint_path\n\n        # Extract HF token for gated model access\n        extra = dict(request.extra_options) if request.extra_options else {}\n        hf_token = extra.get(\"hf_token\") or os.environ.get(\"HF_TOKEN\")\n\n        if not checkpoint_path or not os.path.isdir(checkpoint_path):\n            return backend_pb2.Result(success=False, message=f\"Checkpoint not found: {checkpoint_path}\")\n\n        os.makedirs(output_path, exist_ok=True)\n\n        try:\n            if export_format == \"lora\":\n                # Just copy the adapter files\n                import shutil\n                for f in os.listdir(checkpoint_path):\n                    src = os.path.join(checkpoint_path, f)","sourceCodeStart":679,"sourceCodeEnd":715,"githubUrl":"https://github.com/mudler/LocalAI/blob/44413a9d06bf5bc52ce088ba8ca74e5a2e8bee26/backend/python/trl/backend.py#L679-L715","documentation":"ExportModel confines exported model outputs to LOCALAI_OUTPUT_DIR (default: backend cwd) using the same realpath containment check as datasets. An output_path that resolves outside that directory — absolute path elsewhere, '..' traversal, or a symlink escaping it — is rejected before any file is written.","triggerScenarios":"Calling ExportModel with output_path='/tmp/export' when LOCALAI_OUTPUT_DIR is unset; output_path='../shared/out'; the client using host paths that differ from container paths.","commonSituations":"Operator exposes an exports volume but does not set LOCALAI_OUTPUT_DIR; CI pipelines writing to arbitrary temp dirs; symlinked output dirs.","solutions":["Set LOCALAI_OUTPUT_DIR on the backend to the intended export root and restart, then use paths inside it.","Send output_path relative to the allowed dir or as an absolute path under it.","Replace symlinks with real directories/mounts under the allowed root."],"exampleFix":"# before\nLOCALAI_OUTPUT_DIR unset; request.output_path = \"/tmp/my-export\"\n# after\n# backend env: LOCALAI_OUTPUT_DIR=/exports\nrequest.output_path = \"/exports/my-export\"","handlingStrategy":"validation","validationCode":"import os\n\ndef output_path_allowed(output_path: str) -> bool:\n    allowed = os.path.realpath(os.path.abspath(os.environ.get(\"LOCALAI_OUTPUT_DIR\", os.getcwd())))\n    real = os.path.realpath(os.path.abspath(output_path))\n    return real == allowed or real.startswith(allowed + os.sep)","typeGuard":null,"tryCatchPattern":"try:\n    ExportModel(request, context)\nexcept ValueError as e:\n    if \"outside the allowed directory\" in str(e):\n        return Result(success=False, message=str(e) + f\"; set LOCALAI_OUTPUT_DIR\")\n    raise","preventionTips":["Set LOCALAI_OUTPUT_DIR to the exported-models volume and reuse it for every request.","Construct output paths with os.path.join(allowed_root, name).","Test export in CI with the same env var as production."],"tags":["trl","export","path-traversal","security","configuration","localai"],"backgroundTag":null,"analyzedSha":"44413a9d06bf5bc52ce088ba8ca74e5a2e8bee26","analyzedAt":"2026-08-15T10:13:50.291Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}