{"record":{"id":"651ca9878e90053f","repo":"unslothai/unsloth","slug":"invalid-image-filename","errorCode":null,"errorMessage":"Invalid image filename.","messagePattern":"Invalid image filename\\.","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"error","filePath":"studio/backend/routes/training.py","lineNumber":3673,"sourceCode":"        )\n    except (OSError, UnidentifiedImageError, ValueError):\n        return  # not a decodable image -> not a bomb; leave the existing contract\n    if width > _MAX_TRAINING_IMAGE_SIDE or height > _MAX_TRAINING_IMAGE_SIDE:\n        raise HTTPException(\n            status_code = 400,\n            detail = (\n                f\"Image '{original_name}' is too large ({width}x{height}); maximum is \"\n                f\"{_MAX_TRAINING_IMAGE_SIDE}px per side.\"\n            ),\n        )\n\n\ndef _safe_dataset_image_path(folder: Path, filename: str) -> Path:\n    \"\"\"Resolve ``filename`` to an image path strictly inside ``folder``. Rejects any path\n    separators / traversal / null bytes and non-image extensions.\"\"\"\n    raw = filename or \"\"\n    if \"/\" in raw or \"\\\\\" in raw or \"..\" in raw or \"\\x00\" in raw or raw != Path(raw).name:\n        raise HTTPException(status_code = 400, detail = \"Invalid image filename.\")\n    if Path(raw).suffix.lower() not in _DIFFUSION_DATASET_IMAGE_EXTS:\n        exts = \", \".join(sorted(_DIFFUSION_DATASET_IMAGE_EXTS))\n        raise HTTPException(status_code = 400, detail = f\"Not an image file. Allowed: {exts}\")\n    path = folder / raw\n    # Defense in depth: the real path must stay under the dataset folder.\n    try:\n        path.resolve().relative_to(folder.resolve())\n    except ValueError:\n        raise HTTPException(status_code = 400, detail = \"Invalid image filename.\")\n    return path\n\n\ndef _load_metadata_captions(folder: Path) -> dict[str, str]:\n    \"\"\"Read metadata.jsonl / captions.jsonl into {file_name: caption}, mirroring the\n    trainer's discovery (keys file_name/video/image/file; caption in the ``text`` column).\"\"\"\n    import json\n\n    out: dict[str, str] = {}","sourceCodeStart":3655,"sourceCodeEnd":3691,"githubUrl":"https://github.com/unslothai/unsloth/blob/203007d19051dcd2ae33876786d117c99f6b0368/studio/backend/routes/training.py#L3655-L3691","documentation":"HTTP 400 from _safe_dataset_image_path: the requested image filename failed the lexical safety check — it contains '/', '\\\\', '..', a NUL byte, or does not equal its own Path(raw).name (i.e. it is not a single clean path component). This is the first, string-level guard against path traversal before any filesystem access. A second, identical 400 exists after resolve() as defense in depth.","triggerScenarios":"Calling an image-scoped dataset route (serve image, update caption, delete image) with a filename parameter like '../../etc/passwd', 'sub/dir/img.png', 'img\\x00.png', or anything that is not a bare filename. Also triggered by URL-encoded separators (%2F) that decode before this check.","commonSituations":"Scripted clients joining folder + filename into the path parameter; UI bugs that pass a relative path instead of a name; probing attempts against the endpoint; copy-pasting paths from a file manager into an API call.","solutions":["Pass only the bare filename (single component, no directories): GET /training/diffusion/dataset/myset/image/cat.png — never a path.","Fix the client to use Path(filename).name (or basename) before filling the URL parameter.","If you genuinely need nested layout, flatten it: images must live directly in the dataset folder by contract."],"exampleFix":"# before\nfilename = str(relative_path)  # 'day1/cat.png' -> 400\n# after\nfrom pathlib import PurePosixPath\nfilename = PurePosixPath(relative_path).name  # 'cat.png'","handlingStrategy":"validation","validationCode":"from pathlib import Path\n\ndef is_safe_image_filename(raw: str) -> bool:\n    return bool(raw) and '/' not in raw and '\\\\' not in raw \\\n        and '..' not in raw and '\\x00' not in raw and raw == Path(raw).name","typeGuard":"def is_invalid_filename_error(exc: HTTPException) -> bool:\n    return exc.status_code == 400 and exc.detail == 'Invalid image filename.'","tryCatchPattern":"try:\n    await api.get(f'/training/diffusion/dataset/{name}/image/{quote(filename)}')\nexcept HTTPStatusError as e:\n    if e.response.status_code == 400 and e.response.json()['detail'] == 'Invalid image filename.':\n        filename = Path(filename).name  # sanitize once, retry with bare name\n        raise_if_still_unsafe(filename)","preventionTips":["Always send Path(name).name, never a joined path, in image URL parameters.","URL-encode filenames (quote()) so separators are visible at the client, not decoded server-side.","Reject filenames containing '..' or separators in your own input layer."],"tags":["security","path-traversal","validation","fastapi","http-400"],"backgroundTag":null,"analyzedSha":"203007d19051dcd2ae33876786d117c99f6b0368","analyzedAt":"2026-08-15T02:48:39.846Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}