Arbitrary File Write via artifact extraction in actions/artifact
Description
CVE-2024-42471: Arbitrary file write via path traversal in GitHub Actions Toolkit's artifact package before 2.1.2.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
CVE-2024-42471: Arbitrary file write via path traversal in GitHub Actions Toolkit's artifact package before 2.1.2.
Vulnerability
Description CVE-2024-42471 is a directory traversal vulnerability in the actions/artifact package, part of the GitHub Actions Toolkit. Versions before 2.1.2 on the 2.x branch are vulnerable to arbitrary file write when extracting a crafted artifact using downloadArtifactInternal, downloadArtifactPublic, or streamExtractExternal. The root cause is insufficient validation of filenames during extraction, allowing an attacker to include path traversal sequences (e.g., ../../) in filenames [1][2].
Exploitation
To exploit this, an attacker must provide a malicious artifact (e.g., a zip file) containing files with path traversal filenames. Any action that uses vulnerable @actions/artifact versions to download and extract artifacts from an untrusted source is at risk. No authentication beyond standard workflow permissions is required, but the attacker needs to control an artifact uploaded to a workflow run [2]. This is a classic Zip Slip vulnerability [3].
Impact
Successful exploitation allows an attacker to overwrite arbitrary files on the runner's filesystem, potentially leading to remote code execution or persistence. The attacker could overwrite executable files, configuration files, or other sensitive resources, depending on the permissions of the running action [3].
Mitigation
Users should upgrade to @actions/artifact version 2.1.2 or higher, which fixes the vulnerability by replacing the unzipper library with unzip-stream, which validates paths [1][2]. No workarounds are available.
AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
@actions/artifactnpm | >= 2.0.0, < 2.1.2 | 2.1.2 |
Affected products
2Patches
129885a805ef3Merge pull request #1724 from actions/bethanyj28/update-unzip-stream
5 files changed · +28 −64
packages/artifact/package.json+1 −1 modified@@ -1,6 +1,6 @@ { "name": "@actions/artifact", - "version": "2.1.6", + "version": "2.1.7", "preview": true, "description": "Actions artifact lib", "keywords": [
packages/artifact/package-lock.json+5 −5 modified@@ -1,12 +1,12 @@ { "name": "@actions/artifact", - "version": "2.1.5", + "version": "2.1.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@actions/artifact", - "version": "2.1.5", + "version": "2.1.7", "license": "MIT", "dependencies": { "@actions/core": "^1.10.0", @@ -1738,9 +1738,9 @@ "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==" }, "node_modules/unzip-stream": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/unzip-stream/-/unzip-stream-0.3.1.tgz", - "integrity": "sha512-RzaGXLNt+CW+T41h1zl6pGz3EaeVhYlK+rdAap+7DxW5kqsqePO8kRtWPaCiVqdhZc86EctSPVYNix30YOMzmw==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/unzip-stream/-/unzip-stream-0.3.4.tgz", + "integrity": "sha512-PyofABPVv+d7fL7GOpusx7eRT9YETY2X04PhwbSipdj6bMxVCFJrr+nm0Mxqbf9hUiTin/UsnuFWBXlDZFy0Cw==", "dependencies": { "binary": "^0.3.0", "mkdirp": "^0.5.1"
packages/artifact/RELEASES.md+4 −0 modified@@ -1,5 +1,9 @@ # @actions/artifact Releases +### 2.1.7 + +- Update unzip-stream dependency and reverted to using `unzip.Extract()` + ### 2.1.6 - Will retry on invalid request responses.
packages/artifact/src/internal/download/download-artifact.ts+2 −50 modified@@ -1,7 +1,4 @@ import fs from 'fs/promises' -import * as stream from 'stream' -import {createWriteStream} from 'fs' -import * as path from 'path' import * as github from '@actions/github' import * as core from '@actions/core' import * as httpClient from '@actions/http-client' @@ -47,11 +44,6 @@ async function streamExtract(url: string, directory: string): Promise<void> { await streamExtractExternal(url, directory) return } catch (error) { - if (error.message.includes('Malformed extraction path')) { - throw new Error( - `Artifact download failed with unretryable error: ${error.message}` - ) - } retryCount++ core.debug( `Failed to download artifact after ${retryCount} retries due to ${error.message}. Retrying in 5 seconds...` @@ -86,8 +78,6 @@ export async function streamExtractExternal( } const timer = setTimeout(timerFn, timeout) - const createdDirectories = new Set<string>() - createdDirectories.add(directory) response.message .on('data', () => { timer.refresh() @@ -99,46 +89,8 @@ export async function streamExtractExternal( clearTimeout(timer) reject(error) }) - .pipe(unzip.Parse()) - .pipe( - new stream.Transform({ - objectMode: true, - transform: async (entry, _, callback) => { - const fullPath = path.normalize(path.join(directory, entry.path)) - if (!directory.endsWith(path.sep)) { - directory += path.sep - } - if (!fullPath.startsWith(directory)) { - reject(new Error(`Malformed extraction path: ${fullPath}`)) - } - - if (entry.type === 'Directory') { - if (!createdDirectories.has(fullPath)) { - createdDirectories.add(fullPath) - await resolveOrCreateDirectory(fullPath).then(() => { - entry.autodrain() - callback() - }) - } else { - entry.autodrain() - callback() - } - } else { - core.info(`Extracting artifact entry: ${fullPath}`) - if (!createdDirectories.has(path.dirname(fullPath))) { - createdDirectories.add(path.dirname(fullPath)) - await resolveOrCreateDirectory(path.dirname(fullPath)) - } - - const writeStream = createWriteStream(fullPath) - writeStream.on('finish', callback) - writeStream.on('error', reject) - entry.pipe(writeStream) - } - } - }) - ) - .on('finish', async () => { + .pipe(unzip.Extract({path: directory})) + .on('close', () => { clearTimeout(timer) resolve() })
packages/artifact/__tests__/download-artifact.test.ts+16 −8 modified@@ -200,14 +200,12 @@ describe('download-artifact', () => { } ) - await expect( - downloadArtifactPublic( - fixtures.artifactID, - fixtures.repositoryOwner, - fixtures.repositoryName, - fixtures.token - ) - ).rejects.toBeInstanceOf(Error) + const response = await downloadArtifactPublic( + fixtures.artifactID, + fixtures.repositoryOwner, + fixtures.repositoryName, + fixtures.token + ) expect(downloadArtifactMock).toHaveBeenCalledWith({ owner: fixtures.repositoryOwner, @@ -223,6 +221,16 @@ describe('download-artifact', () => { expect(mockGetArtifactMalicious).toHaveBeenCalledWith( fixtures.blobStorageUrl ) + + // ensure path traversal was not possible + expect( + fs.existsSync(path.join(fixtures.workspaceDir, 'x/etc/hosts')) + ).toBe(true) + expect( + fs.existsSync(path.join(fixtures.workspaceDir, 'y/etc/hosts')) + ).toBe(true) + + expect(response.downloadPath).toBe(fixtures.workspaceDir) }) it('should successfully download an artifact to user defined path', async () => {
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
9- github.com/advisories/GHSA-6q32-hq47-5qq3ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-42471ghsaADVISORY
- github.com/actions/download-artifact/blob/v3/package.jsonghsaWEB
- github.com/actions/toolkit/commit/29885a805ef3e95a9862dcaa8431c30981960017ghsaWEB
- github.com/actions/toolkit/pull/1602ghsaWEB
- github.com/actions/toolkit/pull/1666ghsax_refsource_MISCWEB
- github.com/actions/toolkit/pull/1724ghsaWEB
- github.com/actions/toolkit/security/advisories/GHSA-6q32-hq47-5qq3ghsax_refsource_CONFIRMWEB
- snyk.io/research/zip-slip-vulnerabilityghsax_refsource_MISCWEB
News mentions
0No linked articles in our index yet.