Adversarial diffs, side by side

What you review is not always what git applies.

A diff is drawn by tooling: your pager, a GUI, a terminal. Whoever wrote the change can target that drawing so the reviewer sees one thing and git commits another.

22 branches · 5 seen in the wild · one surprise each

What is this? When you review a code change before accepting it, you trust that what is on your screen is what you are actually approving. This page collects ways an attacker can make those two differ (the screen shows harmless code while git stores something else) and shows the tools that catch each one. Every example is safe to open.

What you see

The rendered diff. Bytes are reordered, hidden, disguised as other characters, or dropped entirely. It looks ordinary; often it looks exactly like the code beside it.

What's really happening

The same change, run through a safe review tool. Dangerous bytes are rendered inert and every anomaly (content, filename, mode, symlink, submodule, ref name) is named out loud. When it can't render safely, it stops.

Seen in the wild

Start here: the tricks with documented real-world exploitation: named campaigns, nation-state actors, and CVEs in CISA's Known Exploited Vulnerabilities catalog. The rest of the examples follow.

Seen in the wild Submodule

Submodule pointer bump

A submodule pointer moves to a different, unfetched commit. The one-line Subproject commit diff is easy to wave through. A submodule is a bookmark to a specific commit in another repo; only the bookmark moved, but it can now point at booby-trapped code.

git diff
-Subproject commit 1111111...
+Subproject commit 2222222...

# points at a commit you don't have
git-diff-review
Submodule 'vendor/sub':
   1111111... -> 2222222...
[ERROR] submodule not an initialized git repo;
        cannot show diff. Failing closed.

The catch: the tool surfaces the pointer move and fails closed: when it can't safely show the changed submodule, it refuses rather than pretend it reviewed it.

Backdoor path: Bump the gitlink to a commit the reviewer can't see; on recursive clone or CI that submodule's code (and its hooks) run. The review only ever saw a 40-char hash.

Seen in the wild: CVE-2025-48384, a malicious-submodule RCE, is in CISA's KEV catalog (its list of actively-exploited flaws), and CrowdStrike documented in-the-wild attacks that clone a booby-trapped repo against developers. Public reporting has not tied it to a specific named group. The related submodule-clone RCEs CVE-2024-32002 and CVE-2018-11235 remain proof-of-concept only.

git diff master..type/submodule-bump
Seen in the wild Filename

Bidi override in a filename

The filename below carries a right-to-left override in the middle, so its real .txt tail renders flipped: the file looks like it ends in an image extension.

git status
  new file:   report‮gpj.txt‬

# you read ".jpg"; git stored ".txt"
git-diff-review
[WARN] path 'report_gpj.txt' has
       suspicious bytes:
    -> U+202E RIGHT-TO-LEFT OVERRIDE (Cf)

The catch: the path is scanned like content is; the hidden control character is reported and the real filename ending is shown.

Backdoor path: Commit setup<RLO>gpj.sh that displays as setup.jpg; reviewers approve an 'image' while the build runs an executable script.

Seen in the wild: right-to-left override filenames (MITRE ATT&CK T1036.002) used in real spearphishing by Ke3chang, BlackTech, BRONZE BUTLER and Ferocious Kitten.

git diff master..path/bidi-filename

Proof of concept

A CVE or research disclosure exists for these, but no documented in-the-wild use. Proof-of-concept and papers, not a known campaign.

Proof of concept Trojan Source · CVE-2021-42574

Bidirectional override

A single invisible U+202E flips the reading order of a line. The logic you read runs backwards from the logic that executes. Like reading a line in a mirror: the letters you see spell something different from what runs.

git diff
   const char *access_level(int admin) {
+     /* return ‮nimda‬ */ return admin ? "user" : "admin";
   }

# reads one way, the bytes say another
git-diff-review
[WARN] src/hello.c suspicious Unicode:
  hello.c:4: /* return [U+202E]nimda[U+202C] */ ...
    -> U+202E RIGHT-TO-LEFT OVERRIDE (Cf)
    -> U+202C POP DIRECTIONAL FORMATTING (Cf)
--- diff (neutralized) ---
+ /* return _nimda_ */ return admin ? "user" : "admin";

