{"record":{"id":"31c22b75e8a451fd","repo":"unslothai/unsloth","slug":"invalid-block-id-outside-upload-root","errorCode":null,"errorMessage":"Invalid block_id: outside upload root","messagePattern":"Invalid block_id: outside upload root","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"error","filePath":"studio/backend/routes/data_recipe/seed.py","lineNumber":601,"sourceCode":"\n    return {\"status\": \"ok\"}\n\n\n@router.delete(\"/seed/unstructured-block/{block_id}\")\nasync def remove_unstructured_block(block_id: str):\n    \"\"\"Delete a block's upload directory; files on disk still count toward its quota.\n\n    Only uid-namespaced directories may be bulk-deleted: they have exactly one\n    owning block. Legacy node-id directories (n1, ...) can be shared by other\n    recipes, so they are managed file-by-file instead.\n    \"\"\"\n    _validate_safe_id(block_id, \"block_id\")\n    if not _UPLOAD_UID_RE.match(block_id):\n        raise HTTPException(400, \"Invalid block_id: only uid-namespaced blocks can be deleted\")\n\n    block_dir = (UNSTRUCTURED_UPLOAD_ROOT / block_id).resolve()\n    if not block_dir.is_relative_to(UNSTRUCTURED_UPLOAD_ROOT.resolve()):\n        raise HTTPException(400, \"Invalid block_id: outside upload root\")\n    if not block_dir.exists():\n        return {\"status\": \"ok\", \"deleted\": False}\n\n    try:\n        shutil.rmtree(block_dir)\n    except OSError as exc:\n        raise log_and_http_error(\n            exc,\n            500,\n            \"failed to delete uploaded files\",\n            event = \"data_recipe.seed.unstructured_block_delete_failed\",\n            log = logger,\n        ) from exc\n    if block_dir.exists():\n        raise HTTPException(500, \"failed to delete uploaded files\")\n    return {\"status\": \"ok\", \"deleted\": True}\n\n","sourceCodeStart":583,"sourceCodeEnd":619,"githubUrl":"https://github.com/unslothai/unsloth/blob/203007d19051dcd2ae33876786d117c99f6b0368/studio/backend/routes/data_recipe/seed.py#L583-L619","documentation":"HTTP 400 raised by DELETE /seed/unstructured-block/{block_id} as a defense-in-depth check: after resolving the path, block_dir must be relative to UNSTRUCTURED_UPLOAD_ROOT. With the prior regex guards this should never fire for well-formed ids; it exists to catch symlinked roots or path resolution surprises before shutil.rmtree runs.","triggerScenarios":"UNSTRUCTURED_UPLOAD_ROOT itself resolves somewhere unexpected (e.g. symlinked data dir, container volume mount oddities), making the resolved block path fall outside the resolved root; otherwise effectively unreachable because _UPLOAD_UID_RE blocks traversal characters.","commonSituations":"Deployment where the uploads root is a symlink (resolve() follows it) while the joined path resolves differently; misconfigured uploads root env var pointing at a different filesystem view; exotic mount setups in containers.","solutions":["Check the configured uploads root env/config: make sure it points to the real directory, not a symlink chain that resolves inconsistently.","Reproduce locally: print (UNSTRUCTURED_UPLOAD_ROOT / block_id).resolve() vs UNSTRUCTURED_UPLOAD_ROOT.resolve() to see the divergence.","Normalize the root path at startup (resolve it once) so both sides of is_relative_to agree."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// Ops-level: verify the configured root resolves consistently\nimport os, pathlib\nroot = pathlib.Path(os.environ['UNSTRUCTURED_UPLOAD_ROOT']).resolve()\nassert (root / 'probe').resolve().is_relative_to(root), 'root resolves inconsistently (symlink?)'","typeGuard":null,"tryCatchPattern":"On 400 'outside upload root', stop and inspect deployment config — this signals an environment/symlink problem, not a bad request; retrying cannot fix it.","preventionTips":["Point the uploads root at a real directory, not a symlink chain.","Resolve the root once at startup and use the resolved path everywhere.","Pin identical upload-root config across replicas."],"tags":["path-traversal","filesystem","http-400","symlink"],"backgroundTag":null,"analyzedSha":"203007d19051dcd2ae33876786d117c99f6b0368","analyzedAt":"2026-08-15T02:48:39.846Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}