{"record":{"id":"fa33fba15315707f","repo":"abi/screenshot-to-code","slug":"invalid-image-path-filename-r","errorCode":null,"errorMessage":"Invalid image path: {filename!r}","messagePattern":"Invalid image path: (.+?)","errorType":"exception","errorClass":"InvalidSetNameError","httpStatus":null,"severity":"error","filePath":"backend/evals/sets.py","lineNumber":300,"sourceCode":"            os.path.isdir(os.path.join(set_dir, \"inputs\"))\n            or os.path.isfile(os.path.join(set_dir, \"briefs.json\"))\n        ):\n            continue\n        infos.append(get_set(entry))\n    return infos\n\n\ndef resolve_set_image_path(set_name: str, filename: str) -> str:\n    \"\"\"Absolute path of a set image; raises on traversal or missing file.\"\"\"\n    inputs_dir = get_set_inputs_dir(set_name)\n    if not os.path.isdir(inputs_dir):\n        raise EvalSetNotFoundError(f\"Eval set not found: {set_name}\")\n    safe_name = os.path.basename(filename)\n    if not safe_name.lower().endswith(\".png\"):\n        raise InvalidSetNameError(f\"Not a set image: {filename!r}\")\n    path = os.path.realpath(os.path.join(inputs_dir, safe_name))\n    if not path.startswith(os.path.realpath(inputs_dir) + os.sep):\n        raise InvalidSetNameError(f\"Invalid image path: {filename!r}\")\n    if not os.path.isfile(path):\n        raise FileNotFoundError(path)\n    return path\n","sourceCodeStart":282,"sourceCodeEnd":304,"githubUrl":"https://github.com/abi/screenshot-to-code/blob/d026163f586dfa8c5c10d28c36edd59a9d3b0e88/backend/evals/sets.py#L282-L304","documentation":"resolve_set_image_path raises InvalidSetNameError(\"Invalid image path\") when the realpath of inputs_dir + basename(filename) escapes the realpath'd inputs directory. This is the path-traversal guard: although basename() already strips directory components, a symlinked file inside inputs/ whose target lives outside would still resolve outside, and the prefix check catches that. It fires when the resolved path does not start with realpath(inputs_dir) + os.sep.","triggerScenarios":"A symlink inside sets/{name}/inputs/ pointing to a file outside the set directory (realpath resolves through it); on platforms where the inputs dir itself is a symlink and path prefix logic mismatches; virtually unreachable for plain string traversal like \"../../etc/passwd\" because basename() neutralizes it first.","commonSituations":"Users symlinking shared image folders into a set's inputs dir instead of copying files; odd mounts or bind mounts making realpath prefixes inconsistent.","solutions":["Replace symlinks in inputs/ with real file copies (cp -L).","If shared images are needed, copy them into the set directory.","Caller-side, reject any filename containing '/' or '..' before calling resolve."],"exampleFix":"# before\nln -s ~/shared/shot.png sets/my-set/inputs/shot.png\n\n# after\ncp ~/shared/shot.png sets/my-set/inputs/shot.png","handlingStrategy":"validation","validationCode":"import os\n\ndef is_safe_image_filename(filename: str) -> bool:\n    base = os.path.basename(filename)\n    return (\n        base == filename\n        and \"/\" not in filename\n        and \"\\\\\" not in filename\n        and \"..\" not in filename\n        and base.lower().endswith(\".png\")\n    )","typeGuard":null,"tryCatchPattern":"try:\n    path = resolve_set_image_path(set_name, filename)\nexcept InvalidSetNameError as e:\n    abort(400, description=str(e))  # traversal or non-png: client error, not 500","preventionTips":["Never put symlinks in a set's inputs/ — copy files instead","Pass plain filenames from os.listdir, not user-supplied paths","Treat InvalidSetNameError from this function as a 400 (client-supplied), not a server fault"],"tags":["evals","security","path-traversal","symlink"],"backgroundTag":null,"analyzedSha":"d026163f586dfa8c5c10d28c36edd59a9d3b0e88","analyzedAt":"2026-08-14T22:02:06.951Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}