{
	"id": "40c764c0-4330-4798-b93e-92288127e9e3",
	"created_at": "2026-04-06T00:11:16.721124Z",
	"updated_at": "2026-04-10T13:12:32.271922Z",
	"deleted_at": null,
	"sha1_hash": "4d6dc938bdd1006110f7e7eca5785097e513fa77",
	"title": "The GitHub Actions Worm: Compromising GitHub Repositories Through the Actions Dependency Tree",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 5070405,
	"plain_text": "The GitHub Actions Worm: Compromising GitHub Repositories\r\nThrough the Actions Dependency Tree\r\nBy By Asi Greenholts\r\nPublished: 2023-09-14 · Archived: 2026-04-02 12:02:10 UTC\r\nLearn how a novel attack vector in GitHub Actions allows attackers to distribute malware across repositories\r\nusing a technique that exploits the actions dependency tree and puts countless open-source projects and internal\r\nrepositories at risk. Get an in-depth look at the attack vectors, technical details and a real-world demo in this blog\r\npost highlighting our latest research.\r\nAs the premier platform for hosting open-source projects, GitHub’s popularity has boosted the popularity of its\r\nCI/CD platform — GitHub Actions. This popularity, however, extends beyond the DevOps community to attract\r\nhackers eager to exploit the platform’s expanding attack surface.\r\nInitial Attack Vectors\r\nIn recent supply chain attacks, we see attackers repeatedly executing the same scheme, ultimately compromising a\r\nrepository or a software library and infecting it with a malicious payload targeting its direct dependents. The\r\npayload tries to steal secrets or create a reverse shell, whether running in pipelines or production environments.\r\nTo achieve control or write permissions on a repository, attackers draw from a range of established techniques:\r\n1. Repojacking\r\nhttps://www.paloaltonetworks.com/blog/cloud-security/github-actions-worm-dependencies/\r\nPage 1 of 15\n\nWhen a repository moves to a new owner (organization or user), or the owner changes its name, GitHub\r\nautomatically redirects requests sent to the old repository name to the new owner and repository. But if the owner\r\nis deleted, an attacker can register the previous name on GitHub and create a repository containing malicious\r\ncode. Use of the previous repository name disables GitHub’s automatic redirection, and consumers consuming this\r\nproject unknowingly consume the malicious repository.\r\nTo protect against repojacking, GitHub employs a security mechanism that disallows the registration of previous\r\nrepository names with 100 clones in the week before renaming or deleting the owner's account. The 100-clone\r\nsecurity measure, though, often proves inadequate for repositories hosting actions. When a GitHub Actions\r\nworkflow uses an action, it downloads a zip of the repository via the GitHub API, bypassing the clone count. In\r\nother words, the downloaded zip file doesn’t contribute to the repository's tally of clones.\r\n2. NPM Package Maintainer Email Hijacking\r\nActions written in JavaScript usually involve dependencies maintained by developers who typically use email\r\naddresses to sign into NPM. Should attackers acquire a maintainer's email domain, and the maintainer's NPM\r\naccount lack two-factor authentication (2FA), the attackers could reset the password. This would allow the\r\nattackers to create a malicious package version that, when used by the action, will enable the attackers to execute\r\nmalicious code within GitHub workflows as the action runs.\r\n3. GitHub Actions Command Injection\r\nWorkflows receive context about the triggering event, including information like issue title, pull request title, and\r\ncommit message. Since attackers can control some of these fields, developers should treat them as untrusted input.\r\nWorkflows that use the untrusted input in bash commands — when using bash’s command substitution, for\r\nexample — can be vulnerable to command injection.\r\nOther attack methods used to compromise repositories include:\r\n1. Dependency confusion\r\n2. Public-PPE\r\n3. Compromise a maintainer’s access token or credentials\r\n4. Create a pull request with hidden malicious code in hopes that project maintainers will merge it\r\nRegardless of the initial attack vector, once attackers gain a foothold in an action's repository, they set out to create\r\na worm. The worm will allow the attackers to directly infect with malware any GitHub Actions workflow\r\nconsuming this action. But how can the attackers extend their reach and infect more repositories?\r\nHow Actions Depend on Actions\r\nAfter creating the worm, the next step involves finding a path for it to spread. For a GitHub Actions worm, the\r\npath travels from action to action, which could involve any of the three types of actions — JavaScript, Docker, or\r\nComposite. Additionally, actions can depend on actions in one of two ways. The first way uses composite actions,\r\nwhich combine multiple workflow steps within one action. All action types, though, use an action.yml file to\r\ndefine the action's inputs, outputs and main entrypoint.\r\nhttps://www.paloaltonetworks.com/blog/cloud-security/github-actions-worm-dependencies/\r\nPage 2 of 15\n\nFigure 1: Action.yml file, the metadata file of actions\r\nIn figure 1, a sample action.yml file instructs a composite action to run actions/checkout, a dependency of the\r\ncomposite action.\r\nThe second way actions can depend on other actions is through the action’s CI/CD pipeline. It’s common to see\r\nactions that use GitHub Actions workflows to build and test their code.\r\nhttps://www.paloaltonetworks.com/blog/cloud-security/github-actions-worm-dependencies/\r\nPage 3 of 15\n\nFigure 2: An example workflow file used as a CI of an action\r\nWe can see that the workflow file uses the trilom/file-changes-action action during its run, which makes it an\r\nimplicit dependency of the action.\r\nNote that this dependency action isn’t used as part of the action but only in the action’s workflow, a component\r\nprocess in the proper flow of the CI process.\r\nThese two ways for actions to depend on other actions form a tree of dependencies that interconnect the actions in\r\nthe GitHub Marketplace.\r\nWe can now use this knowledge to parse action.yml files that define actions and CI workflows of actions. In this,\r\nwe can identify actions dependant on other actions and create the GitHub Actions dependency tree over a Neo4j\r\ngraph:\r\nhttps://www.paloaltonetworks.com/blog/cloud-security/github-actions-worm-dependencies/\r\nPage 4 of 15\n\nFigure 3: GitHub Actions dependency tree over a Neo4j graph referred to as “The Flower”\r\nA purple node represents a repository. An orange node represents an instance of an action used in another action’s\r\nworkflow or an action directly referenced in an action configuration file (action.yml).\r\nIn figure 3, we see several repos (purple) with a workflow. Each workflow uses an action (orange), and the action\r\nis hosted in another repo (purple). The action stored in this repository might have its own workflow, which uses\r\nanother action (orange), and so on.\r\nHow Compromised Actions Infect Dependency Actions\r\nWe demonstrated how actions can be dependent on other actions. Let’s now explore how attackers can abuse these\r\ndependencies to spread their worm.\r\nDumping Secrets from a Runner’s Memory\r\nTo secure secrets in GitHub Actions, you can use its encrypted secrets feature, which allows you to define secrets\r\nin the organization, repository or environment settings.\r\nhttps://www.paloaltonetworks.com/blog/cloud-security/github-actions-worm-dependencies/\r\nPage 5 of 15\n\nWhen a job starts, the GitHub Actions runner receives all the secrets used in the job. Because the runner receives\r\nthe secrets when the job starts, we can dump the runner’s memory to reveal all secrets defined in the job even\r\nbefore they’re used. This means that no matter when we achieve code execution by compromising an action used\r\nin the job, we can read from memory all secrets referenced in the job.\r\nAlso noteworthy, jobs can use a secret called GITHUB_TOKEN — uniquely generated for each workflow run —\r\nto allow jobs to authenticate and use GitHub’s API against the repository. Is the GITHUB_TOKEN as accessible\r\nas other secrets? We’ll soon find out.\r\nFigure 4: Workflow file that dumps the memory of the runner\r\nThe workflow seen in figure 4 contains two jobs. Each job runs on a different runner and uses a different secret. In\r\nthe second job, we see that the second step dumps the runner’s memory to retrieve its secrets.\r\nFigure 5: Decoded memory dump\r\nIn the decoded base64 that the second job prints, you’ll notice two interesting details:\r\n1. We don’t see the FIRST_SECRET secret because it’s used in a different job and runs on a different\r\nmachine. Note the possibility on self-hosted runners for two jobs to run on the same runner if the runner\r\nisn’t ephemeral.\r\n2. We see the GITHUB_TOKEN secret, although we didn’t reference it in the workflow file. This token, in\r\nother words, can be accessed from any job in the workflow, even if the workflow doesn’t reference it.\r\nIncidentally, we often see a GitHub personal access token (PAT) stored as a secret and used by steps in the\r\nworkflow to perform tasks against the repository.\r\nOverriding Action’s Code\r\nhttps://www.paloaltonetworks.com/blog/cloud-security/github-actions-worm-dependencies/\r\nPage 6 of 15\n\nTo understand how we can infect an action’s repository, we need to understand how actions are used.\r\nThe common format for calling an action follows {owner}/{repo}@{ref}. The “ref” key has three forms:\r\n1. Reference a commit hash.\r\nFigure 6: Calling an action using a commit hash\r\n2. Reference a branch.\r\nFigure 7: Calling an action using branch name\r\n3. Reference a tag.\r\nFigure 8: Calling an action using a tag\r\nWe can use the secrets exfiltrated in the flow to infect the repository with malicious code. Overwriting a commit\r\nwhile keeping its hash the same isn’t possible, so we can’t abuse a commit hash reference. We still have two\r\noptions:\r\n1. Infecting by pushing code to a branch: We can use the GITHUB_TOKEN or PAT used in a job to push\r\nmalicious code to a branch if the token is granted with the contents:write permission.\r\n2. Infecting by creating a tag: We can use the GITHUB_TOKEN or PAT used in a job to create a malicious\r\ntag or override an existing tag to infect dependent repositories referencing the action by a tag.\r\nSuccessfully pushing code also depends on the GITHUB_TOKEN permissions, branch protection rules and\r\nprotected tags configured in the repository.\r\nYou can calculate the GITHUB_TOKEN permissions of a workflow, even before running it. Until recently, all\r\nGitHub Actions workflows had default read and write permissions against their repositories. GitHub changed the\r\nhttps://www.paloaltonetworks.com/blog/cloud-security/github-actions-worm-dependencies/\r\nPage 7 of 15\n\ndefault configuration in February 2023 to read permissions on the contents, packages and metadata scopes. This\r\nmeans that most repositories now have default write permissions.\r\nBesides permissions granted in the repository setting of the GITHUB_TOKEN, permissions can be overwritten by\r\nconfiguring them inside the workflow file.\r\nFigure 9: Workflow file limiting the contents permission of the GITHUB_TOKEN\r\nWhile a real worm doesn’t need to know the permissions — it simply tries to infect any repository it encounters\r\n— we only created a static analysis as part of our research and wanted to discover the permissions granted to the\r\nGITHUB_TOKEN in each workflow. To do that, we examined the workflow run log.\r\nhttps://www.paloaltonetworks.com/blog/cloud-security/github-actions-worm-dependencies/\r\nPage 8 of 15\n\nFigure 10: Sample workflow run\r\nWe can see the write permission on the contents scope — and many other granted permissions — and use them to\r\npush code to the repository.\r\nSounds Like a Worm, Right?\r\nLet’s pause and look at what we have so far.\r\nhttps://www.paloaltonetworks.com/blog/cloud-security/github-actions-worm-dependencies/\r\nPage 9 of 15\n\n1. We know how we can find our initial attack vector to compromise the first action’s repository.\r\n2. We also know how we can leverage the compromised action’s repository to infect a dependent repository's\r\ncode — by pushing malware to one of its branches or overwriting an existing tag.\r\nImagine what would happen if we did this recursively. We have the potential to create a GitHub Actions worm\r\nacross the Actions dependency tree.\r\nFigure 10: Flow of a GitHub Actions worm\r\nFinding Attack Graphs at Scale\r\nNow that we have all the pieces of the puzzle, we can start scanning targets for dependencies vulnerable to\r\nexploitation.\r\nFirst, we gathered two sets of targets:\r\n4,700 repositories that use GitHub Actions out of GitHub’s top 10,000 repositories by star count.\r\n7,200 repositories that use GitHub Actions out of 32,000 repositories of companies that have a bug bounty\r\nprogram.\r\nTo create a Neo4j graph, we then automated a process that accomplished the following for each target:\r\n1. Clone the repository and create a node for it.\r\n2. Check if any of the potential initial attack vectors apply to this repository.\r\n3. Parse the actions’ workflows and action.yml files to find all used actions and enrich nodes with metadata,\r\nsuch as secret names accessible to the action, version in use and the permission of the GITHUB_TOKEN\r\non the repository contents scope.\r\n4. Recursively undertake the above steps for each discovered action.\r\nFigure 11 depicts an attack graph of repositories we can’t disclose. In this graph, you can see two viable initial\r\nattack vectors to execute on two repositories. These two repositories are actually the same repository but moved\r\nhttps://www.paloaltonetworks.com/blog/cloud-security/github-actions-worm-dependencies/\r\nPage 10 of 15\n\nfrom one org to another. By attacking the initial repository, we can directly infect 18 actions that depend on it.\r\nFrom infecting these 18 actions, we can infect 72 of the target repositories.\r\nFigure 11: Purple/repository, orange/action usage, red/initial attack vector\r\nThe scale of this infection chain is larger than presented in the graph. We’ll explain why through a practical, real-world example.\r\nPublic Disclosure\r\nWe reported the issues we found to all vulnerable projects we could contact. Hangfire and Veracode allowed us to\r\npublicly disclose their cases.\r\nIn the figure 12 attack graph, you can see the HangfireIO/Hangfire (8.4k ⭐) public GitHub repository at the\r\nbottom-left side. This repository used two actions in one of its workflow files: veracode/veracode-pipeline-scan-results-to-sarif and papeloto/action-zip. The veracode action also used papeloto/action-zip in its workflow file.\r\nhttps://www.paloaltonetworks.com/blog/cloud-security/github-actions-worm-dependencies/\r\nPage 11 of 15\n\nFigure 12: Attack graph\r\nThe papeloto/action-zip action was moved from its original repository to the vimtor organization, and the\r\npapeloto organization was available for registration, making the action-zip repository vulnerable to repojacking.\r\nOur team registered the organization to prevent malicious actors from exploiting this vulnerability.\r\nFigure 13: Ownership of the papeloto organization and the ability to create the action-zip repository\r\nBy repojacking this repository, we successfully attacked the HangfireIO/Hangfire repository directly. Infecting the\r\nveracode/veracode-pipeline-scan-results-to-sarif repository achieved the same ends.\r\nThe potential scale of this vulnerability is massive. We can attack the veracode repository’s 1,600 dependents,\r\nrepresented in figure 14 by orange circles. The action-zip action has about 600 dependents that will execute our\r\nmalicious code. But that’s not all.\r\nThe Hangfire repository deploys a NuGet package that has 9,400 daily downloads we can attack. Additionally, the\r\ndependents have dependents, and the NuGet package consumers likely have their dependents, so the infection\r\nchain continues ad infinitum. That’s still not all, though.\r\nWe’re talking only about the public repositories we analyzed in the GitHub public ecosystem. A real worm would\r\nrun on a vast number of private repositories — and impose an immediate impact. If the worm encounters a private\r\nhttps://www.paloaltonetworks.com/blog/cloud-security/github-actions-worm-dependencies/\r\nPage 12 of 15\n\nrepository granting minimal read-only permissions to the GITHUB_TOKEN, it could steal source code. If the\r\nrepository’s code can be modified, the attack escalates. Disastrous best describes the scope of this attack scenario.\r\nNow take all of that and imagine multiple possible attack graphs like the one in figure 14.\r\nFigure 14: Impact of a worm spread\r\nVeracode and Hangfire acted on the findings we reported:\r\nVeracode fixed the issue by replacing the vulnerable action with custom bash commands.\r\nHangfire decided to completely delete all workflow files.\r\nThe Worm Demo\r\nWe created a closed demo environment to show how a GitHub Actions worm takes advantage of the methods used\r\nto spread malware to any infectable repository across the GitHub Actions dependency tree.\r\nFigure 15: Illustration of the demo attack flow\r\nOur demo includes four repositories:\r\nA repository named the-repo is our primary target, and we want to steal its secrets. The repo has a\r\nworkflow that uses the random-action action with the v1 tag reference.\r\nhttps://www.paloaltonetworks.com/blog/cloud-security/github-actions-worm-dependencies/\r\nPage 13 of 15\n\nrandom-action has a workflow that uses the rev-date-action action using the main branch reference.\r\nrev-date-action depends on the rev-action action by using it directly through its action.yml file.\r\nrev-action is the weakest link of the chain. The attacker had previously compromised the repository to gain write\r\naccess using an initial attack vector.\r\nVideo: The infection action chain demonstrated\r\nAs seen in the video showing the infection chain, the attacker infects the rev-action action by committing code to\r\nits action.yml file, which downloads and executes the worm.\r\n1. rev-date-action is a composite action that directly depends on rev-action, configured through its action.yml\r\nfile. It’s immediately infected via the rev-action action without the worm performing any action.\r\n2. Next, we see the execution of the CI workflow of the random-action action. This workflow uses the rev-date-action action, and it also grants the GITHUB_TOKEN write permissions on the contents scope against\r\nthe repository. When rev-date-action runs, it uses the GITHUB_TOKEN and tries to infect the random-action repository by pushing code to the main branch. This attempt fails, because the repository has a\r\nbranch protection rule that protects the main branch. Next, the worm tries to overwrite the latest created\r\ntag. This attempt succeeds, and it successfully overwrites the tag with the malicious code that installs the\r\nworm.\r\n3. the-repo, our target repository, contains the VERY_SECRET secret. When its CI workflow runs, it uses the\r\ninfected v1 tag of the random-action action, which makes the workflow leak its credentials, including the\r\nVERY_SECRET credential.\r\nKnow Your Pipeline Dependencies\r\nIn software development, the concept of dependencies is well-understood. When you build a software application,\r\nyou rely on a variety of software components — libraries, frameworks, tools, etc. Development teams can track\r\nand manage these dependencies using a software bill of materials (SBOM).\r\nThe same concept applies to pipelines. A pipeline is a set of steps used to automate a task, such as building, testing\r\nand deploying software. Pipelines can also have dependencies in the form of other pipelines, tools and services.\r\nAs in software development, the dependencies of pipelines can pose security risks. If one of the dependencies up\r\nthe dependency chain is compromised, it could affect the entire pipeline. This is why teams need to track and\r\nmanage the dependencies of pipelines as carefully as they would track and manage the dependencies of software\r\napplications.\r\nHow to Protect Your Workflows and Assets\r\nMultiple security controls can prevent or raise the difficulty of successfully attacking repositories using the worm.\r\nIn order of effectiveness, controls include:\r\n1. Set GITHUB_TOKEN and PAT contents permission to the minimum required — with special attention to\r\nreducing write permissions against the repository — to prevent infection by the worm. Consider using\r\nGitHub’s actions-permissions project to reduce workflow permissions. In general, implement strict PBAC\r\nhttps://www.paloaltonetworks.com/blog/cloud-security/github-actions-worm-dependencies/\r\nPage 14 of 15\n\n(Pipeline-Based Access Controls) to make sure the workflow is granted with the least privileges and access\r\nit needs to fulfill its purpose.\r\n2. Configure branch and tag protection to further prevent infection and protect the codebase.\r\n3. Monitor and limit outbound network connections from workflow runners to prevent the download of\r\nmalicious code into pipelines and prevent malware from reporting to C2 servers.\r\n4. Pin actions using a commit hash to reduce the risk of using a maliciously modified action.\r\nLearn More\r\nThe CI/CD attack surface has changed considerably in recent years, making it challenging to know where to get\r\nstarted with CI/CD security. If you’re looking for practical help, check out the Top 10 CI/CD Security Risks: The\r\nTechnical Guide.\r\nAnd if you’re now thinking about Prisma Cloud, take it for a free 30-day test drive and discover the advantage.\r\nSource: https://www.paloaltonetworks.com/blog/cloud-security/github-actions-worm-dependencies/\r\nhttps://www.paloaltonetworks.com/blog/cloud-security/github-actions-worm-dependencies/\r\nPage 15 of 15",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"MITRE"
	],
	"origins": [
		"web"
	],
	"references": [
		"https://www.paloaltonetworks.com/blog/cloud-security/github-actions-worm-dependencies/"
	],
	"report_names": [
		"github-actions-worm-dependencies"
	],
	"threat_actors": [
		{
			"id": "dfee8b2e-d6b9-4143-a0d9-ca39396dd3bf",
			"created_at": "2022-10-25T16:07:24.467088Z",
			"updated_at": "2026-04-10T02:00:05.000485Z",
			"deleted_at": null,
			"main_name": "Circles",
			"aliases": [],
			"source_name": "ETDA:Circles",
			"tools": [],
			"source_id": "ETDA",
			"reports": null
		}
	],
	"ts_created_at": 1775434276,
	"ts_updated_at": 1775826752,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/4d6dc938bdd1006110f7e7eca5785097e513fa77.pdf",
		"text": "https://archive.orkl.eu/4d6dc938bdd1006110f7e7eca5785097e513fa77.txt",
		"img": "https://archive.orkl.eu/4d6dc938bdd1006110f7e7eca5785097e513fa77.jpg"
	}
}