Evil Merge: Malicious Code Hidden in Git Merge Commits
Malicious, obfuscated code was discovered in a project's vite.config.js file. It wasn't introduced in a pull request (PR), but directly within a merge commit during conflict resolution. GitHub's PR view only shows the diff from the feature branch, ignoring changes made in the merge commit itself. Analysis using git log revealed a merge commit with identical parent commits but altered file content.
MD5 hashes of the parent commits matched:
- Parent 1:
aa82acb0c335430d8300b6cb306dc824(clean) - Parent 2:
aa82acb0c335430d8300b6cb306dc824(clean) - Merge:
2a54754defae4d13aab39f256738dbbf(modified)
In a three-way merge, Git should produce an unchanged result when the parent files are identical. Any differences indicate manual editing during the merge process.
Injection and Obfuscation Mechanism
The malicious payload was injected into two files across different modules. The contributor added the payload when clicking the merge button in the PR. The code executed during npm run dev and npm run build for 3.5 months.
Obfuscation techniques included:
- Reconstructing strings like
require,module,constructorby scrambling them with a numeric seed. - Using
Functioninstead ofevalto bypass static analysis. - A custom decoder using a character substitution table.
The payload contained no URLs or IP addresses. Instead, it:
- Queried the latest transaction of a TRON wallet.
- Extracted a BSC transaction hash.
- Made a JSON-RPC
eth_getTransactionByHashcall to a BSC node. - XOR-decoded the code from the transaction's
inputfield.
Aptos was used as a fallback if TRON was unavailable. Execution used child_process.spawn with detached: true and stdio: 'ignore', and a 30-second timer to counter watch-mode detection.
Reasons It Went Unnoticed
- GitHub does not display diffs for merge commits in PRs.
git logdoesn't reveal changes; you needgit diff <parent1>..<merge>.- The code was shifted beyond the 200th column using whitespace.
- SAST tools failed due to lack of signatures: no
eval, URLs, or base64.
This technique is known as an evil merge — a merge that introduces changes not present in either parent (gitglossary(7)). It was described by a Git maintainer in 2013, but few detection tools exist.
Evil Merge Detector: Automated Scanner
A detector written in Go compares the expected three-way merge result (via a common ancestor) with the actual merge commit. Discrepancies signal manual edits.
Usage:
evilmerge scan /path/to/repo
Options:
--format=sariffor GitHub Code Scanning.- Severity levels: manual conflict resolutions vs. edits without conflicts.
Integrations:
- GitHub Action:
```yaml
- uses: fimskiy/Evil-merge-detector@v1
with:
fail-on: warning
```
- GitHub App: PR checks and retrospective history scans.
- Templates for GitLab, Bitbucket, and self-hosted setups (pre-receive hooks).
Mitigations and Workflow Vulnerabilities
Attacks via contributor access: legitimate commits followed by injection during a merge. The PR itself remains clean.
Recommendations:
- Use a linear history (squash/rebase).
- Implement pre-receive hooks on the server.
- On GitHub: enable "Dismiss stale reviews" to require re-review if the PR branch changes.
GitHub confirmed this is intentional design, with possible future restrictions. Current measures focus on prevention, not detection.
Key Takeaways
- Evil merge is the official term for manual edits in merge commits.
- The payload uses blockchain (TRON/BSC/Aptos) for command-and-control without URLs/IPs.
- The Go-based detector scans history and integrates into CI/CD.
- 3.5 months of undetected activity in a project with CI and code review.
- GitHub has no immediate plans to change the merge workflow.
— Editorial Team
No comments yet.