{
"$type": "site.standard.document",
"bskyPostRef": {
"cid": "bafyreigc4den7nkk7sh2gd2fjnsuwviseu4yjl5iay6snub3nyqihhumru",
"uri": "at://did:plc:anldby4lwneunjl777bq6ih7/app.bsky.feed.post/3mnro2s22gy22"
},
"coverImage": {
"$type": "blob",
"ref": {
"$link": "bafkreicg3izgvugqeqaeikwma57vga2ihaoaojdsmrsnet5kbt5b6uuc7q"
},
"mimeType": "image/png",
"size": 581109
},
"description": "[In Depth #40] We trust version numbers to mean a specific, fixed release - but they're really just labels pointing at a commit, and an attacker can quietly move them. Let's dig into tag hijacking, the attack behind tj-actions and Laravel-Lang. š",
"path": "/in-depth-version-numbers-are-vanity-labels/",
"publishedAt": "2026-06-08T12:17:54.000Z",
"site": "https://securinglaravel.com",
"tags": [
"last week's Security Tip",
"Laravel Moat",
"tj-actions/changed-files",
"was compromised",
"CVE-2025-30066",
"GitHub issue",
"recent GitHub data leak",
"full account",
"Snyk",
"Aikido",
"Subscribe now"
],
"textContent": "Following on from last week's Security Tip about using Laravel Moat to protect our repositories, I want to dive into the mechanics of how _supply chain attacks_ work - specifically tag hijacking. There is a surprising amount to cover, and it reveals just how fragile the user-friendly scaffolding we've built really is, and thus why we're seeing so many successful attacks at the moment.\n\nSomething I say a lot when talking about password security is that **humans are forgetful and lazy** , so we'll reach for simple passwords that are easy to remember. Funnily enough, this logic applies to dependencies too - we use friendly version numbers everywhere (`v1.2.3`), instead of commit SHA hashes (`06e1e8ef749ba9f9daa8cf561c2f94ab696e1b3d`), because version numbers are **so much easier to use**.\n\nBut here's the catch: **version numbers aren't immutable!**\n\nVersion numbers are a tag that has been applied to a specific commit, however this tag is simply a vanity label. It can be removed from one commit and assigned to another. Which means an attacker who can manipulate tags on a repository can swap out the legitimate version for a malicious one - and any tool pulling in that tag won't notice the difference _(under the right conditions)_.\n\nThis loophole makes hijacking packages and injecting malicious versions powerful, although let's not forget that incrementing a version number for malicious versions is still highly effective - all you need is access to the repository. We're not just dealing with local package managers either, but also tools like CI (Continuous Integration) systems, such as GitHub Actions, which also rely on version numbers through tags.\n\nUltimately, any system that relies on `v2` as the indicator for a safe version is at risk if `v2` can be redirected or `v2.1` can be added.\n\nLet's dig into it!\n\n## What Happened With `tj-actions/changed-files` and `Laravel-Lang`?\n\nBefore we look at the mechanics and how all the pieces fit together, let's take a look at two recent examples of this attack in action.\n\n### `tj-actions/changed-files`\n\nThe tj-actions/changed-files GitHub Action was compromised through a stolen Personal Access Token (PAT) for a maintenance bot, which had access to manage the repository. Because GitHub forks share the same internal git object storage, a commit pushed to a fork can be loaded by hash from the parent repository, which allowed the attacker to push a malicious commit to a fork and update the tags on the repository to point to this malicious commit.\n\nMalicious commit hash set on every tag.\n\nWith all tags pointing to the malicious commit, every CI workflow that referenced an updated tag loaded the malicious code. This code caused CI secrets/keys to be dumped into the build logs - which are public - allowing the attacker (and anyone else) to harvest these secrets and keys.\n\nThis was all made possible because CI workflows were referring to the Action based on the version, and the system had no way to verify if it was getting the actual version:\n\n\n - name: Get changed files\n id: changed-files\n uses: tj-actions/changed-files@v44\n\nThe `tj-actions/changed-files` action was used by over 23,000 repositories at the time, and the attack was assigned CVE-2025-30066.\n\nš”\n\n __The__ GitHub issue__is worth a read if you want a raw chronological narrative of the attack being discovered and investigated by various folks, looking at possible attack vectors, mitigations, etc.__\n\n### `Laravel-Lang`\n\nSimilar to the previous attack, a Personal Access Token belonging to one of the `Laravel-Lang/*` team members was obtained by the attacker from a recent GitHub data leak, who then used it to update release tags on the targeted packages, pointing them towards malicious commits they had pushed into the repositories.\n\nOnce the tags were in place, any new `composer install` or `composer update` pulled in the malicious commits, introducing malware into the victim's environment. This malware was hidden inside `src/helpers.php`, and autoloaded through `composer.json:autoload.files` - which loads the malicious code every time Composer's autoloader is initiated.\n\nScarily, as the package maintainers tried to stop the attack and clean up tags, the attackers continued to recreate them - in the maintainer's own words, _they were being restored \"right before our eyes.\"_ See their full account for the details.\n\nIt's worth pointing out that this attack would not have worked when an existing `composer.lock` was present alongside `composer install`, as that uses the package commit hash to lock down versions. However, running `composer install` without `composer.lock` or `composer update` would have pulled in the malicious version.\n\nAs reported by Snyk, 700 versions were compromised across four packages (`laravel-lang/lang`, `laravel-lang/http-statuses`, `laravel-lang/attributes`, and `laravel-lang/actions`), and an earlier report by Aikido caught ~233 before the attack continued.\n\n## How Git Tags Actually Work\n\nSo how does a friendly little `v2.1` end up handing over your AWS keys, and why is it just a vanity label?\n\n### This post is for subscribers only\n\nBecome a member to get access to all content\n\nSubscribe now",
"title": "In Depth: Version Numbers Are Vanity Labels",
"updatedAt": "2026-06-08T12:20:53.158Z"
}