Sitemap

GitHub artifact immutability is a lie

6 min readApr 23, 2025

--

Recently I’ve been involved in a security incident where an attacker was able to gain control over an unprivileged GitHub Actions job. Escalation from there would have required tampering an artifact — which GitHub declares to be immutable:

I decided to carry out a follow up analysis; it uncovered some vulnerabilities. As I expected, some of the issues are known by the GitHub staff, they are by design and not going to be fixed. However, customers are most likely not aware of these attack vectors, hence this advisory.

Related SLSA concepts

SLSA (Supply-chain Levels for Software Artifacts) is a security framework, a checklist of standards and controls to prevent tampering, improve integrity, and secure packages and infrastructure.

In this terminology, an artifact is: An immutable blob of data; primarily refers to software, but SLSA can be used for any artifact.

It is an important building block for integrity and build provenance. One of SLSA’s guiding principles is to “trust platforms, verify artifacts”. Another principle, probably the most important one, is that build jobs of trusted build platforms must be isolated to prevent forgery (e.g. one build job shall not be able to forge artifacts of another).

Issue #1: lack of permission control

Managing artifacts (listing/uploading/downloading/deleting) is not bound to any GITHUB_TOKEN permissions. This is not documented anywhere (at least I didn’t find anything). Users could incorrectly assume that permissions: { contents: read } or permissions: {} prevent access to this functionality, but they don't.

Consider the following workflow:

As you can see, it is currently impossible to prevent the untrusted job accessing the artifacts and could escalate to trusted_post_processing.

Currently, the internal APIs behind this functionality are completely decoupled from GITHUB_TOKEN and rely on ACTIONS_RUNTIME_TOKEN instead. This token must have the scope Actions.Results and Actions.UploadArtifacts. In other words, they authenticate the job and do not care about the permissions GITHUB_TOKEN may have or not have for the public APIs. And while this makes sense, the capability of being able to generate ID tokens is also a job specific thing and has nothing to do with the fine grained permissions of your GITHUB_TOKEN, still, it is to be granted among the permissions.

ACTIONS_RUNTIME_TOKEN is not present in the context of normal step executions among the environment variables. However, there is no security boundary that would prevent obtaining ACTIONS_RUNTIME_TOKEN.

Available options:

  • extract it from the runner memory. Is it available even when no action handlers are executed by the job (especially not the official uploader)
  • Use the one added for id token creation ACTIONS_ID_TOKEN_REQUEST_TOKEN
  • Intercepting a future step (of the same job) by patching /home/runner/work/_actions/actions/upload-artifact/v4/dist/upload/index.js

Effectively this means jobs have access to this functionality regardless it was meant to be used by the current job or not. Workflow authors have no way to disable this.

PoC: https://github.com/actions/upload-artifact?tab=readme-ov-file#overwriting-an-artifact You can repeat the same even with permissions: {}.

Issue #2: immutability

While it is highlighted multiple times that artifacts are immutable, the documentation of the official uploader also states it is possible to delete and reupload an artifact. This is not limited to artifacts produced by the same job — it is possible to tamper artifacts of other build jobs (of the same workflow) as well. Under the hood this generates a new artifact ID.

However,

  • the old artifact is no longer accessible
  • there is no trace of the reupload in the public REST APIs
  • even though it is possible to extract the artifact id generated by an upload step, the official artifact downloader does not support fetching an artifact by ID, so it is not possible to obtain the intended version (which would be not available in the case of tampering anyway)
  • the only way to detect tampering is cross checking the build logs
  • the documentation’s claims about immutability contradict with the re-upload feature

Combined with the previous concern the overall risk turns higher, as nothing prevents an attacker controlling an unprivileged job to modify artifacts of the higher privileged ones.

PoC: https://github.com/actions/upload-artifact?tab=readme-ov-file#overwriting-an-artifact

Issue #3: arbitrary artifact content

The public download API claims that “The :archive_format must be zip”, which implies that it is safe to rely on the zip format when processing the artifact. However, this is a false assumption, as there is currently nothing on GitHub’s side that would prevent uploading other archive formats or simply just arbitrary data.

