惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Cisco Talos Blog
Cisco Talos Blog
阮一峰的网络日志
阮一峰的网络日志
云风的 BLOG
云风的 BLOG
D
Docker
Vercel News
Vercel News
IT之家
IT之家
Recent Announcements
Recent Announcements
Last Week in AI
Last Week in AI
V
Visual Studio Blog
Engineering at Meta
Engineering at Meta
腾讯CDC
Google DeepMind News
Google DeepMind News
I
InfoQ
博客园 - 三生石上(FineUI控件)
Apple Machine Learning Research
Apple Machine Learning Research
The GitHub Blog
The GitHub Blog
博客园 - Franky
The Cloudflare Blog
A
About on SuperTechFans
有赞技术团队
有赞技术团队
Y
Y Combinator Blog
T
Tenable Blog
P
Proofpoint News Feed
Recorded Future
Recorded Future
Security Latest
Security Latest
H
Hackread – Cybersecurity News, Data Breaches, AI and More
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
博客园 - 聂微东
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Google Online Security Blog
Google Online Security Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Simon Willison's Weblog
Simon Willison's Weblog
The Last Watchdog
The Last Watchdog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
N
News and Events Feed by Topic
TaoSecurity Blog
TaoSecurity Blog
U
Unit 42
The Hacker News
The Hacker News
Martin Fowler
Martin Fowler
T
Threat Research - Cisco Blogs
NISL@THU
NISL@THU
F
Full Disclosure
M
MIT News - Artificial intelligence
人人都是产品经理
人人都是产品经理
Hugging Face - Blog
Hugging Face - Blog
V
V2EX
Project Zero
Project Zero

dgl.cx

SSH port knocking with OpenBSD 7.9 SSH port knocking with OpenBSD 7.9 Bash a newline: Exploiting SSH via ProxyCommand, again (CVE-2025-61984) Switchable dark mode with 5 lines of JavaScript Images over DNS Can your terminal do emojis? How big? Blink and you'll miss it — a URL handler surprise Using HAProxy to protect me from scrapers Déjà vu: Ghostly CVEs in my terminal title Restrict sftp with Linux user namespaces ""?! ANSI Terminal security in 2023 and finding 10 CVEs NAT-Again: IRC NAT helper flaws ip.wtf and showing you your actual HTTP request
CVE-2025-48384: Breaking Git with a carriage return and cloning RCE
2025-07-10 · via dgl.cx

tl;dr: On Unix-like platforms, if you use git clone --recursive on an untrusted repo, it could achieve remote code execution. Update to a fixed version of Git and other software that embeds Git (including GitHub Desktop).

If you've ever used an old mechanical typewriter, you know that when you get to the end of the line there's a physical action to get back to the start of the line. Sometimes this was done through an actual lever on the typewriter, later models had a button. Because this action — the carriage return — was distinct from the line feed, it has its own character. In ASCII this is the character known as "Carriage Return", represented as "␍", character number 13. The "↵" icon, as often seen on the "Enter" or "Return" key on a modern keyboard encodes this action, along with the action of moving to the next line, known as "Line Feed" (␊).

This legacy from the very early days of communications (carriage return was introduced by the Murray code in 1901!) is still something we have to deal with. Unix attempted to simplify this by using just LF (a "\n" in C-string terms) to separate lines, but Windows and various Internet protocols use both CR+LF (a "\r\n" in C-string terms).

Why am I talking about this?

git has a simple .ini style configuration format, that looks like this:

[section]
         key = value

If this was just used in the user’s configuration files there wouldn’t be a concern about the formats that are accepted. However this configuration format is also used in the .gitmodules file, which is a file that is checked into the git repository that tracks submodules.

The format supports DOS line endings. It handles this in the fairly obvious way of stripping them when it reads lines, below is the function get_next_char in config.c:

static int get_next_char(struct config_source *cs)
{
        int c = cs->do_fgetc(cs);

        if (c == '\r') {
                /* DOS like systems */
                c = cs->do_fgetc(cs);
                if (c != '\n') {
                        if (c != EOF)
                                cs->do_ungetc(c, cs);
                        c = '\r';
                }
        }

The cs->do_fgetc call is nearly equivalent to the standard C function fgetc(), so even without knowledge of git's particular functions this is quite understandable.

It boils down to: If the character is a carriage return (\r), then look at the next character, if it is a line feed (\n), just return a line feed (and eat the carriage return). If however the next character isn’t a line feed, return a carriage return (\r) and put the next character back (ungetc), so it is returned next time.

The key thing to understand here is that this is done per line. So if for some reason a line happens to end with a CR, it will be stripped, regardless of the overall format of the file.

In addition to the code to read the configuration, git can also write configuration files, for example when the user uses git config to set a configuration value, it will use the code below to write key = value pairs out to the file.

static ssize_t write_pair(int fd, const char *key, const char *value, [...]
{
       [...]

       /*
         * Check to see if the value needs to be surrounded with a dq pair.
         * Note that problematic characters are always backslash-quoted; this
         * check is about not losing leading or trailing SP and strings that
         * follow beginning-of-comment characters (i.e. ';' and '#') by the
         * configuration parser.
         */
        if (value[0] == ' ')
                quote = "\"";
        for (i = 0; value[i]; i++)
                if (value[i] == ';' || value[i] == '#')
                        quote = "\"";
        if (i && value[i - 1] == ' ')
                quote = "\"";

        strbuf_addf(&sb, "\t%s = %s", key + store->baselen + 1, quote);

The actual bug is in the interaction of the above get_next_char function with this code. Elsewhere in the configuration reading code git supports quoted strings, using double quotes. However, when it writes a value back out to the configuration file, it will quote it only if it contains spaces in certain positions, or a ; or # anywhere. This means when it later reads the value write_pair wrote to the configuration file back, it can be tricked to drop a final \r off the value.

So, if we have a file with key = "foo^M", when it's written out again it becomes key = foo^M (where ^M is the literal CR character).

As I alluded to already, .gitmodules is untrusted, so if we can confuse the config parser, maybe that’s enough to confuse the code that handles submodules within git?

On Unix based systems (not Windows), it is possible to have control characters in filenames, so a path entry like the following in .gitmodules will attempt to check out the module into a directory named with a control character in its name.

[submodule "foo"]
  path = "foo^M"

However when this is written out by git’s configuration code to .git/modules/foo/config, it will look something like this:

[core]
  workdir = ../../../foo^M

The validation has already happened against the untrusted path that was read from .gitmodules. This means the path then essentially changes after validation because when it is read, the config reading code strips off the final \r.

The result of all this, is when a submodule clone is performed, it might read one location from path = ..., but write out a different path that doesn’t end with the ^M.

This simple primitive is enough to confuse git, such that when it checks out a submodule the contents of it will be written to a different path. This is very similar to CVE-2024-32002 where case-insensitivity (or lack of) in submodules could be used to confuse git. Ironically, that bug required a case-insensitive file system, this one requires a file system that allows control characters in filenames, therefore Windows is not directly vulnerable to this particular bug (and macOS is vulnerable to both CVE-2024-32002 and CVE-2025-48384).

When cloning on the command line, git clone on its own is not enough to clone submodules, so a manual mitigation for this is to use git clone without --recursive first, examine .gitmodules to check it is safe and then init the submodules.

However GitHub Desktop automatically clones with the recursive option by default, so if you use GitHub Desktop to clone, this can happen:

GitHub Desktop Demo

▶️

The patch for this is surprisingly simple, ensure that in write_pair, when writing a string with a carriage return in it, it is quoted. (Technically only the last character needs quoting, but it is safer to always quote.)

 	for (i = 0; value[i]; i++)
-		if (value[i] == ';' || value[i] == '#')
+		if (value[i] == ';' || value[i] == '#' || value[i] == '\r')
 			quote = "\"";

The confused write primitive can be used to put malicious files from a submodule in nearly any location on the filesystem and achieve arbitrary file write (as it has bypassed validation it can follow symlinks outside the repository). The most straightforward way to exploit this is to use it to write inside the .git directory and create a hook script, leading to attacker controlled code execution when the hook is run by Git, however there are other possibilities such as overwriting .git/config. I'm not sharing a PoC yet, but it is an almost trivial modification of an exploit for CVE-2024-32002. There is also a test in the commit fixing it that should give large hints.

This is not the first time the carriage return has caused issues for Git, in January RyotaK found issues with the credential helper protocol that could also be tricked with carriage returns. It is also not the first time issues have been found with the configuration parsing, in 2023 there was a logic error found by André Baptista and Vítor Pinho.

I find this particularly interesting because this isn't fundamentally a problem of the software being written in C. These are logic errors that are possible in nearly all languages, the common factor being this is a vulnerability in the interprocess communication of the components (either between git and external processes, or within the components of git itself). It is possible to draw a parallel with CRLF injection as seen in HTTP (or even SMTP smuggling). For a long time the Internet worked on Postel's robustness principle:

Be conservative in what you do, be liberal in what you accept from others

However that may not be the most sensible advice now. This is covered in more detail than I can put here in RFC 9413.

This was found as part of an audit of Git and there are several other bugs I found of varying severity fixed in the releases today. Thanks to G-Research Open Source for enabling me to work on this.

9th July 2025