{"record":{"id":"ad873ed7b195d05d","repo":"github/spec-kit","slug":"eloop","errorCode":"ELOOP","errorMessage":"Refusing to write through a symlinked download file","messagePattern":"Refusing to write through a symlinked download file","errorType":"exception","errorClass":"OSError","httpStatus":null,"severity":"error","filePath":"src/specify_cli/extensions/_commands.py","lineNumber":821,"sourceCode":"    root) immediately before an exclusive create. ``O_EXCL`` guarantees an\n    attacker cannot pre-stage the leaf as a symlink/junction, ``O_TEMPORARY``\n    makes the OS delete it on close, and a post-open inode-identity check\n    detects a leaf swapped underneath us. The returned descriptor is the only\n    handle installation ever uses, so the cache pathname is never reopened.\n    \"\"\"\n    zip_path = download_dir / zip_filename\n    project_root_resolved = project_root.resolve()\n\n    if download_dir.is_symlink() or not download_dir.is_dir():\n        raise OSError(\n            errno.ENOTDIR, \"Download cache directory is not a real directory\"\n        )\n    try:\n        download_dir.resolve().relative_to(project_root_resolved)\n    except (OSError, ValueError):\n        raise OSError(errno.ENOTDIR, \"Download cache directory escapes project root\")\n    if zip_path.is_symlink():\n        raise OSError(errno.ELOOP, \"Refusing to write through a symlinked download file\")\n\n    flags = os.O_RDWR | os.O_CREAT | os.O_EXCL\n    flags |= getattr(os, \"O_NOFOLLOW\", 0)\n    flags |= getattr(os, \"O_CLOEXEC\", 0)\n    flags |= getattr(os, \"O_BINARY\", 0)\n    o_temporary = getattr(os, \"O_TEMPORARY\", 0)\n    flags |= o_temporary\n\n    download_fd = os.open(zip_path, flags, 0o600)\n    try:\n        _verify_leaf_identity(download_fd, zip_path)\n    except OSError:\n        os.close(download_fd)\n        # Without O_TEMPORARY the leaf is not auto-deleted, so remove the file\n        # we just exclusively created (best effort, never through a symlink).\n        if not o_temporary:\n            try:\n                if not zip_path.is_symlink():","sourceCodeStart":803,"sourceCodeEnd":839,"githubUrl":"https://github.com/github/spec-kit/blob/bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c/src/specify_cli/extensions/_commands.py#L803-L839","documentation":"Pre-open guard in _safe_open_download_zip: the target zip_path inside the download cache is itself a symlink, and the tool refuses to write through it (ELOOP). Combined with O_CREAT|O_EXCL and O_NOFOLLOW this closes the pre-staged-symlink attack where a local attacker plants a link so the installer overwrites an arbitrary file.","triggerScenarios":"`specify extension install <url>` when a file with the exact transient name (extension-url-download-<hex>.archive) already exists as a symlink in the cache dir — planted, or left by a sync/AV tool converting files to links. Note the name embeds a fresh uuid4 per attempt, so a pre-existing link at that exact name is highly abnormal.","commonSituations":"Adversarial multi-user environments; file-sync clients (Dropbox/OneDrive-style) that replace in-flight files with placeholder symlinks; deliberately crafted test fixtures for the security suite.","solutions":["List and clear suspicious links in the cache area: `find .specify -type l -delete` (after review), then retry the install","Exclude .specify from file-sync/cloud-placeholder tooling so files stay real","Audit who else can write to the project's .specify and tighten permissions (go-w)","If it recurs, capture `ls -la` of the cache dir — a repeated hit at a random-uuid name indicates active interference"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"from pathlib import Path\n\ndef no_symlink_at(path: Path) -> bool:\n    return not path.is_symlink()","typeGuard":null,"tryCatchPattern":"import errno\nfrom specify_cli.extensions import ExtensionError\n\ntry:\n    install_from_url_cmd(project_root, url, speckit_version)\nexcept ExtensionError as e:\n    cause = e.__cause__\n    if isinstance(cause, OSError) and cause.errno == errno.ELOOP:\n        # symlink planted at the transient download name; clean cache dir, retry","preventionTips":["Sweep the cache area for unexpected symlinks before installs: find .specify -type l","Exclude .specify from cloud-sync placeholder tooling"],"tags":["extensions","security","symlink","eloop"],"backgroundTag":null,"analyzedSha":"bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c","analyzedAt":"2026-08-14T19:43:37.150Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}