The catch: the override never reaches your terminal; it becomes an inert _, and the exact codepoint is named.

Backdoor path: Hide a bidi override in a comment or string so the branch reviewers read as a safe check actually compiles as its opposite: e.g. an authorization test that silently always passes.

Proof of concept: CVE-2021-42574 (Trojan Source): research disclosure; no documented in-the-wild source-code use.

git diff master..content/bidi-trojan-source
Proof of concept Look-alike

Homoglyph identifier

The a below is Cyrillic (U+0430), not Latin. It is a different identifier that renders pixel-for-pixel the same: the same trick as a lookalike domain like paypaI.com (capital i), identical to the eye, different underneath.

git diff
+   return vаlidate("role");

# looks like validate(); it isn't
git-diff-review
[WARN] src/hello.c suspicious Unicode:
    -> U+0430 CYRILLIC SMALL LETTER A (Ll)
--- diff (neutralized) ---
+ return v_lidate("role");

The catch: two identical-looking identifiers resolve to different symbols. The tool marks the imposter character.

Backdoor path: Name a function or variable with a look-alike letter so a call the reviewer reads as validate() actually invokes a different, attacker-defined symbol.

Proof of concept: CVE-2021-42694: the homoglyph half of Trojan Source; disclosure only.

git diff master..content/homoglyph-identifier
Proof of concept Terminal injection

ANSI escape sequence

A cursor-up + erase-line escape (not a color code) rewrites the previous line. Your terminal shows PASS over the real FAIL.

cat config.env
STATUS=PASS

# the escape erased "STATUS=FAIL" a line above
git-diff-review
[WARN] data/config.env suspicious Unicode:
    -> U+001B ESC (Cc)
