FlawOpen

flawopen.com/Teardowns/cve-2024-32002-git-submodule-rce

● CVE-2024-32002 · CVSS 9.0 · Critical
FlawOpen Security Research

CVE-2024-32002: Git Submodule Symlink RCE

CVE-2024-32002 let a crafted Git repository write through a case-colliding symlink into .git during recursive clone, allowing a hook to run before inspection. Git’s upstream fix checks the submodule destination before and after cloning and aborts on unexpected contents; upgrade to a fixed Git release.

💡 Plain English Explainer (ELI5)

Imagine a warehouse where signs treat A and a as the same doorway. If a submodule’s route passes through a sign that redirects into the records room (.git), files meant for one storage bay can land in Git’s control records. Git’s fix checks the destination before and after cloning and stops when its contents are unexpected.

Core Concepts & Subsystem Terms

Git Submodule
A separate Git repository embedded as a subdirectory inside a parent repository.
Symlink
A symbolic link filesystem object that redirects path traversal to another target.
Case Collision
Filesystem behavior on NTFS and APFS where paths differing only by letter case resolve to the same inode.
Git Hooks
Executable scripts in .git/hooks/ triggered automatically by git actions like post-checkout.

Framework mappings

Root Cause Analysis

On a case-insensitive filesystem with symlink support, a crafted repository could give a submodule path that collided by case with a symlink. During recursive clone, Git could replace a directory created for the clone with that symlink and write files outside the submodule worktree into .git. A hook written there could run before the user had a chance to inspect the checkout. The upstream patch refuses to clone into a pre-existing non-empty submodule destination and, after cloning, rejects a destination whose contents are not limited to .git metadata.

Step-by-Step Attack Flow

Step 1

Recursive clone reaches attacker-controlled metadata

A user recursively clones an untrusted repository on a case-insensitive filesystem where Git supports symlinks. The repository supplies submodule paths and tracked entries that collide after case folding.

Step 2

The path collision crosses a trust boundary

Git can resolve the colliding submodule path through a symlink rather than into the intended submodule worktree. The affected boundary is Git’s filesystem path handling during recursive submodule clone.

Step 3

Files land in Git metadata

The redirected write can place a hook in the repository’s .git area. The Git project advisory confirms that a hook can be written there and run while clone is still in progress.

Step 4

The clone can execute code before review

Git runs the hook as part of the clone operation, before the user can inspect the checked-out files. Fixed Git releases add destination-state checks and abort when unexpected files are present.

Source Code: Flaw vs. Secure Implementation

This is an excerpt of Git’s upstream C change in clone_submodule(), not a standalone function. Before the fix, the shown clone path had no corresponding destination-content guard. The fix rejects a pre-existing non-empty checkout directory and checks after cloning that only .git metadata is present; its helper performs the directory-entry check. The actual implementation is linked in the upstream commit.

UNPATCHED FLAW
if (!file_exists(sm_gitdir)) {
    if (safe_create_leading_directories_const(sm_gitdir) < 0)
        die(_("could not create directory '%s'"), sm_gitdir);

    if (run_command(&cp))
        die(_("clone of '%s' into submodule path '%s' failed"),
            clone_data->url, clone_data_path);
}
HARDENED SECURE PATCH
struct stat st;

if (!file_exists(sm_gitdir)) {
    if (clone_data->require_init && !stat(clone_data_path, &st) &&
        !is_empty_dir(clone_data_path))
        die(_("directory not empty: '%s'"), clone_data_path);

    if (safe_create_leading_directories_const(sm_gitdir) < 0)
        die(_("could not create directory '%s'"), sm_gitdir);

    if (run_command(&cp))
        die(_("clone of '%s' into submodule path '%s' failed"),
            clone_data->url, clone_data_path);

    if (clone_data->require_init && !stat(clone_data_path, &st) &&
        !dir_contains_only_dotgit(clone_data_path)) {
        char *dot_git = xstrfmt("%s/.git", clone_data_path);
        unlink(dot_git);
        free(dot_git);
        die(_("directory not empty: '%s'"), clone_data_path);
    }
}

Engineering & System Hardening Checklist

Sources