Consider a post processing job running outside of GitHub Actions that relies on the popular 7z utility to extract the acrhive.

7-zip features file format detection independently from the file extension and supports a really broad range of file formats: Packing / unpacking: 7z, XZ, BZIP2, GZIP, TAR, ZIP and WIM. Unpacking only: APFS, AR, ARJ, CAB, CHM, CPIO, CramFS, DMG, EXT, FAT, GPT, HFS, IHEX, ISO, LZH, LZMA, MBR, MSI, NSIS, NTFS, QCOW2, RAR, RPM, SquashFS, UDF, UEFI, VDI, VHD, VHDX, VMDK, XAR and Z

Overall, the lack of limitation expands the attack surface in such use-cases significantly.

PoC: https://github.com/irsl/artifacts-repro/blob/main/.github/workflows/archive_confusion.yaml

Issue #4: mutate artifacts without re-creating them

According to the official GitHub Actions artifact uploader, the artifacts cannot be mutated without creating a new ID for them: “Although it’s not possible to mutate an Artifact, can completely overwrite one. But do note that this will give the Artifact a new ID, the previous one will no longer exist

A compromised workload could save the signed_upload_url returned by the github.actions.results.api.v1.ArtifactService/CreateArtifactinternal API. This was a signed Azure Blob Storage URL with 12h validity window. By sending additional PUT requests to the same upload url multiple times, it is possible to mutate artifacts (either before or after they have been finalized with the FinalizeArtifact API). Since this happens independently from the GitHub control plane, the artifact ID remains the same.

Furthermore, this can be done even after the build job has completed. This is not possible normally, as ACTIONS_RUNTIME_TOKEN is not accepted after their corresponding job has completed: {"code":"permission_denied","msg":"job is completed"}

PoC: https://github.com/irsl/artifacts-repro/blob/main/.github/workflows/immutability.yaml

As a response to this, GitHub decreased the validity window of the signed URLs to 1h. GitHub also added a new security measure in the download-artifact Actions codebase to counter this attack vector: they started verifying the hash of the artifacts during download. (The uploader has already been calculating and submitting hashes, it was just ignored by downloaders.

Workloads using another library to retrieve artifacts are still highly recommended to perform the same step.

Issue #5: path traversal

I found a variant of CVE-2024–42471, a path traversal flaw in https://github.com/actions/download-artifact.

While extracting zip files is hopefully secure by now, artifact names were not validated correctly (at all).

The handler supports fetching/extracting multiple artifacts in a single shot, and it extracts them to a destination like this:

<base-directory>/<artifact-name>

The corresponding code can be found here: https://github.com/actions/download-artifact/blob/7fba95161a0924506ed1ae69cdbae8371ee00b3f/src/download-artifact.ts#L120

As you can see, it is a simple path join, no validation of any kind is present.

The destination subdirectories may already exist. The destination files may already exist, in that case the content is overwritten, POSIX file permissions (of the already existing file) are preserved. Newly created files are created as 0644. This made this attack vector ideal for overwriting existing binaries/executables.

The following workflow was vulnerable:

PoC: https://github.com/irsl/artifacts-repro/blob/main/.github/workflows/pathtraversal_via_artifact_names.yaml

GitHub implemented a server side security check to reject artifact names that contain the ../ substring.

Despite to the original, GitHub did not release an official advisory about this flaw. I think it would deserve more attention. If you agree, please ping them.

Conclusions

Given artifacts is the endorsed mechanism of sharing data between jobs securely, it is an important low-level building block and the bar of its security controls must be set high.

The design issues aren’t going to be remediated — at least not in the near future : “This is an intentional design decision and is working as expected. We may make this functionality more strict in the future, but don’t have anything to announce right now.

Until then, do not rely on artifacts without careful consideration — remember that untrusted jobs of your workflow could modify the others produced by the higher privileged ones. In other words: do not use artifacts of trusted build jobs when untrusted build jobs are present in the same workflow.

--

--

Imre Rad
Imre Rad

Written by Imre Rad

Software developer daytime, security researcher in freetime

No responses yet