--- diff (neutralized) ---
+ STATUS=FAIL_[1A_[2KSTATUS=PASS

The catch: the escape is neutralized, so FAIL is right there in plain sight. (Real color codes are kept on purpose.)

Backdoor path: Embed a cursor-up + erase-line escape in the diff or a build log so the reviewer's terminal repaints the malicious line as benign: the backdoor lands while the screen says it never existed.

Proof of concept: CVE-2025-55754: disclosed in Apache Tomcat; the advisory reports no known attack vector.

git diff master..content/ansi-escape

File contents

Bytes inside a file. What a viewer shows you may not match the bytes stored on disk.

Invisible

Zero-width character

A U+200B splits a token where you see none. ADMIN​TOKEN and ADMINTOKEN look identical; only one matches: an invisible extra character wedged inside a word.

git diff
+ ADMIN​TOKEN=granted

# one hidden character lives between N and T
git-diff-review
[WARN] data/config.env suspicious Unicode:
    -> U+200B ZERO WIDTH SPACE (Cf)
--- diff (neutralized) ---
+ ADMIN_TOKEN=granted

The catch: the invisible becomes visible: one inert _ exactly where the zero-width byte hid.

Backdoor path: Split an identifier with a zero-width character so admin_token and admin<zwsp>_token look identical but bind to different values, routing a secret to the attacker's name.

git diff master..content/zero-width
Valid but confusing

Unicode whitespace

A no-break space (U+00A0) stands in for an ordinary space. Perfectly legitimate, endlessly confusing, and it must be surfaced, not treated as an error.

git diff
+ RETRY_LIMIT = 0

# those aren't ASCII spaces
git-diff-review
[WARN] data/config.env suspicious Unicode:
    -> U+00A0 NO-BREAK SPACE (Zs)
    -> U+00A0 NO-BREAK SPACE (Zs)
--- diff (neutralized) ---
+ RETRY_LIMIT_=_0

The catch: a benign-but-baffling change is shown plainly rather than crashing the review: an ordinary-looking space that is really a Unicode character.

Backdoor path: Swap an ASCII space for U+00A0 in a config key, so the parser reads a different key than the reviewer: a hardening flag that looks set is silently ignored.

git diff master..content/unicode-whitespace
Hidden by length

Over-long line

One 6000-character line. Pagers truncate it or hang; the interesting part scrolls off the right edge into oblivion.

git diff
+ payload=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA... (6000)

# what's hiding past column 200?
git-diff-review
[WARN] data/config.env has a '6008'-char line;
        a viewer may truncate/hang.

The catch: the length is called out up front, so a truncating viewer can't quietly swallow the tail.

Backdoor path: Pad the line with thousands of characters so a payload sits far past where the pager truncates: off-screen, and unreviewed.

git diff master..content/overlong-line
Hidden as binary

Embedded NUL byte

One \0 makes git treat the file as binary. The real change is never rendered: git just says the files "differ".

git diff
Binary files a/data/config.env and
b/data/config.env differ

# the actual change is invisible
git-diff-review
[WARN] data/config.env suspicious Unicode:
    -> U+0000 NUL (Cc)
[NOTICE] looks BINARY (NUL byte); shown as
         --stat only, not opened in the viewer.

The catch: the binary flip itself is flagged, so "git treated this as binary" can't be mistaken for "nothing to see".

Backdoor path: One NUL byte flips a source file to 'binary', so git diff shows only 'Binary files differ' and the added backdoor line is never rendered for review.

git diff master..content/nul-byte

Paths

The same tricks, moved into the filename or into git's own metadata, where a diff viewer often prints raw, unchecked bytes.

Fails closed

.gitattributes that hides a change

Add a .gitattributes marking a changed file -diff, and a plain git diff shows nothing for it. The edit ships unseen. Like a note on a package telling the inspector 'don't open this box', and the inspector obeys.

git diff
+ secret.txt -diff
  (.gitattributes)

# secret.txt WAS changed, but not shown
git-diff-review
[ERROR] .gitattributes changed: it can hide
        OTHER files' contents from the diff.
        Failing closed.
[NOTICE] set GIT_REVIEW_ALLOW_GITATTRIBUTES=1
         to review anyway.

The catch: rather than trust an attribute that can blind it, the tool refuses. The same holds when the file sits in a non-ASCII directory that git quotes in its output.

Backdoor path: Commit a .gitattributes marking auth.c -diff, then backdoor auth.c: the pull request shows no change for that file, so the edit ships invisibly.

git diff master..path/gitattributes-hide-diff

Types & metadata

Changes with no content at all: a mode bit, a symlink target, a submodule pointer. A content diff shows nothing; the danger is in the metadata.

Invisible change

Executable bit, no content change

Flip a file to +x without touching a byte of content. A content diff is empty; the file is now runnable.

git diff
(no visible change)

# same bytes, so a content diff is blank
git-diff-review
MODE CHANGE 'src/build.sh':
     100644 -> 100755
[NOTE] 'src/build.sh' is now EXECUTABLE (+x).

The catch: the mode transition is reported explicitly, so "nothing changed" can't hide "it's executable now".

Backdoor path: Set +x on a committed data or config file with no content change; a content-only diff shows nothing, but a build step or cron now executes it.

git diff master..type/mode-exec

Ref names

git blocks control bytes in ref names, but allows bidi, homoglyph and zero-width characters. A branch name printed after a fetch can spoof another.

Branch name

Spoofed ref names

One branch reads as releasegpj; the other sits next to master and is a homoglyph of it. git prints both verbatim.

git fetch
 * [new branch]  release‮gpj‬
 * [new branch]  mаster

# which one is your real master?
check-ref-names-for-unicode
[WARN] ref 'release_gpj':
    -> U+202E RIGHT-TO-LEFT OVERRIDE (Cf)
[WARN] ref 'm_ster':
    -> U+0430 CYRILLIC SMALL LETTER A (Ll)

The catch: scan ref names, not just file content -- a fetched branch list is attacker-controlled text too.

Backdoor path: Push a branch whose name is a homoglyph of master; a maintainer merges the look-alike and pulls attacker code, believing they merged the real default branch.

Reproduce · the branch list itself is the attack: a bidi-reordered name and a homoglyph master. · all branches →
git for-each-ref --format='%(refname)' refs/heads

Try it yourself

Same command, any branch. Run a naive viewer and a safe one on the exact same change and watch the difference.

Clone the examples:

git clone https://github.com/output-lies/git-diffs-lie
cd git-diffs-lie

What you see (raw bytes reach your terminal):

git --no-pager diff master..content/bidi-trojan-source

What's really happening (neutralized and flagged):

git -c diff.external=git-diff-review diff master..content/bidi-trojan-source

Every branch doubles as a test fixture: manifest.tsv records what each one is and how a safe tool must handle it (neutralize, surface, or fail closed), and an automated suite checks them all.

How the safe tools catch all of this

The fix is a set of safe review tools that sit between git and your screen. Point git at one through its diff.external setting and, instead of dumping raw bytes at your terminal, it renders every change defensively. Every one of them does three things:

01

Neutralize

Dangerous bytes (overrides, invisibles, escapes, NUL) become inert placeholders. Real color codes are kept so honest diffs still render.

02

Surface

Every anomaly is named: the codepoint, the mode change, the symlink target, the submodule move, the suspicious path or ref name.

03

Fail closed

When it can't render or decide safely (undecodable bytes, a .gitattributes that can hide files) it stops instead of showing you something misleading.

The tools (git-meld, git-kdiff3, git-diff-review, git-review-difftool, git-review-mergetool) ship in Kicksecure's helper-scripts. Point git's diff.external at one and it does the rest.

FAQ

Is any of this dangerous to open?

No. Every example is safe: RFC2606 example.com hosts, obviously-fake targets like /nonexistent/DEMO-CREDENTIALS, and published Trojan-Source characters. Nothing here executes when you read or diff it: the payloads are inert demonstrations.

Who discovered these attacks?

The bidirectional-override and homoglyph class was formalized as Trojan Source by Nicholas Boucher and Ross Anderson (University of Cambridge, 2021): CVE-2021-42574 (bidi) and CVE-2021-42694 (homoglyph). The terminal-escape, symlink, submodule and .gitattributes tricks are long known to the git and security communities, and git has shipped real CVEs of its own: submodule remote code execution (RCE) (CVE-2024-32002, CVE-2018-11235), symlink-checkout RCE (CVE-2021-21300), and a carriage-return-in-path RCE (CVE-2025-48384). The .gitattributes diff-hiding trick abuses a documented feature (-diff / linguist-generated). This corpus gathers them, safely, in one place.

Are these bugs in git?

It's a mix. A few genuinely were git bugs: the crafted repositories that reach code execution on clone were real flaws in git itself, patched in security releases: CVE-2025-48384, CVE-2021-21300 and CVE-2024-32002. But most of the corpus is not a git bug at all: git faithfully applies exactly what was committed. The deception lives in the gap between how a change is rendered to a human and what git will apply, and that rendering is done by your terminal, pager, editor and repository frontend, not by git.

So whose fault is it?

There is no single culprit, which is exactly why it persists: a long-standing, ecosystem-wide problem grown over decades. Unicode, terminal emulators, editors, compilers and code-review frontends all turn untrusted bytes into something a human reads, and almost none of them flag dangerous characters by default.

The Trojan Source researchers put the fix in layers, not one tool: compilers and interpreters should warn on unterminated bidi controls and mixed-script identifiers; language specifications should forbid them; and editors and repository frontends should make invisible and look-alike characters perceptible.

Terminal emulators are a large part of it: mainstream terminals are built to interpret control sequences rather than block them, and none flag confusable characters by default. Building one that does is genuinely hard: an ASCII-only view breaks non-Latin languages and text interfaces (TUIs), so you would need opt-in allow-lists per script. Until that layered fix lands, safe-viewing tools like these are a workaround that still takes discipline, not a cure.

What are the "safe tools"?

External diff drivers from Kicksecure's helper-scripts: git-meld, git-kdiff3, git-diff-review, git-review-difftool and git-review-mergetool. Point git's diff.external at one and it neutralizes, surfaces, and fails closed for you.

Why exactly one surprise per branch?

So git diff master..<branch> isolates a single trick: easy to teach, and precise as a test fixture. master is a clean baseline; each branch adds exactly one thing.

Can I use it in my own tests?

Yes. manifest.tsv maps every branch to its expected behavior and drives a data-driven suite that asserts the review tool neutralizes, surfaces, or fails closed as documented.

Was this built with AI?

Yes: the corpus, the tests, and this page are AI-assisted.