Skip to content

Require Git 2.38+ for app security checks - #8856

Open
jek wants to merge 3 commits into
mainfrom
git-version
Open

jek wants to merge 3 commits into
mainfrom
git-version

Conversation

@jek

@jek jek commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

WHY are these changes introduced?

shopify app security check runs Git while inspecting untrusted repositories. Its protections rely on safe.bareRepository, which was introduced in Git 2.38. Older versions can't enforce the complete protection set.

WHAT is this pull request doing?

Require Git 2.38 or later before every Git command app security runs. A version manager can select a different Git for each working directory, so the version is checked in the directory where each command runs. The check runs Git the same way the scan does, so it also refuses a git executable in the scanned directory.

A scan that doesn't use Git, such as --no-git-ignore with no Git-dependent secret candidate, still runs without it.

When Git is missing, can't run, reports an unrecognized version, or is too old, the command stops with an actionable error. The version guard lives in CLI Kit for reuse, but only app security enforces it today. Other Git-using commands are unchanged.

Git versions before 2.45 can't turn off lazy fetching, and their empty transport allow-list still allows a helper with an empty name, which a remote gets from an empty vcs or a URL that starts with ::. Git runs that helper through the repository's remote- alias, so every Git command now also replaces that alias.

This also fixes CLI Kit's check that refuses a program found in the working directory, which never worked on Windows: it joined the directory to PATH with :, and Windows splits PATH on ;. It now looks up a bare command name as a file in the working directory, so either path separator works. A command given as a path, such as ./build.sh, runs the file the caller named and isn't checked. This applies to every command the CLI runs.

How to manually test your changes?

  1. Create a temporary app and a Git executable that reports an unsupported version:

    real_git="$(command -v git)"
    app="$(mktemp -d)"
    fake_git="$(mktemp -d)"
    cp packages/e2e/data/valid-app/shopify.app.toml "$app/shopify.app.toml"
    printf 'export const answer = 42\n' > "$app/index.ts"
    printf '#!/bin/sh\necho "git version 2.37.6"\n' > "$fake_git/git"
    chmod +x "$fake_git/git"
  2. Run file listing with default Git filtering:

    PATH="$fake_git:$PATH" node packages/cli/bin/dev.js app security check \
      --path "$app" \
      --list-files

    Confirm that it exits before gathering and reports that Git 2.38 or later is required.

  3. Turn off Git filtering:

    PATH="$fake_git:$PATH" node packages/cli/bin/dev.js app security check \
      --path "$app" \
      --no-git-ignore \
      --list-files

    Confirm that it lists the app files and exits successfully because this scan doesn't use Git.

  4. Replace the fake Git with one that, like a version manager, selects an old Git only in a nested repository:

    mkdir "$app/nested"
    "$real_git" -C "$app/nested" init -q
    touch "$app/nested/.old-git"
    cat > "$fake_git/git" <<EOF
    #!/bin/sh
    if [ "\$1" = "--version" ]; then
      if [ -f .old-git ]; then echo "git version 2.37.6"; else echo "git version 2.55.0"; fi
      exit 0
    fi
    exec "$real_git" "\$@"
    EOF
    PATH="$fake_git:$PATH" node packages/cli/bin/dev.js app security check \
      --path "$app" \
      --list-files

    Confirm that it reports Git 2.37.6 is installed, even though the app directory selects Git 2.55.0.

  5. Remove the temporary directories:

    rm -rf "$app" "$fake_git"

Checklist

  • I've considered possible cross-platform impacts (Mac, Linux, Windows)
  • I've considered possible documentation changes
  • I've considered analytics changes to measure impact
  • The change is user-facing — I've identified the correct bump type (patch for bug fixes · minor for new features · major for breaking changes) and added a changeset with pnpm changeset add

@jek
jek requested review from a team as code owners October 9, 2026 15:11
@github-actions github-actions Bot added the Area: @shopify/cli @shopify/cli package issues label Oct 9, 2026
@jek
jek force-pushed the git-version branch 5 times, most recently from 990a119 to 90592ea Compare October 9, 2026 16:49
@jek
jek requested review from jplhomer and lopez-mar October 9, 2026 16:50

@dmerand dmerand left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM with the exception of this issue, which we discussed async.

await inTemporaryDirectory(async (directory) => {
writeFileSync(joinPath(directory, 'git.cmd'), '@echo git version 2.55.0\r\n')

await expect(git.ensureGitVersionIsAtLeast('2.38.0', {cwd: directory})).rejects.toThrow(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new Windows-only regression fails in Windows Node 26 CI: it expects /Skipped run of unsecure binary/, but receives Couldn't determine the installed Git version. The macOS smoke run skips this test.

Could we fix the Windows refusal/test behavior and get the Windows job plus required Unit tests green before landing? Please keep the planted-script regression. Since execa is mocked here, this failure alone does not prove that git.cmd ran.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pushed as an additional commit

@lopez-mar lopez-mar left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

approved pending what we discussed async

jek added 3 commits October 9, 2026 12:30
Add a reusable CLI Kit Git version guard. App security checks the Git
selected in a directory before every Git command it runs there, because
a version manager can select Git by working directory. Git 2.38 is
required for the protections used when reading untrusted repositories.
The check that refuses a command found in the working directory joined
that directory to PATH with pathe's `:` delimiter, but `which` splits
on `;` on Windows, so the directory was never searched. Look a bare
command name up as a file in the working directory instead, which also
handles either path separator and a directory whose name contains the
delimiter. A command given as a path runs the file the caller named, so
it isn't checked.
Git's empty transport allow-list still allows a helper with an empty
name, which a remote gets from an empty `vcs` or a URL that starts with
`::`. Git runs it as `git remote-`, which falls back to the
repository's `remote-` alias. Replace that alias on every Git command,
for Git versions that can't turn off lazy fetching.
@github-actions

github-actions Bot commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

Differences in type declarations

We detected differences in the type declarations generated by Typescript for this branch compared to the baseline ('main' branch). Please, review them to ensure they are backward-compatible. Here are some important things to keep in mind:

  • Some seemingly private modules might be re-exported through public modules.
  • If the branch is behind main you might see odd diffs, rebase main into this branch.

New type declarations

We found no new type declarations in this PR

Existing type declarations

packages/cli-kit/dist/public/node/git.d.ts
@@ -102,6 +102,17 @@ export declare function getHeadSymbolicRef(directory?: string): Promise<string>;
  * an abort error.
  */
 export declare function ensureGitIsPresentOrAbort(): Promise<void>;
+export interface GitVersionCheckOptions {
+    /** The directory to run Git in. A version manager can select a different Git for each working directory. */
+    cwd?: string;
+}
+/**
+ * Aborts when Git isn't installed, can't run, reports an unrecognized version, or is older than the required minimum.
+ *
+ * @param minimumVersion - The oldest supported Git version, in semantic version format.
+ * @param options - Where to run Git.
+ */
+export declare function ensureGitVersionIsAtLeast(minimumVersion: string, options?: GitVersionCheckOptions): Promise<void>;
 export declare class OutsideGitDirectoryError extends AbortError {
 }
 /**

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area: @shopify/cli @shopify/cli package issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants