VYPR
Moderate severityNVD Advisory· Published Aug 13, 2022· Updated Apr 23, 2025

Delimiter injection vulnerability in @actions/core exportVariable

CVE-2022-35954

Description

CVE-2022-35954: Delimiter injection in @actions/core exportVariable allows attackers to overwrite environment variables via untrusted input written to GITHUB_ENV.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

CVE-2022-35954: Delimiter injection in @actions/core exportVariable allows attackers to overwrite environment variables via untrusted input written to GITHUB_ENV.

Vulnerability

The core.exportVariable function in @actions/core uses a static delimiter _GitHubActionsFileCommandDelimeter_ to separate variable names and values when writing to the GITHUB_ENV file. An attacker who can control the value passed to exportVariable can inject this delimiter to break out of the intended variable and assign arbitrary values to other environment variables, such as PATH [1][4]. This is a classic delimiter injection vulnerability.

Exploitation

The attack surface is any GitHub Action workflow that writes untrusted user input (e.g., issue titles, branch names) to environment variables via core.exportVariable. The attacker does not need special privileges beyond the ability to provide input that gets processed by a vulnerable action. The injection occurs when the untrusted value contains the delimiter string, causing the subsequent content to be interpreted as a new variable assignment [4].

Impact

By overwriting sensitive environment variables like PATH, an attacker could influence the behavior of subsequent steps in a workflow, potentially leading to arbitrary code execution or privilege escalation within the runner's context [1]. The vulnerability can compromise the integrity of the entire CI/CD pipeline if exploited in a public repository or a repository that accepts external contributions.

Mitigation

The issue is fixed in @actions/core version 1.9.1, which replaces the static delimiter with a randomly generated one using UUIDs [2][3]. Users should upgrade to this version. A workaround is to sanitize user input by removing or rejecting any occurrence of the delimiter before calling exportVariable [4].

AI Insight generated on May 21, 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.

PackageAffected versionsPatched versions
@actions/corenpm
< 1.9.11.9.1

Affected products

2

Patches

1
4beda9cbc00b

Merge pull request from GHSA-7r3h-m5j6-3q42

https://github.com/actions/toolkitCory MillerAug 8, 2022via ghsa
5 files changed · +92 11
  • packages/core/package.json+5 3 modified
    @@ -1,6 +1,6 @@
     {
       "name": "@actions/core",
    -  "version": "1.9.0",
    +  "version": "1.9.1",
       "description": "Actions core lib",
       "keywords": [
         "github",
    @@ -36,9 +36,11 @@
         "url": "https://github.com/actions/toolkit/issues"
       },
       "dependencies": {
    -    "@actions/http-client": "^2.0.1"
    +    "@actions/http-client": "^2.0.1",
    +    "uuid": "^8.3.2"
       },
       "devDependencies": {
    -    "@types/node": "^12.0.2"
    +    "@types/node": "^12.0.2",
    +    "@types/uuid": "^8.3.4"
       }
     }
    
  • packages/core/package-lock.json+31 4 modified
    @@ -1,18 +1,20 @@
     {
       "name": "@actions/core",
    -  "version": "1.9.0",
    +  "version": "1.9.1",
       "lockfileVersion": 2,
       "requires": true,
       "packages": {
         "": {
           "name": "@actions/core",
    -      "version": "1.8.1",
    +      "version": "1.9.1",
           "license": "MIT",
           "dependencies": {
    -        "@actions/http-client": "^2.0.1"
    +        "@actions/http-client": "^2.0.1",
    +        "uuid": "^8.3.2"
           },
           "devDependencies": {
    -        "@types/node": "^12.0.2"
    +        "@types/node": "^12.0.2",
    +        "@types/uuid": "^8.3.4"
           }
         },
         "node_modules/@actions/http-client": {
    @@ -29,13 +31,27 @@
           "integrity": "sha512-5tabW/i+9mhrfEOUcLDu2xBPsHJ+X5Orqy9FKpale3SjDA17j5AEpYq5vfy3oAeAHGcvANRCO3NV3d2D6q3NiA==",
           "dev": true
         },
    +    "node_modules/@types/uuid": {
    +      "version": "8.3.4",
    +      "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz",
    +      "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==",
    +      "dev": true
    +    },
         "node_modules/tunnel": {
           "version": "0.0.6",
           "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
           "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==",
           "engines": {
             "node": ">=0.6.11 <=0.7.0 || >=0.7.3"
           }
    +    },
    +    "node_modules/uuid": {
    +      "version": "8.3.2",
    +      "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
    +      "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
    +      "bin": {
    +        "uuid": "dist/bin/uuid"
    +      }
         }
       },
       "dependencies": {
    @@ -53,10 +69,21 @@
           "integrity": "sha512-5tabW/i+9mhrfEOUcLDu2xBPsHJ+X5Orqy9FKpale3SjDA17j5AEpYq5vfy3oAeAHGcvANRCO3NV3d2D6q3NiA==",
           "dev": true
         },
    +    "@types/uuid": {
    +      "version": "8.3.4",
    +      "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz",
    +      "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==",
    +      "dev": true
    +    },
         "tunnel": {
           "version": "0.0.6",
           "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
           "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="
    +    },
    +    "uuid": {
    +      "version": "8.3.2",
    +      "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
    +      "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
         }
       }
     }
    
  • packages/core/RELEASES.md+3 0 modified
    @@ -1,5 +1,8 @@
     # @actions/core Releases
     
    +### 1.9.1
    +- Randomize delimiter when calling `core.exportVariable`
    +
     ### 1.9.0
     - Added `toPosixPath`, `toWin32Path` and `toPlatformPath` utilities [#1102](https://github.com/actions/toolkit/pull/1102)
     
    
  • packages/core/src/core.ts+12 1 modified
    @@ -4,6 +4,7 @@ import {toCommandProperties, toCommandValue} from './utils'
     
     import * as os from 'os'
     import * as path from 'path'
    +import { v4 as uuidv4 } from 'uuid'
     
     import {OidcClient} from './oidc-utils'
     
    @@ -86,7 +87,17 @@ export function exportVariable(name: string, val: any): void {
     
       const filePath = process.env['GITHUB_ENV'] || ''
       if (filePath) {
    -    const delimiter = '_GitHubActionsFileCommandDelimeter_'
    +    const delimiter = `ghadelimiter_${uuidv4()}`
    +
    +    // These should realistically never happen, but just in case someone finds a way to exploit uuid generation let's not allow keys or values that contain the delimiter.  
    +    if (name.includes(delimiter)) {
    +      throw new Error(`Unexpected input: name should not contain the delimiter "${delimiter}"`)
    +    }
    +
    +    if (convertedVal.includes(delimiter)) {
    +      throw new Error(`Unexpected input: value should not contain the delimiter "${delimiter}"`)
    +    }
    +
         const commandValue = `${name}<<${delimiter}${os.EOL}${convertedVal}${os.EOL}${delimiter}`
         issueFileCommand('ENV', commandValue)
       } else {
    
  • packages/core/__tests__/core.test.ts+41 3 modified
    @@ -4,6 +4,9 @@ import * as path from 'path'
     import * as core from '../src/core'
     import {HttpClient} from '@actions/http-client'
     import {toCommandProperties} from '../src/utils'
    +import * as uuid from 'uuid'
    +
    +jest.mock('uuid')
     
     /* eslint-disable @typescript-eslint/unbound-method */
     
    @@ -41,6 +44,9 @@ const testEnvVars = {
       GITHUB_ENV: ''
     }
     
    +const UUID = '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d'
    +const DELIMITER = `ghadelimiter_${UUID}`
    +
     describe('@actions/core', () => {
       beforeAll(() => {
         const filePath = path.join(__dirname, `test`)
    @@ -54,6 +60,14 @@ describe('@actions/core', () => {
           process.env[key] = testEnvVars[key as keyof typeof testEnvVars]
         }
         process.stdout.write = jest.fn()
    +
    +    jest.spyOn(uuid, 'v4').mockImplementation(() => {
    +      return UUID
    +    })
    +  })
    +
    +  afterEach(() => {
    +    jest.restoreAllMocks()
       })
     
       it('legacy exportVariable produces the correct command and sets the env', () => {
    @@ -91,7 +105,7 @@ describe('@actions/core', () => {
         core.exportVariable('my var', 'var val')
         verifyFileCommand(
           command,
    -      `my var<<_GitHubActionsFileCommandDelimeter_${os.EOL}var val${os.EOL}_GitHubActionsFileCommandDelimeter_${os.EOL}`
    +      `my var<<${DELIMITER}${os.EOL}var val${os.EOL}${DELIMITER}${os.EOL}`
         )
       })
     
    @@ -101,7 +115,7 @@ describe('@actions/core', () => {
         core.exportVariable('my var', true)
         verifyFileCommand(
           command,
    -      `my var<<_GitHubActionsFileCommandDelimeter_${os.EOL}true${os.EOL}_GitHubActionsFileCommandDelimeter_${os.EOL}`
    +      `my var<<${DELIMITER}${os.EOL}true${os.EOL}${DELIMITER}${os.EOL}`
         )
       })
     
    @@ -111,10 +125,34 @@ describe('@actions/core', () => {
         core.exportVariable('my var', 5)
         verifyFileCommand(
           command,
    -      `my var<<_GitHubActionsFileCommandDelimeter_${os.EOL}5${os.EOL}_GitHubActionsFileCommandDelimeter_${os.EOL}`
    +      `my var<<${DELIMITER}${os.EOL}5${os.EOL}${DELIMITER}${os.EOL}`
         )
       })
     
    +  it('exportVariable does not allow delimiter as value', () => {
    +    const command = 'ENV'
    +    createFileCommandFile(command)
    +    
    +    expect(() => {
    +      core.exportVariable('my var', `good stuff ${DELIMITER} bad stuff`)
    +    }).toThrow(`Unexpected input: value should not contain the delimiter "${DELIMITER}"`)
    +
    +    const filePath = path.join(__dirname, `test/${command}`)
    +    fs.unlinkSync(filePath)
    +  })
    +
    +  it('exportVariable does not allow delimiter as name', () => {
    +    const command = 'ENV'
    +    createFileCommandFile(command)
    +    
    +    expect(() => {
    +      core.exportVariable(`good stuff ${DELIMITER} bad stuff`, 'test')
    +    }).toThrow(`Unexpected input: name should not contain the delimiter "${DELIMITER}"`)
    +    
    +    const filePath = path.join(__dirname, `test/${command}`)
    +    fs.unlinkSync(filePath)
    +  })
    +
       it('setSecret produces the correct command', () => {
         core.setSecret('secret val')
         assertWriteCalls([`::add-mask::secret val${os.EOL}`])
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

4

News mentions

0

No linked articles in our index yet.