{"id":"5032ec375c6a120e","repo":"pypa/pip","slug":"invalid-script-entry-point-name-entry-name-r-th","errorCode":null,"errorMessage":"Invalid script entry point name {entry.name!r}: the script would be installed outside the scripts directory ({scripts_dir}).","messagePattern":"Invalid script entry point name (.+?): the script would be installed outside the scripts directory \\((.+?)\\)\\.","errorType":"exception","errorClass":"InstallationError","httpStatus":null,"severity":"critical","filePath":"src/pip/_internal/operations/install/wheel.py","lineNumber":411,"sourceCode":"            \"information.\"\n        )\n\n\ndef _raise_for_invalid_entrypoint(specification: str, scripts_dir: str) -> None:\n    entry = get_export_entry(specification)\n    if entry is None:\n        return\n\n    if entry.suffix is None:\n        raise MissingCallableSuffix(str(entry))\n\n    # distlib joins the entry point name onto the scripts directory, so a name\n    # with path separators or ``..`` components can resolve elsewhere. The script\n    # must resolve to a path strictly inside the scripts directory.\n    dest = os.path.join(scripts_dir, entry.name)\n    resolves_to_scripts_dir = os.path.abspath(dest) == os.path.abspath(scripts_dir)\n    if resolves_to_scripts_dir or not is_within_directory(scripts_dir, dest):\n        raise InstallationError(\n            f\"Invalid script entry point name {entry.name!r}: the script \"\n            f\"would be installed outside the scripts directory ({scripts_dir}).\"\n        )\n\n\nclass PipScriptMaker(ScriptMaker):\n    # Override distlib's default script template with one that\n    # doesn't import `re` module, allowing scripts to load faster.\n    script_template = textwrap.dedent(\"\"\"\\\n        import sys\n        from %(module)s import %(import_name)s\n        if __name__ == '__main__':\n            sys.argv[0] = sys.argv[0].removesuffix('.exe')\n            sys.exit(%(func)s())\n\"\"\")\n\n    def make(\n        self, specification: str, options: dict[str, Any] | None = None","sourceCodeStart":393,"sourceCodeEnd":429,"githubUrl":"https://github.com/pypa/pip/blob/d7d0d0a39494e28ec1c407bd0680e4a4d1067791/src/pip/_internal/operations/install/wheel.py#L393-L429","documentation":"Raised as InstallationError when a console/gui script entry point name contains path separators or '..' components that would cause the generated wrapper to be written outside the scripts directory. At wheel.py:408-414, _raise_for_invalid_entrypoint joins entry.name onto scripts_dir and checks both that it doesn't resolve exactly to scripts_dir and that it stays within is_within_directory(scripts_dir, dest).","triggerScenarios":"An entry point whose name is something like '../evil', 'sub/dir/cmd', or an empty/relative name, so os.path.abspath(os.path.join(scripts_dir, entry.name)) escapes scripts_dir. This is a path-traversal guard on entry point names.","commonSituations":"Malicious or malformed wheel with a crafted entry point name attempting to write outside the scripts dir. Also a buggy build config that accidentally includes a slash in a script name.","solutions":["Inspect the wheel's entry_points.txt for any name containing '/', '\\\\', or '..'.","Fix the script name to be a plain filename (no path separators, no relative components).","Rebuild and reinstall the corrected wheel.","If the wheel came from an untrusted source, treat this as a potential supply-chain attack and audit the file."],"exampleFix":"# before (entry_points.txt)\n[console_scripts]\n../payload = mypkg.payload:run\n\n# after\n[console_scripts]\npayload = mypkg.payload:run","handlingStrategy":"validation","validationCode":"import os, re, configparser, zipfile\n\ndef validate_script_names(whl, scripts_dir):\n    bad = []\n    with zipfile.ZipFile(whl) as z:\n        for n in z.namelist():\n            if n.endswith(\"entry_points.txt\"):\n                cp = configparser.ConfigParser()\n                cp.read_string(z.read(n).decode())\n                for sec in (\"console_scripts\", \"gui_scripts\"):\n                    for name in cp[sec] if sec in cp else {}:\n                        dest = os.path.join(scripts_dir, name)\n                        if os.path.abspath(dest) == os.path.abspath(scripts_dir) or \\\n                           not dest.startswith(os.path.abspath(scripts_dir) + os.sep):\n                            bad.append(name)\n    if bad:\n        raise ValueError(f\"unsafe entry point names: {bad}\")","typeGuard":"import os\ndef is_safe_script_name(name: str, scripts_dir: str) -> bool:\n    dest = os.path.join(scripts_dir, name)\n    if os.path.abspath(dest) == os.path.abspath(scripts_dir):\n        return False\n    return os.path.abspath(dest).startswith(os.path.abspath(scripts_dir) + os.sep)","tryCatchPattern":null,"preventionTips":["Reject entry point names containing path separators or '..' at build time.","Run a wheel-validation step in CI before publishing.","Treat any name with '/' or '\\\\' as a packaging defect or security issue."],"tags":["wheel","entry-points","path-traversal","security","install"],"analyzedSha":"d7d0d0a39494e28ec1c407bd0680e4a4d1067791","analyzedAt":"2026-08-04T20:55:04.259Z","schemaVersion":2}