VYPR
Critical severity9.8NVD Advisory· Published Apr 25, 2026· Updated Apr 29, 2026

CVE-2026-6951

CVE-2026-6951

Description

Versions of the package simple-git before 3.36.0 are vulnerable to Remote Code Execution (RCE) due to an incomplete fix for CVE-2022-25912 that blocks the -c option but not the equivalent --config form. If untrusted input can reach the options argument passed to simple-git, an attacker may still achieve remote code execution by enabling protocol.ext.allow=always and using an ext:: clone source.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
simple-gitnpm
< 3.36.03.36.0

Affected products

1

Patches

1
89a2294febed

Environment Parsing (#1156)

https://github.com/steveukx/git-jsSteve KingApr 11, 2026via ghsa
29 files changed · +1093 279
  • .changeset/config.json+2 2 modified
    @@ -1,10 +1,10 @@
     {
    -  "$schema": "https://unpkg.com/@changesets/config@1.6.4/schema.json",
    +  "$schema": "https://unpkg.com/@changesets/config@3.1.3/schema.json",
       "changelog": "@changesets/cli/changelog",
       "commit": false,
       "linked": [],
       "access": "public",
    -  "baseBranch": "main",
    +  "baseBranch": "origin/main",
       "updateInternalDependencies": "patch",
       "ignore": [
         "@simple-git/test-utils"
    
  • .changeset/deep-ducks-care.md+11 0 added
    @@ -0,0 +1,11 @@
    +---
    +"@simple-git/argv-parser": minor
    +simple-git: minor
    +---
    +
    +Extend known exploitable configuration keys and per-task environment variables.
    +
    +Note - `ParsedVulnerabilities` from `argv-parser` is removed in favour of a readonly array of `Vulnerability` to match usage in `simple-git`, rolled into the new `vulnerabilityCheck` for simpler access to the identified issues. 
    +
    +Thanks to @zebbern for identifying the need to block `core.fsmonitor`.
    +Thanks to @kodareef5 for identifying the need to block `GIT_CONFIG_COUNT` environment variables and  `--template` / `merge` related config. 
    
  • docs/PLUGIN-UNSAFE-ACTIONS.md+407 14 modified
    @@ -1,37 +1,44 @@
     ## Unsafe Actions
     
    -As `simple-git` passes generated arguments through to a child process of the calling node.js process, it is recommended
    -that any parameter sourced from user input is validated before being passed to the `simple-git` API.
    +As `simple-git` passes generated arguments directly to a `git` child process, **all parameters sourced from
    +user input must be validated and sanitised** before being passed to any `simple-git` API, regardless of which
    +command is being called. There is no command that is inherently safe to call with unsanitised user data.
     
    -In some cases where there is an elevated potential for harm `simple-git` will throw an exception unless you have
    -explicitly opted in to the potentially unsafe action.
    +In cases where there is a heightened potential for harm — where a single unsanitised argument could allow
    +arbitrary command execution or the disclosure of sensitive credentials — `simple-git` will additionally throw
    +a `GitPluginError` unless you have explicitly opted in to the potentially unsafe behaviour.
     
    -### Enabling custom upload and receive packs
    +These blocks are a safety net, not a substitute for input validation. They cover known high-risk patterns,
    +but they do not protect against every possible injection or misuse of the `git` command line.
     
    -Instead of using the default `git-receive-pack` and `git-upload-pack` binaries to parse incoming and outgoing
    -data, `git` can be configured to use _any_ arbitrary binary or evaluable script.
    +### Custom upload and receive packs
     
    -To avoid accidentally triggering the evaluation of a malicious script when merging user provided parameters
    -into command executed by `simple-git`, custom pack options (usually with the `--receive-pack` and `--upload-pack`)
    -are blocked without explicitly opting into their use  
    +Instead of using the default `git-receive-pack` and `git-upload-pack` binaries to parse incoming and outgoing
    +data, `git` can be configured to use _any_ arbitrary binary or evaluable script. This applies whether the
    +binary is set via the `--upload-pack` / `--receive-pack` flags or through per-remote configuration
    +(`remote.<name>.uploadpack` / `remote.<name>.receivepack`).
     
     ```typescript
     import { simpleGit } from 'simple-git';
     
    -// throws
    +// throws — via flag
     await simpleGit()
        .raw('push', '--receive-pack=git-receive-pack-custom');
     
    -// allows calling clone with a helper transport
    +// throws — via per-remote configuration
    +await simpleGit()
    +   .raw('-c', 'remote.origin.uploadpack=/custom/upload-pack', 'fetch');
    +
    +// opt in to using custom pack binaries
     await simpleGit({ unsafe: { allowUnsafePack: true } })
        .raw('push', '--receive-pack=git-receive-pack-custom');
     ```
     
     ### Overriding allowed protocols
     
    -A standard installation of `git` permits `file`, `http` and `ssh` protocols for a remote. A range of 
    +A standard installation of `git` permits `file`, `http` and `ssh` protocols for a remote. A range of
     [git remote helpers](https://git-scm.com/docs/gitremote-helpers) other than these default few can be
    -used by referring to te helper name in the remote protocol - for example the git file descriptor transport
    +used by referring to the helper name in the remote protocol - for example the git file descriptor transport
     [git-remote-fd](https://git-scm.com/docs/git-remote-fd) would be used in a remote protocol such as:
     
     ```
    @@ -57,3 +64,389 @@ await simpleGit({ unsafe: { allowUnsafeProtocolOverride: true } })
     > *Be advised* helper transports can be used to call arbitrary binaries on the host machine.
     > Do not allow them in applications where you are not in control of the input parameters.
     
    +### Command aliases
    +
    +Git allows defining shorthand aliases for any command or external shell script via `alias.*` configuration.
    +Passing an unsanitised value as an alias target could cause `git` to execute an arbitrary command.
    +
    +```typescript
    +import { simpleGit } from 'simple-git';
    +
    +// throws
    +await simpleGit()
    +   .raw('-c', 'alias.ls=!ls -la', 'ls');
    +
    +// opt in to defining aliases
    +await simpleGit({ unsafe: { allowUnsafeAlias: true } })
    +   .raw('-c', 'alias.ls=!ls -la', 'ls');
    +```
    +
    +### Credential helpers
    +
    +Git credential helpers are external binaries or scripts that store and retrieve authentication credentials.
    +An attacker-controlled `credential.helper` value could direct `git` to run an arbitrary binary with access
    +to any credentials passed through.
    +
    +```typescript
    +import { simpleGit } from 'simple-git';
    +
    +// throws
    +await simpleGit()
    +   .raw('-c', 'credential.helper=/path/to/malicious-script', 'clone', '--', 'https://example.com/repo');
    +
    +// opt in to using a custom credential helper
    +await simpleGit({ unsafe: { allowUnsafeCredentialHelper: true } })
    +   .raw('-c', 'credential.helper=/path/to/custom-helper', 'clone', '--', 'https://example.com/repo');
    +```
    +
    +### Ask-pass programs
    +
    +The `core.askPass` configuration and the `GIT_ASKPASS` / `SSH_ASKPASS` environment variables define an
    +external binary that `git` will call to prompt for passwords. Controlling this value allows an attacker to
    +intercept credentials by substituting their own binary.
    +
    +```typescript
    +import { simpleGit } from 'simple-git';
    +
    +// throws — via config flag
    +await simpleGit()
    +   .raw('-c', 'core.askPass=/path/to/capture-credentials', 'clone', '--', 'https://example.com/repo');
    +
    +// throws — via environment variable
    +await simpleGit()
    +   .env('GIT_ASKPASS', '/path/to/capture-credentials')
    +   .clone('https://example.com/repo');
    +
    +// opt in to setting a custom ask-pass program
    +await simpleGit({ unsafe: { allowUnsafeAskPass: true } })
    +   .raw('-c', 'core.askPass=/usr/lib/git-core/git-gui--askpass', 'clone', '--', 'https://example.com/repo');
    +```
    +
    +### SSH command
    +
    +The `core.sshCommand` configuration and the `GIT_SSH` / `GIT_SSH_COMMAND` environment variables define the
    +binary used by `git` when making SSH connections. An attacker-controlled value could substitute an arbitrary
    +binary for the SSH transport.
    +
    +```typescript
    +import { simpleGit } from 'simple-git';
    +
    +// throws — via config flag
    +await simpleGit()
    +   .raw('-c', 'core.sshCommand=malicious-binary', 'clone', 'git@example.com:repo.git');
    +
    +// throws — via environment variable
    +await simpleGit()
    +   .env('GIT_SSH_COMMAND', 'malicious-binary')
    +   .clone('git@example.com:repo.git');
    +
    +// opt in to using a custom SSH binary
    +await simpleGit({ unsafe: { allowUnsafeSshCommand: true } })
    +   .env('GIT_SSH_COMMAND', 'ssh -i ~/.ssh/deploy_key')
    +   .clone('git@example.com:repo.git');
    +```
    +
    +### Git proxy command
    +
    +The `core.gitProxy` configuration and `GIT_PROXY_COMMAND` environment variable define a command to be
    +executed as a proxy for the `git://` transport. Passing an attacker-controlled value here can result in
    +arbitrary command execution on each remote operation.
    +
    +```typescript
    +import { simpleGit } from 'simple-git';
    +
    +// throws — via config flag
    +await simpleGit()
    +   .raw('-c', 'core.gitProxy=malicious-binary', 'fetch');
    +
    +// throws — via environment variable
    +await simpleGit()
    +   .env('GIT_PROXY_COMMAND', 'malicious-binary')
    +   .fetch();
    +
    +// opt in to using a custom git proxy
    +await simpleGit({ unsafe: { allowUnsafeGitProxy: true } })
    +   .env('GIT_PROXY_COMMAND', 'socks5proxywrapper')
    +   .fetch();
    +```
    +
    +### Text editor
    +
    +The `core.editor` and `sequence.editor` configurations and the `EDITOR` / `GIT_EDITOR` / `GIT_SEQUENCE_EDITOR`
    +environment variables define the text editor binary that `git` will open for interactive operations.
    +`core.editor` is used for commit messages and similar prompts; `sequence.editor` and `GIT_SEQUENCE_EDITOR`
    +are used specifically for the interactive rebase todo list. A malicious value in any of these can substitute
    +an arbitrary binary.
    +
    +```typescript
    +import { simpleGit } from 'simple-git';
    +
    +// throws — general editor via config
    +await simpleGit()
    +   .raw('-c', 'core.editor=malicious-binary', 'commit', '--amend');
    +
    +// throws — rebase sequence editor via config
    +await simpleGit()
    +   .raw('-c', 'sequence.editor=malicious-binary', 'rebase', '-i', 'HEAD~3');
    +
    +// throws — via environment variable
    +await simpleGit()
    +   .env('GIT_SEQUENCE_EDITOR', 'malicious-binary')
    +   .raw('rebase', '-i', 'HEAD~3');
    +
    +// opt in to using a custom editor
    +await simpleGit({ unsafe: { allowUnsafeEditor: true } })
    +   .env('GIT_EDITOR', '/usr/bin/nano')
    +   .raw('commit', '--amend');
    +```
    +
    +### Pager
    +
    +The `core.pager` configuration and the `GIT_PAGER` / `PAGER` environment variables control the binary used
    +to page output from `git` commands. Substituting a malicious binary here provides an execution path that
    +runs for any paged output.
    +
    +```typescript
    +import { simpleGit } from 'simple-git';
    +
    +// throws — via config flag
    +await simpleGit()
    +   .raw('-c', 'core.pager=malicious-binary', 'log');
    +
    +// throws — via environment variable
    +await simpleGit()
    +   .env('GIT_PAGER', 'malicious-binary')
    +   .log();
    +
    +// opt in to using a custom pager
    +await simpleGit({ unsafe: { allowUnsafePager: true } })
    +   .env('GIT_PAGER', 'less -R')
    +   .log();
    +```
    +
    +### Hooks path
    +
    +The `core.hooksPath` configuration redirects `git` to load its event hooks from a location other than the
    +default `.git/hooks` directory. Controlling this path allows an attacker to cause arbitrary scripts to be
    +run automatically on standard git operations such as `commit` and `merge`.
    +
    +```typescript
    +import { simpleGit } from 'simple-git';
    +
    +// throws
    +await simpleGit()
    +   .raw('-c', 'core.hooksPath=/attacker/controlled/hooks', 'commit', '-m', 'message');
    +
    +// opt in to using a custom hooks path
    +await simpleGit({ unsafe: { allowUnsafeHooksPath: true } })
    +   .raw('-c', 'core.hooksPath=/custom/shared/hooks', 'commit', '-m', 'message');
    +```
    +
    +### Template directory
    +
    +The `init.templateDir` configuration, `--template` flag, and `GIT_TEMPLATE_DIR` environment variable
    +define a directory whose contents are copied into a newly initialised `.git` directory. An attacker-controlled
    +template directory can plant hooks or configuration into every repository initialised by the process.
    +
    +```typescript
    +import { simpleGit } from 'simple-git';
    +
    +// throws — via config flag
    +await simpleGit()
    +   .raw('-c', 'init.templateDir=/attacker/controlled/template', 'init', 'new-repo');
    +
    +// throws — via flag
    +await simpleGit()
    +   .raw('init', '--template=/attacker/controlled/template', 'new-repo');
    +
    +// throws — via environment variable
    +await simpleGit()
    +   .env('GIT_TEMPLATE_DIR', '/attacker/controlled/template')
    +   .init('new-repo');
    +
    +// opt in to using a custom template directory
    +await simpleGit({ unsafe: { allowUnsafeTemplateDir: true } })
    +   .raw('init', '--template=/custom/template', 'new-repo');
    +```
    +
    +### External diff tool
    +
    +The `diff.external` configuration, per-driver `diff.<driver>.command`, and `GIT_EXTERNAL_DIFF` environment
    +variable define an external binary that `git` calls to generate diffs. Substituting an attacker-controlled
    +binary gives it read access to every file involved in a diff operation.
    +
    +```typescript
    +import { simpleGit } from 'simple-git';
    +
    +// throws — global external diff via config
    +await simpleGit()
    +   .raw('-c', 'diff.external=malicious-diff-tool', 'diff');
    +
    +// throws — per-driver diff command via config
    +await simpleGit()
    +   .raw('-c', 'diff.pdf.command=malicious-diff-tool', 'diff', 'document.pdf');
    +
    +// throws — via environment variable
    +await simpleGit()
    +   .env('GIT_EXTERNAL_DIFF', 'malicious-diff-tool')
    +   .diff();
    +
    +// opt in to using a custom diff tool
    +await simpleGit({ unsafe: { allowUnsafeDiffExternal: true } })
    +   .env('GIT_EXTERNAL_DIFF', '/usr/local/bin/my-diff-tool')
    +   .diff();
    +```
    +
    +### Diff text conversion
    +
    +The `diff.textconv` configuration (set per driver via `diff.<driver>.textconv`) defines a binary that converts
    +file content to text before generating a diff. This binary is called automatically whenever git diffs a file
    +with a matching driver and has read access to the file's content.
    +
    +```typescript
    +import { simpleGit } from 'simple-git';
    +
    +// throws
    +await simpleGit()
    +   .raw('-c', 'diff.pdf.textconv=malicious-converter', 'diff', 'document.pdf');
    +
    +// opt in to using a custom text converter
    +await simpleGit({ unsafe: { allowUnsafeDiffTextConv: true } })
    +   .raw('-c', 'diff.pdf.textconv=pdftotext', 'diff', 'document.pdf');
    +```
    +
    +### Filter operations
    +
    +The `filter.<driver>.clean` and `filter.<driver>.smudge` configuration values define binaries that transform
    +file content when checking out (`smudge`) and staging (`clean`). Controlling either value allows an attacker
    +to read or modify every file that passes through the filter.
    +
    +```typescript
    +import { simpleGit } from 'simple-git';
    +
    +// throws — clean filter
    +await simpleGit()
    +   .raw('-c', 'filter.lfs.clean=malicious-binary', 'add', '.');
    +
    +// throws — smudge filter
    +await simpleGit()
    +   .raw('-c', 'filter.lfs.smudge=malicious-binary', 'checkout', 'main');
    +
    +// opt in to using custom filter binaries
    +await simpleGit({ unsafe: { allowUnsafeFilter: true } })
    +   .raw('-c', 'filter.lfs.clean=git-lfs clean -- %f', '-c', 'filter.lfs.smudge=git-lfs smudge -- %f', 'checkout', 'main');
    +```
    +
    +### File system monitor
    +
    +The `core.fsmonitor` configuration specifies an external binary that `git` uses to track file system changes.
    +This binary is invoked automatically in the background during many common operations, making it a persistent
    +execution path if an attacker can control the value.
    +
    +```typescript
    +import { simpleGit } from 'simple-git';
    +
    +// throws
    +await simpleGit()
    +   .raw('-c', 'core.fsmonitor=malicious-monitor', 'status');
    +
    +// opt in to using a custom file system monitor
    +await simpleGit({ unsafe: { allowUnsafeFsMonitor: true } })
    +   .raw('-c', 'core.fsmonitor=true', 'status');
    +```
    +
    +### GPG signing program
    +
    +The `gpg.program` configuration defines the binary used to sign commits and tags. Per-format variants
    +`gpg.ssh.program` and `gpg.x509.program` select the signing binary for SSH and X.509 signatures
    +respectively. All three are matched by a single block on `gpg.*.program`. Controlling any of these values
    +allows an attacker to run an arbitrary binary whenever a signed commit or tag is created.
    +
    +```typescript
    +import { simpleGit } from 'simple-git';
    +
    +// throws — default GPG program
    +await simpleGit()
    +   .raw('-c', 'gpg.program=malicious-binary', 'commit', '-S', '-m', 'signed commit');
    +
    +// throws — SSH signing program
    +await simpleGit()
    +   .raw('-c', 'gpg.ssh.program=malicious-binary', 'commit', '-S', '-m', 'signed commit');
    +
    +// opt in to using a custom GPG binary
    +await simpleGit({ unsafe: { allowUnsafeGpgProgram: true } })
    +   .raw('-c', 'gpg.program=/usr/local/bin/gpg2', 'commit', '-S', '-m', 'signed commit');
    +```
    +
    +### Merge drivers
    +
    +The `merge.driver`, `mergetool.cmd`, and `mergetool.path` configurations define external binaries used to
    +resolve merge conflicts. Controlling any of these values allows an attacker to run arbitrary code whenever
    +a merge conflict occurs.
    +
    +```typescript
    +import { simpleGit } from 'simple-git';
    +
    +// throws — custom merge driver
    +await simpleGit()
    +   .raw('-c', 'merge.union.driver=malicious-merger %O %A %B', 'merge', 'feature-branch');
    +
    +// throws — merge tool command
    +await simpleGit()
    +   .raw('-c', 'mergetool.custom.cmd=malicious-binary $MERGED', 'mergetool');
    +
    +// opt in to using custom merge drivers
    +await simpleGit({ unsafe: { allowUnsafeMergeDriver: true } })
    +   .raw('-c', 'mergetool.vimdiff.path=/usr/bin/vim', 'mergetool');
    +```
    +
    +### Configuration paths via environment variables
    +
    +The `GIT_CONFIG_GLOBAL`, `GIT_CONFIG_SYSTEM`, `GIT_CONFIG`, `GIT_EXEC_PATH`, and `PREFIX` environment
    +variables override the paths `git` uses to locate its configuration files and built-in commands. Controlling
    +these paths allows an attacker to supply an entirely malicious git configuration or replace git's built-in
    +commands with arbitrary binaries.
    +
    +```typescript
    +import { simpleGit } from 'simple-git';
    +
    +// throws
    +await simpleGit()
    +   .env('GIT_CONFIG_GLOBAL', '/attacker/controlled/gitconfig')
    +   .clone('https://example.com/repo');
    +
    +// opt in to overriding git configuration paths
    +await simpleGit({ unsafe: { allowUnsafeConfigPaths: true } })
    +   .env('GIT_CONFIG_GLOBAL', '/custom/global/gitconfig')
    +   .clone('https://example.com/repo');
    +```
    +
    +### Environment-based configuration
    +
    +Git supports injecting configuration values at runtime through a set of numbered environment variables:
    +`GIT_CONFIG_COUNT`, `GIT_CONFIG_KEY_n`, and `GIT_CONFIG_VALUE_n`. When `GIT_CONFIG_COUNT` is set to `N`,
    +git reads `N` key/value pairs from the corresponding environment variables and treats them as the highest
    +priority configuration. Because this mechanism can set any configuration value, the injected keys are
    +subject to the same block-listing checks as values passed via `-c` flags.
    +
    +```typescript
    +import { simpleGit } from 'simple-git';
    +
    +// throws — GIT_CONFIG_COUNT triggers the check; the injected key is also evaluated
    +await simpleGit()
    +   .env({
    +      GIT_CONFIG_COUNT: '1',
    +      GIT_CONFIG_KEY_0: 'core.hooksPath',
    +      GIT_CONFIG_VALUE_0: '/attacker/hooks',
    +   })
    +   .commit('message');
    +
    +// opt in to using environment-based configuration injection
    +await simpleGit({ unsafe: { allowUnsafeConfigEnvCount: true } })
    +   .env({
    +      GIT_CONFIG_COUNT: '1',
    +      GIT_CONFIG_KEY_0: 'user.email',
    +      GIT_CONFIG_VALUE_0: 'ci-bot@example.com',
    +   })
    +   .commit('CI build commit');
    +```
    
  • packages/argv-parser/index.ts+5 3 modified
    @@ -1,13 +1,15 @@
    -export { parseArgv } from './src/parse-argv';
    +export { parseArgv } from './src/args/parse-argv';
     export type {
        ConfigRead,
        ConfigScope,
        ConfigWrite,
        ParsedArgv,
        ParsedFlag,
    -} from './src/parse-argv.types';
    +} from './src/args/parse-argv.types';
    +export { parseEnv } from './src/env/parse-env';
     export type {
        Vulnerability,
        VulnerabilityCategory,
    +   VulnerabilityCategoryFlags,
     } from './src/vulnerabilities/vulnerability.types';
    -export { vulnerabilityAnalysis } from './src/vulnerabilities/vulnerability-analysis';
    +export { vulnerabilityCheck } from './src/vulnerabilities/vulnerability-check';
    
  • packages/argv-parser/src/args/parse-argv.ts+5 5 renamed
    @@ -1,9 +1,9 @@
    -import { collectConfigAccess } from './config/analyse-config';
    -import type { Flag } from './flags/flags.helpers';
    -import { parseGlobalFlags } from './flags/parse-global-flags';
    -import { parseTaskFlags } from './flags/parse-task-flags';
    +import { collectConfigAccess } from '../config/analyse-config';
    +import type { Flag } from '../flags/flags.helpers';
    +import { parseGlobalFlags } from '../flags/parse-global-flags';
    +import { parseTaskFlags } from '../flags/parse-task-flags';
    +import { vulnerabilityAnalysis } from '../vulnerabilities/vulnerability-analysis';
     import type { ParsedArgv, ParsedFlag } from './parse-argv.types';
    -import { vulnerabilityAnalysis } from './vulnerabilities/vulnerability-analysis';
     
     /**
      * Parse the tokens that would be forwarded to a `git` child-process and
    
  • packages/argv-parser/src/args/parse-argv.types.ts+2 2 renamed
    @@ -1,4 +1,4 @@
    -import type { ParsedVulnerabilities } from './vulnerabilities/vulnerability.types';
    +import type { Vulnerability } from '../vulnerabilities/vulnerability.types';
     
     /** Where a config value originates / which scope it targets. */
     export type ConfigScope =
    @@ -72,5 +72,5 @@ export interface ParsedArgv {
        /**
         * Attack vectors discovered in the arguments
         */
    -   vulnerabilities: ParsedVulnerabilities;
    +   readonly vulnerabilities: Vulnerability[];
     }
    
  • packages/argv-parser/src/config/analyse-config.ts+1 1 modified
    @@ -1,5 +1,5 @@
    +import type { ConfigScope, ConfigWrite, ParsedConfigActivity } from '../args/parse-argv.types';
     import { type Flag, scopedFlags } from '../flags/flags.helpers';
    -import type { ConfigScope, ConfigWrite, ParsedConfigActivity } from '../parse-argv.types';
     import type { ConfigOperation } from './config.types';
     import { detectConfigAction, toOperation } from './detect-config-action';
     
    
  • packages/argv-parser/src/config/detect-config-action.ts+1 1 modified
    @@ -1,5 +1,5 @@
    +import type { ConfigScope } from '../args/parse-argv.types';
     import { type Flag, scopedFlags } from '../flags/flags.helpers';
    -import type { ConfigScope } from '../parse-argv.types';
     import type { ConfigOperation } from './config.types';
     import {
        CONFIG_READ_FLAGS,
    
  • packages/argv-parser/src/env/parse-env.ts+84 0 added
    @@ -0,0 +1,84 @@
    +import type { ConfigWrite, ParsedConfigActivity } from '../args/parse-argv.types';
    +import type { Vulnerability, VulnerabilityCategory } from '../vulnerabilities/vulnerability.types';
    +import { vulnerabilityAnalysis } from '../vulnerabilities/vulnerability-analysis';
    +
    +const GitEnvKeys = {
    +   'editor': 'allowUnsafeEditor',
    +   'git_askpass': 'allowUnsafeAskPass',
    +   'git_config_global': 'allowUnsafeConfigPaths',
    +   'git_config_system': 'allowUnsafeConfigPaths',
    +   'git_config_count': 'allowUnsafeConfigEnvCount',
    +   'git_config': 'allowUnsafeConfigPaths',
    +   'git_editor': 'allowUnsafeEditor',
    +   'git_exec_path': 'allowUnsafeConfigPaths',
    +   'git_external_diff': 'allowUnsafeDiffExternal',
    +   'git_pager': 'allowUnsafePager',
    +   'git_proxy_command': 'allowUnsafeGitProxy',
    +   'git_template_dir': 'allowUnsafeTemplateDir',
    +   'git_sequence_editor': 'allowUnsafeEditor',
    +   'git_ssh': 'allowUnsafeSshCommand',
    +   'git_ssh_command': 'allowUnsafeSshCommand',
    +   'pager': 'allowUnsafePager',
    +   'prefix': 'allowUnsafeConfigPaths',
    +   'ssh_askpass': 'allowUnsafeAskPass',
    +} as const satisfies Record<string, VulnerabilityCategory>;
    +
    +type GitEnv = Record<string, string> & {
    +   git_config_count?: string;
    +};
    +
    +function* collectConfigByCount(env: GitEnv): Generator<ConfigWrite> {
    +   const count = parseInt(env.git_config_count ?? '0', 10);
    +   for (let index = 0; index < count; index++) {
    +      const key = env[`git_config_key_${index}`];
    +      const value = env[`git_config_value_${index}`];
    +
    +      if (key !== undefined) {
    +         yield { key: key.toLowerCase().trim(), value, scope: 'env' };
    +      }
    +   }
    +}
    +
    +function* collectConfigVulnerabilities(env: GitEnv): Generator<Vulnerability> {
    +   for (const key of Object.keys(env)) {
    +      if (isGitEnvKey(key)) {
    +         const category = GitEnvKeys[key];
    +         yield {
    +            category,
    +            message: `Use of "${key.toUpperCase()}" is not permitted without enabling ${category}`,
    +         };
    +      }
    +   }
    +}
    +
    +function isGitEnvKey(key: string): key is keyof typeof GitEnvKeys {
    +   return Object.hasOwn(GitEnvKeys, key);
    +}
    +
    +function prepareEnv(env: Record<string, unknown>): GitEnv {
    +   const gitEnv: GitEnv = {};
    +   for (const [key, value] of Object.entries(env)) {
    +      const envKey = key.toLowerCase().trim();
    +      if (isGitEnvKey(envKey) || envKey.startsWith('git')) {
    +         gitEnv[envKey] = String(value);
    +      }
    +   }
    +   return gitEnv;
    +}
    +
    +export function parseEnv(raw: Record<string, unknown>) {
    +   const env = prepareEnv(raw);
    +   const config: ParsedConfigActivity = {
    +      read: [],
    +      write: [...collectConfigByCount(env)],
    +   };
    +   const vulnerabilities = [
    +      ...collectConfigVulnerabilities(env),
    +      ...vulnerabilityAnalysis(null, [], config),
    +   ];
    +
    +   return {
    +      config,
    +      vulnerabilities,
    +   };
    +}
    
  • packages/argv-parser/src/tokens/flag-specs.ts+5 1 modified
    @@ -52,7 +52,7 @@ const COMMANDS: Record<string, FlagSpec> = {
              ['s', false], // -s shared
              ['u', true], // -u <upload-pack>
           ]),
    -      long: new Set(['branch', 'config', 'jobs', 'origin', 'upload-pack', 'u']),
    +      long: new Set(['branch', 'config', 'jobs', 'origin', 'upload-pack', 'u', 'template']),
        },
        commit: {
           short: new Map([
    @@ -76,6 +76,10 @@ const COMMANDS: Record<string, FlagSpec> = {
           short: new Map(),
           long: new Set(['upload-pack']),
        },
    +   init: {
    +      short: new Map(),
    +      long: new Set(['template']),
    +   },
        pull: {
           short: new Map(),
           long: new Set(['upload-pack']),
    
  • packages/argv-parser/src/vulnerabilities/detect-config-writes.ts+0 43 removed
    @@ -1,43 +0,0 @@
    -import type { ParsedConfigActivity } from '../parse-argv.types';
    -import type { Vulnerability, VulnerabilityCategory } from './vulnerability.types';
    -
    -export function* detectConfigWrites({ write }: ParsedConfigActivity): Generator<Vulnerability> {
    -   for (const config of write) {
    -      for (const helper of preventUnsafeConfig) {
    -         const vulnerability = helper(config.key);
    -         if (vulnerability) {
    -            yield vulnerability;
    -         }
    -      }
    -   }
    -}
    -
    -function preventConfigBuilder(
    -   config: string | RegExp,
    -   category: VulnerabilityCategory,
    -   message = String(config)
    -) {
    -   const regex = typeof config === 'string' ? new RegExp(`\\s*${config}`, 'i') : config;
    -
    -   return function preventCommand(key: string): Vulnerability | void {
    -      if (regex.test(key)) {
    -         return {
    -            category,
    -            message: `Configuring ${message} is not permitted without enabling ${category}`,
    -         };
    -      }
    -   };
    -}
    -
    -const preventUnsafeConfig = [
    -   preventConfigBuilder(
    -      /^\s*protocol(.[a-z]+)?.allow/i,
    -      'allowUnsafeProtocolOverride',
    -      'protocol.allow'
    -   ),
    -   preventConfigBuilder('core.sshCommand', 'allowUnsafeSshCommand'),
    -   preventConfigBuilder('core.fsmonitor', 'allowUnsafeFsMonitor'),
    -   preventConfigBuilder('core.gitProxy', 'allowUnsafeGitProxy'),
    -   preventConfigBuilder('core.hooksPath', 'allowUnsafeHooksPath'),
    -   preventConfigBuilder('diff.external', 'allowUnsafeDiffExternal'),
    -];
    
  • packages/argv-parser/src/vulnerabilities/detect-upload-pack.ts+0 28 removed
    @@ -1,28 +0,0 @@
    -import type { Flag } from '../flags/flags.helpers';
    -import type { Vulnerability } from './vulnerability.types';
    -
    -export function* detectUploadPack(task: null | string, flags: Flag[]): Generator<Vulnerability> {
    -   for (const flag of flags) {
    -      if (/^--(upload|receive)-pack/.test(flag.name)) {
    -         yield {
    -            category: 'allowUnsafePack',
    -            message:
    -               'Use of --upload-pack or --receive-pack is not permitted without enabling allowUnsafePack',
    -         };
    -      }
    -      if (task === 'clone' && (/^-\w*u/.test(flag.name) || flag.name === '--u')) {
    -         yield {
    -            category: 'allowUnsafePack',
    -            message:
    -               'Use of clone with option -u is not permitted without enabling allowUnsafePack',
    -         };
    -      }
    -      if (task === 'push' && /^--exec/.test(flag.name)) {
    -         yield {
    -            category: 'allowUnsafePack',
    -            message:
    -               'Use of push with option --exec is not permitted without enabling allowUnsafePack',
    -         };
    -      }
    -   }
    -}
    
  • packages/argv-parser/src/vulnerabilities/detect-vulnerable-config-writes.ts+63 0 added
    @@ -0,0 +1,63 @@
    +import type { ParsedConfigActivity } from '../args/parse-argv.types';
    +import type { Vulnerability, VulnerabilityCategory } from './vulnerability.types';
    +
    +export function* detectVulnerableConfigWrites({
    +   write,
    +}: ParsedConfigActivity): Generator<Vulnerability> {
    +   for (const config of write) {
    +      for (const helper of preventUnsafeConfig) {
    +         const vulnerability = helper(config.key);
    +         if (vulnerability) {
    +            yield vulnerability;
    +         }
    +      }
    +   }
    +}
    +
    +function preventConfigBuilder(
    +   config: string | RegExp,
    +   category: VulnerabilityCategory,
    +   message = String(config)
    +) {
    +   const regex = typeof config === 'string' ? new RegExp(`\\s*${config.toLowerCase()}`) : config;
    +
    +   return function preventCommand(key: string): Vulnerability | void {
    +      if (regex.test(key)) {
    +         return {
    +            category,
    +            message: `Configuring ${message} is not permitted without enabling ${category}`,
    +         };
    +      }
    +   };
    +}
    +
    +function preventExpandedConfigBuilder(config: string, category: VulnerabilityCategory) {
    +   const regex = new RegExp(`\\s*${config.toLowerCase().replace(/\./g, '(\..+)?.')}`);
    +   return preventConfigBuilder(regex, category, config);
    +}
    +
    +const preventUnsafeConfig = [
    +   preventConfigBuilder('alias', 'allowUnsafeAlias'),
    +   preventConfigBuilder('core.askPass', 'allowUnsafeAskPass'),
    +   preventConfigBuilder('core.editor', 'allowUnsafeEditor'),
    +   preventConfigBuilder('core.fsmonitor', 'allowUnsafeFsMonitor'),
    +   preventConfigBuilder('core.gitProxy', 'allowUnsafeGitProxy'),
    +   preventConfigBuilder('core.hooksPath', 'allowUnsafeHooksPath'),
    +   preventConfigBuilder('core.pager', 'allowUnsafePager'),
    +   preventConfigBuilder('core.sshCommand', 'allowUnsafeSshCommand'),
    +   preventExpandedConfigBuilder('credential.helper', 'allowUnsafeCredentialHelper'),
    +   preventExpandedConfigBuilder('diff.command', 'allowUnsafeDiffExternal'),
    +   preventConfigBuilder('diff.external', 'allowUnsafeDiffExternal'),
    +   preventExpandedConfigBuilder('diff.textconv', 'allowUnsafeDiffTextConv'),
    +   preventExpandedConfigBuilder('filter.clean', 'allowUnsafeFilter'),
    +   preventExpandedConfigBuilder('filter.smudge', 'allowUnsafeFilter'),
    +   preventExpandedConfigBuilder('gpg.program', 'allowUnsafeGpgProgram'),
    +   preventConfigBuilder('init.templateDir', 'allowUnsafeTemplateDir'),
    +   preventExpandedConfigBuilder('merge.driver', 'allowUnsafeMergeDriver'),
    +   preventExpandedConfigBuilder('mergetool.path', 'allowUnsafeMergeDriver'),
    +   preventExpandedConfigBuilder('mergetool.cmd', 'allowUnsafeMergeDriver'),
    +   preventExpandedConfigBuilder('protocol.allow', 'allowUnsafeProtocolOverride'),
    +   preventExpandedConfigBuilder('remote.receivepack', 'allowUnsafePack'),
    +   preventExpandedConfigBuilder('remote.uploadpack', 'allowUnsafePack'),
    +   preventConfigBuilder('sequence.editor', 'allowUnsafeEditor'),
    +];
    
  • packages/argv-parser/src/vulnerabilities/detect-vulnerable-flags.ts+48 0 added
    @@ -0,0 +1,48 @@
    +import type { Flag } from '../flags/flags.helpers';
    +import type { Vulnerability, VulnerabilityCategory } from './vulnerability.types';
    +
    +export function* detectVulnerableFlags(
    +   task: null | string,
    +   flags: Flag[]
    +): Generator<Vulnerability> {
    +   for (const flag of flags) {
    +      for (const helper of preventUnsafeFlags) {
    +         const vulnerability = helper(task, flag.name);
    +         if (vulnerability) {
    +            yield vulnerability;
    +         }
    +      }
    +   }
    +}
    +
    +function preventFlagBuilder(
    +   task: string | null,
    +   flag: string | RegExp,
    +   category: VulnerabilityCategory,
    +   name = String(flag)
    +) {
    +   const regex = typeof flag === 'string' ? new RegExp(`\\s*${flag.toLowerCase()}`) : flag;
    +   const message = `Use of ${task ? `${task} with option ` : ''}${name} is not permitted without enabling ${category}`;
    +
    +   return function preventFlag(currentTask: string | null, flagName: string): Vulnerability | void {
    +      if ((!task || currentTask === task) && regex.test(flagName)) {
    +         return {
    +            category,
    +            message,
    +         };
    +      }
    +   };
    +}
    +
    +const preventUnsafeFlags = [
    +   preventFlagBuilder(
    +      null,
    +      /--(upload|receive)-pack/,
    +      'allowUnsafePack',
    +      '--upload-pack or --receive-pack'
    +   ),
    +   preventFlagBuilder('clone', /^-\w*u/, 'allowUnsafePack'),
    +   preventFlagBuilder('clone', '--u', 'allowUnsafePack'),
    +   preventFlagBuilder('push', '--exec', 'allowUnsafePack'),
    +   preventFlagBuilder(null, '--template', 'allowUnsafeTemplateDir'),
    +];
    
  • packages/argv-parser/src/vulnerabilities/vulnerability-analysis.ts+6 21 modified
    @@ -1,28 +1,13 @@
    +import type { ParsedConfigActivity } from '../args/parse-argv.types';
     import type { Flag } from '../flags/flags.helpers';
    -import type { ParsedConfigActivity } from '../parse-argv.types';
    -import { detectConfigWrites } from './detect-config-writes';
    -import { detectUploadPack } from './detect-upload-pack';
    -import type {
    -   ParsedVulnerabilities,
    -   Vulnerability,
    -   VulnerabilityCategory,
    -} from './vulnerability.types';
    +import { detectVulnerableConfigWrites } from './detect-vulnerable-config-writes';
    +import { detectVulnerableFlags } from './detect-vulnerable-flags';
    +import type { Vulnerability } from './vulnerability.types';
     
     export function vulnerabilityAnalysis(
        task: null | string,
        flags: Flag[],
        config: ParsedConfigActivity
    -): ParsedVulnerabilities {
    -   const vulnerabilities: Vulnerability[] = [
    -      ...detectUploadPack(task, flags),
    -      ...detectConfigWrites(config),
    -   ];
    -   const categories = vulnerabilities.reduce((all, vulnerability) => {
    -      return all.add(vulnerability.category);
    -   }, new Set<VulnerabilityCategory>());
    -
    -   return {
    -      categories,
    -      vulnerabilities,
    -   };
    +): Vulnerability[] {
    +   return [...detectVulnerableFlags(task, flags), ...detectVulnerableConfigWrites(config)];
     }
    
  • packages/argv-parser/src/vulnerabilities/vulnerability-check.ts+10 0 added
    @@ -0,0 +1,10 @@
    +import { parseArgv } from '../args/parse-argv';
    +import { parseEnv } from '../env/parse-env';
    +
    +/**
    + * Retrieves just the vulnerabilities identified in the supplied varargs tokens
    + * and environment variables.
    + */
    +export function vulnerabilityCheck(tokens: readonly string[], env: Record<string, unknown>) {
    +   return [...parseArgv(...tokens).vulnerabilities, ...parseEnv(env).vulnerabilities];
    +}
    
  • packages/argv-parser/src/vulnerabilities/vulnerability.types.ts+117 11 modified
    @@ -1,18 +1,124 @@
    -export type VulnerabilityCategory =
    -   | 'allowUnsafeDiffExternal'
    -   | 'allowUnsafeFsMonitor'
    -   | 'allowUnsafeGitProxy'
    -   | 'allowUnsafeHooksPath'
    -   | 'allowUnsafePack'
    -   | 'allowUnsafeProtocolOverride'
    -   | 'allowUnsafeSshCommand';
    +export type VulnerabilityCategory = keyof VulnerabilityCategoryFlags;
     
     export interface Vulnerability {
        category: VulnerabilityCategory;
        message: string;
     }
     
    -export interface ParsedVulnerabilities {
    -   categories: Set<VulnerabilityCategory>;
    -   vulnerabilities: Vulnerability[];
    +export interface VulnerabilityCategoryFlags {
    +   /**
    +    * Use of the `alias.*` configuration settings in simple-git tasks
    +    */
    +   allowUnsafeAlias: boolean;
    +
    +   /**
    +    * Use of the `core.askPass` configuration setting and environment variables in simple-git tasks
    +    */
    +   allowUnsafeAskPass: boolean;
    +
    +   /**
    +    * Allows using environment variables to set configuration paths in simple-git tasks
    +    */
    +   allowUnsafeConfigPaths: boolean;
    +
    +   /**
    +    * Allows setting configuration fields from environment variables in simple-git tasks. Any
    +    * configuration set in this way will still be subject to the same block-listing checks and
    +    * may require other unsafe flags to be enabled for use.
    +    */
    +   allowUnsafeConfigEnvCount: boolean;
    +
    +   /**
    +    * Allows setting credential helper in simple-git tasks
    +    */
    +   allowUnsafeCredentialHelper: boolean;
    +
    +   /**
    +    * Allows setting path to the text editor utility in simple-git tasks
    +    */
    +   allowUnsafeEditor: boolean;
    +
    +   /**
    +    * Allows use of setting paths for merge tools in simple-git tasks
    +    */
    +   allowUnsafeMergeDriver: boolean;
    +
    +   /**
    +    * Allows setting path to the pager utility in simple-git tasks
    +    */
    +   allowUnsafePager: boolean;
    +
    +   /**
    +    * By default, `simple-git` prevents the use of inline configuration
    +    * options to override the protocols available for the `git` child
    +    * process to prevent accidental security vulnerabilities when
    +    * unsanitised user data is passed directly into operations such as
    +    * `git.addRemote`, `git.clone` or `git.raw`.
    +    *
    +    * Enable this override to use the `ext::` protocol (see examples on
    +    * [git-scm.com](https://git-scm.com/docs/git-remote-ext#_examples)).
    +    */
    +   allowUnsafeProtocolOverride: boolean;
    +
    +   /**
    +    * Given the possibility of using `--upload-pack` and `--receive-pack` as
    +    * attack vectors, the use of these in any command (or the shorthand
    +    * `-u` option in a `clone` operation) are blocked by default.
    +    *
    +    * Enable this override to permit the use of these arguments.
    +    */
    +   allowUnsafePack: boolean;
    +
    +   /**
    +    * Using a `-c` switch to enable custom SSH commands opens up a potential
    +    * attack vector for running arbitrary commands.
    +    */
    +   allowUnsafeSshCommand: boolean;
    +
    +   /**
    +    * Using a `-c` switch to enable custom proxy command for the `git://` transport
    +    * exposes and attack vector for running arbitrary commands.
    +    */
    +   allowUnsafeGitProxy: boolean;
    +
    +   /**
    +    * Using a `-c` switch to enable custom hooks path commands to be run automatically
    +    * exposes and attack vector for running arbitrary commands.
    +    */
    +   allowUnsafeHooksPath: boolean;
    +
    +   /**
    +    * Using a `-c` switch to enable setting binary for processing diffs
    +    * exposes and attack vector for running arbitrary commands.
    +    */
    +   allowUnsafeDiffExternal: boolean;
    +
    +   /**
    +    * Using a `-c` switch to enable setting binary for retrieving text content of a file
    +    */
    +   allowUnsafeDiffTextConv: boolean;
    +
    +   /**
    +    * Using a `-c` switch to enable setting binary for `smudge` and `clean` operations
    +    * which can add and remove content to a file during checkout and commit.
    +    */
    +   allowUnsafeFilter: boolean;
    +
    +   /**
    +    * Using a `-c` switch to enable setting the binary to which `git` will delegate
    +    * file content change detection.
    +    */
    +   allowUnsafeFsMonitor: boolean;
    +
    +   /**
    +    * Using a `-c` switch to configure the GPG signing program (`gpg.program`) or a
    +    * per-format variant (`gpg.ssh.program`, `gpg.x509.program`). Controlling the signing
    +    * binary allows an attacker to run arbitrary code whenever a commit or tag is signed.
    +    */
    +   allowUnsafeGpgProgram: boolean;
    +
    +   /**
    +    * Allows overriding template directory either by environment variable or configuration in simple-git tasks
    +    */
    +   allowUnsafeTemplateDir: boolean;
     }
    
  • packages/argv-parser/test/attack-vectors.spec.ts+13 2 modified
    @@ -1,7 +1,7 @@
    -import { parseArgv } from '@simple-git/argv-parser';
    +import { parseArgv, parseEnv } from '@simple-git/argv-parser';
     import { describe, expect, it } from 'vitest';
     
    -import { aWriteConfig } from './__fixtures__/mocks';
    +import { aWriteConfig, oneVulnerability } from './__fixtures__/mocks';
     
     describe('security edge cases', () => {
        it('detects core.sshCommand injection via -c on any sub-command', () => {
    @@ -71,4 +71,15 @@ describe('security edge cases', () => {
           expect(flags).toEqual([]);
           expect(paths).toContain('-not-a-flag');
        });
    +
    +   it('detects use of template directory', () => {
    +      const expected = oneVulnerability('allowUnsafeTemplateDir');
    +
    +      expect(parseArgv('any-cmd', '-c', 'init.templateDir=/foo')).toHaveProperty(
    +         'vulnerabilities',
    +         expected
    +      );
    +      expect(parseArgv('any-cmd', '--template=/foo')).toHaveProperty('vulnerabilities', expected);
    +      expect(parseEnv({ 'Git_Template_Dir': '/foo' })).toHaveProperty('vulnerabilities', expected);
    +   });
     });
    
  • packages/argv-parser/test/__fixtures__/mocks.ts+19 1 modified
    @@ -1,4 +1,7 @@
    -import type { ConfigRead, ConfigWrite, ParsedFlag } from '../../src/parse-argv.types';
    +import { expect } from 'vitest';
    +
    +import type { ConfigRead, ConfigWrite, ParsedFlag } from '../../src/args/parse-argv.types';
    +import type { VulnerabilityCategory } from '../../src/vulnerabilities/vulnerability.types';
     
     export function aParsedFlag(name: string, value?: string): ParsedFlag {
        return value !== undefined ? { name: name, value } : { name: name };
    @@ -15,3 +18,18 @@ export function aWriteConfig(
     export function aReadConfig(key: string, scope: ConfigRead['scope']): ConfigRead {
        return { key, scope };
     }
    +
    +export function aVulnerability(category: VulnerabilityCategory) {
    +   return {
    +      category,
    +      message: expect.stringContaining(`enabling ${category}`),
    +   };
    +}
    +
    +export function oneVulnerability(category: VulnerabilityCategory) {
    +   return [aVulnerability(category)];
    +}
    +
    +export function noVulnerabilities() {
    +   return [];
    +}
    
  • packages/argv-parser/test/parse-argv.spec.ts+4 4 modified
    @@ -17,7 +17,7 @@ describe('full ParsedArgv shape', () => {
              flags: [aParsedFlag('-m', 'initial')],
              paths: [],
              config: { write: [], read: [] },
    -         vulnerabilities: { categories: new Set(), vulnerabilities: [] },
    +         vulnerabilities: [],
           });
        });
     
    @@ -27,7 +27,7 @@ describe('full ParsedArgv shape', () => {
              flags: [aParsedFlag('--force')],
              paths: [],
              config: { write: [], read: [] },
    -         vulnerabilities: { categories: new Set(), vulnerabilities: [] },
    +         vulnerabilities: [],
           });
        });
     
    @@ -37,7 +37,7 @@ describe('full ParsedArgv shape', () => {
              flags: [],
              paths: [],
              config: { write: [], read: [] },
    -         vulnerabilities: { categories: new Set(), vulnerabilities: [] },
    +         vulnerabilities: [],
           });
        });
     
    @@ -47,7 +47,7 @@ describe('full ParsedArgv shape', () => {
              flags: [],
              paths: [],
              config: { write: [], read: [] },
    -         vulnerabilities: { categories: new Set(), vulnerabilities: [] },
    +         vulnerabilities: [],
           });
        });
     
    
  • packages/argv-parser/test/parse-env.spec.ts+71 0 added
    @@ -0,0 +1,71 @@
    +import { parseEnv, type VulnerabilityCategory } from '@simple-git/argv-parser';
    +import { describe, expect, it } from 'vitest';
    +
    +import {
    +   aVulnerability,
    +   aWriteConfig,
    +   noVulnerabilities,
    +   oneVulnerability,
    +} from './__fixtures__/mocks';
    +
    +describe('parseEnv', () => {
    +   describe('permitted settings', () => {
    +      it('allows empty environment variables', () => {
    +         expect(parseEnv({})).toHaveProperty('vulnerabilities', noVulnerabilities());
    +      });
    +
    +      it('allows innocuous environment variables', () => {
    +         expect(parseEnv({ 'PATH': '...', LANG: 'C', LC_ALL: 'C' })).toHaveProperty(
    +            'vulnerabilities',
    +            noVulnerabilities()
    +         );
    +      });
    +   });
    +
    +   it.each<[string, string, VulnerabilityCategory | null]>([
    +      ['EDITOR', 'malicious', 'allowUnsafeEditor'],
    +      ['GIT_ASKPASS', 'malicious', 'allowUnsafeAskPass'],
    +      ['GIT_CONFIG_GLOBAL', '/tmp/malicious', 'allowUnsafeConfigPaths'],
    +      ['GIT_CONFIG_SYSTEM', '/tmp/malicious', 'allowUnsafeConfigPaths'],
    +      ['GIT_CONFIG_COUNT', '1', 'allowUnsafeConfigEnvCount'],
    +      ['GIT_CONFIG', '/tmp/malicious', 'allowUnsafeConfigPaths'],
    +      ['GIT_EDITOR', '/tmp/malicious', 'allowUnsafeEditor'],
    +      ['GIT_SEQUENCE_EDITOR', '/tmp/malicious', 'allowUnsafeEditor'],
    +      ['GIT_EXEC_PATH', '/tmp/malicious', 'allowUnsafeConfigPaths'],
    +      ['GIT_EXTERNAL_DIFF', '/tmp/malicious', 'allowUnsafeDiffExternal'],
    +      ['GIT_PAGER', '/tmp/malicious', 'allowUnsafePager'],
    +      ['GIT_PROXY_COMMAND', '/tmp/malicious', 'allowUnsafeGitProxy'],
    +      ['GIT_SSH', '/tmp/malicious', 'allowUnsafeSshCommand'],
    +      ['GIT_SSH_COMMAND', '/tmp/malicious', 'allowUnsafeSshCommand'],
    +      ['PAGER', 'malicious', 'allowUnsafePager'],
    +      ['PREFIX', 'malicious', 'allowUnsafeConfigPaths'],
    +      ['SSH_ASKPASS', 'malicious', 'allowUnsafeAskPass'],
    +   ])('with environment variable %s = %s', (key, value, category) => {
    +      const expected = category ? oneVulnerability(category) : noVulnerabilities();
    +      const parsed = parseEnv({ [key]: value });
    +
    +      expect(parsed.vulnerabilities).toEqual(expected);
    +   });
    +
    +   it('triggers configuration warnings when using environment variables', () => {
    +      const parsed = parseEnv({
    +         'git_config_count': '2',
    +         git_config_key_0: 'CORE.FSMonitor',
    +         git_config_value_0: 'malicious',
    +         git_config_key_1: 'user.name',
    +         git_config_value_1: 'bob',
    +      });
    +
    +      expect(parsed).toHaveProperty('config', {
    +         read: [],
    +         write: [
    +            aWriteConfig('core.fsmonitor', 'env', 'malicious'),
    +            aWriteConfig('user.name', 'env', 'bob'),
    +         ],
    +      });
    +      expect(parsed).toHaveProperty('vulnerabilities', [
    +         aVulnerability('allowUnsafeConfigEnvCount'),
    +         aVulnerability('allowUnsafeFsMonitor'),
    +      ]);
    +   });
    +});
    
  • packages/argv-parser/test/vulnerability-analysis.spec.ts+109 53 modified
    @@ -1,26 +1,107 @@
     import { parseArgv, type VulnerabilityCategory } from '@simple-git/argv-parser';
     import { describe, expect, it } from 'vitest';
     
    -function oneVulnerability(category: VulnerabilityCategory) {
    -   return {
    -      categories: new Set([category]),
    -      vulnerabilities: [
    -         {
    -            category,
    -            message: expect.stringContaining(`enabling ${category}`),
    -         },
    -      ],
    -   };
    -}
    -
    -function noVulnerabilities() {
    -   return {
    -      categories: new Set(),
    -      vulnerabilities: [],
    -   };
    -}
    +import { noVulnerabilities, oneVulnerability } from './__fixtures__/mocks';
     
     describe('VulnerabilityAnalysis', () => {
    +   describe('permitted settings', () => {
    +      it.skip.each<[string, string]>([
    +         ['alias.status', 'safe'],
    +         ['core.fsmonitor', 'false'],
    +         ['core.gitproxy', 'none'],
    +         ['credential.helper', 'osxkeychain'],
    +      ])('allows writing %s = %s to the git config', (key, value) => {
    +         expect(parseArgv('config', key, value)).toHaveProperty(
    +            'vulnerabilities',
    +            noVulnerabilities()
    +         );
    +      });
    +
    +      it('allows local cloning', () => {
    +         const parsed = parseArgv('clone', '--no-checkout', '--', './first', 'second');
    +         expect(parsed.vulnerabilities).toEqual(noVulnerabilities());
    +      });
    +
    +      it('allows local cloning without checkout', () => {
    +         const parsed = parseArgv('clone', '--no-checkout', '--', './first', 'second');
    +         expect(parsed.vulnerabilities).toEqual(noVulnerabilities());
    +      });
    +
    +      it('clone non-default branch is allowed (#1137)', async () => {
    +         const parsed = parseArgv(
    +            'clone',
    +            '-b',
    +            'non-default-branch',
    +            '--',
    +            'https://github.com/example/bruno.git',
    +            '/tmp/target'
    +         );
    +         expect(parsed.vulnerabilities).toEqual(noVulnerabilities());
    +      });
    +
    +      it('allows -u for non-clone commands', async () => {
    +         const parsed = parseArgv('push', '-u', 'origin/main');
    +         expect(parsed.vulnerabilities).toEqual(noVulnerabilities());
    +      });
    +
    +      it('uses pathspec protection for -u in remote', async () => {
    +         const parsed = parseArgv('clone', '--', '-u touch /tmp/pwn', 'file:///tmp/zero12');
    +         expect(parsed.vulnerabilities).toEqual(noVulnerabilities());
    +      });
    +   });
    +
    +   it.each<[string, string, VulnerabilityCategory | null]>([
    +      ['alias.status', '!malicious', 'allowUnsafeAlias'],
    +      ['core.askPass', '!malicious', 'allowUnsafeAskPass'],
    +      ['core.editor', 'malicious', 'allowUnsafeEditor'],
    +      ['core.fsmonitor', 'malicious', 'allowUnsafeFsMonitor'],
    +      ['core.gitproxy', 'malicious', 'allowUnsafeGitProxy'],
    +      ['core.hooksPath', '/malicious', 'allowUnsafeHooksPath'],
    +      ['core.pager', 'malicious', 'allowUnsafePager'],
    +      ['core.sshCommand', 'malicious', 'allowUnsafeSshCommand'],
    +      ['credential.helper', '/tmp/evil', 'allowUnsafeCredentialHelper'],
    +      ['credential.https://example.com.helper', '!malicious', 'allowUnsafeCredentialHelper'],
    +      ['credential.helper', '!malicious', 'allowUnsafeCredentialHelper'],
    +      ['diff.command', 'malicious', 'allowUnsafeDiffExternal'],
    +      ['diff.driver.command', 'malicious', 'allowUnsafeDiffExternal'],
    +      ['diff.external', 'malicious', 'allowUnsafeDiffExternal'],
    +      ['diff.textconv', 'malicious', 'allowUnsafeDiffTextConv'],
    +      ['diff.driver.textconv', 'malicious', 'allowUnsafeDiffTextConv'],
    +      ['filter.clean', 'malicious', 'allowUnsafeFilter'],
    +      ['filter.driver.clean', 'malicious', 'allowUnsafeFilter'],
    +      ['filter.smudge', 'malicious', 'allowUnsafeFilter'],
    +      ['filter.driver.smudge', 'malicious', 'allowUnsafeFilter'],
    +      ['gpg.program', 'malicious', 'allowUnsafeGpgProgram'],
    +      ['gpg.type.program', 'malicious', 'allowUnsafeGpgProgram'],
    +      ['init.templateDir', '/tmp/evil', 'allowUnsafeTemplateDir'],
    +      ['merge.driver', '/tmp/evil', 'allowUnsafeMergeDriver'],
    +      ['merge.foo.driver', '/tmp/evil', 'allowUnsafeMergeDriver'],
    +      ['mergetool.path', '/tmp/evil', 'allowUnsafeMergeDriver'],
    +      ['mergetool.foo.path', '/tmp/evil', 'allowUnsafeMergeDriver'],
    +      ['mergetool.cmd', '/tmp/evil', 'allowUnsafeMergeDriver'],
    +      ['mergetool.foo.cmd', '/tmp/evil', 'allowUnsafeMergeDriver'],
    +      ['remote.receivepack', 'malicious', 'allowUnsafePack'],
    +      ['remote.https://example.com.receivepack', 'malicious', 'allowUnsafePack'],
    +      ['remote.uploadpack', 'malicious', 'allowUnsafePack'],
    +      ['remote.https://example.com.uploadpack', 'malicious', 'allowUnsafePack'],
    +      ['sequence.editor', 'malicious', 'allowUnsafeEditor'],
    +   ])('writing %s = %s to the git config', (key, value, category) => {
    +      const expected = category ? oneVulnerability(category) : noVulnerabilities();
    +
    +      // writing local config
    +      expect(parseArgv('config', key, value).vulnerabilities).toEqual(expected);
    +
    +      // trailing inline config
    +      expect(
    +         parseArgv('clone', 'remote', 'local', '-c', `${key}=${value}`).vulnerabilities
    +      ).toEqual(expected);
    +
    +      // leading inline config
    +      expect(
    +         parseArgv('-c', `${key}=${value}`, 'clone', 'remote', 'local').vulnerabilities
    +      ).toEqual(expected);
    +   });
    +
        it.each([
           ['protocol.allow=always'],
           ['PROTOCOL.ALLOW=always'],
    @@ -30,7 +111,7 @@ describe('VulnerabilityAnalysis', () => {
           ['protocol.ssh.ALLOW=always'],
           ['protocol.ext.allow=always'],
           ['protocol.git.ALLOW=never'],
    -   ])('detects protocol.allow in format %s', async (cmd) => {
    +   ])('Case insensitive configuration captures protocol.allow in format "%s"', async (cmd) => {
           const parsed = parseArgv('config', '-c', cmd, 'config', '--list');
           expect(parsed.vulnerabilities).toEqual(oneVulnerability('allowUnsafeProtocolOverride'));
     
    @@ -52,7 +133,16 @@ describe('VulnerabilityAnalysis', () => {
     
        it.each<[VulnerabilityCategory, string]>([
           ['allowUnsafeDiffExternal', `diff.external=sh -c 'id > pwned'`],
    +      ['allowUnsafeDiffTextConv', `diff.evil.textconv=touch /tmp/proof`],
    +      ['allowUnsafeDiffExternal', `diff.evil.command=sh -c 'id > pwned'`],
    +      ['allowUnsafeFilter', `filter.evil.clean=touch /tmp/proof`],
    +      ['allowUnsafeFilter', `filter.evil.smudge=touch /tmp/proof`],
           ['allowUnsafeGitProxy', `core.gitProxy=sh -c 'id > pwned'`],
    +      ['allowUnsafeGpgProgram', `gpg.program=malicious`],
    +      ['allowUnsafeGpgProgram', `gpg.ssh.program=malicious`],
    +      ['allowUnsafeGpgProgram', `gpg.x509.program=malicious`],
    +      ['allowUnsafePack', `remote.origin.uploadpack=malicious`],
    +      ['allowUnsafePack', `remote.origin.receivepack=malicious`],
           ['allowUnsafeHooksPath', `core.hooksPath=sh -c 'id > pwned'`],
           ['allowUnsafeFsMonitor', `core.FsMonitor=CMD`],
           ['allowUnsafeSshCommand', `core.sshCommand=sh -c 'id > pwned'`],
    @@ -93,38 +183,4 @@ describe('VulnerabilityAnalysis', () => {
              ).toEqual(oneVulnerability('allowUnsafePack'));
           });
        });
    -
    -   describe('not-vulnerable', () => {
    -      it('allows local cloning', () => {
    -         const parsed = parseArgv('clone', '--no-checkout', '--', './first', 'second');
    -         expect(parsed.vulnerabilities).toEqual(noVulnerabilities());
    -      });
    -
    -      it('allows local cloning without checkout', () => {
    -         const parsed = parseArgv('clone', '--no-checkout', '--', './first', 'second');
    -         expect(parsed.vulnerabilities).toEqual(noVulnerabilities());
    -      });
    -
    -      it('clone non-default branch is allowed (#1137)', async () => {
    -         const parsed = parseArgv(
    -            'clone',
    -            '-b',
    -            'non-default-branch',
    -            '--',
    -            'https://github.com/example/bruno.git',
    -            '/tmp/target'
    -         );
    -         expect(parsed.vulnerabilities).toEqual(noVulnerabilities());
    -      });
    -
    -      it('allows -u for non-clone commands', async () => {
    -         const parsed = parseArgv('push', '-u', 'origin/main');
    -         expect(parsed.vulnerabilities).toEqual(noVulnerabilities());
    -      });
    -
    -      it('uses pathspec protection for -u in remote', async () => {
    -         const parsed = parseArgv('clone', '--', '-u touch /tmp/pwn', 'file:///tmp/zero12');
    -         expect(parsed.vulnerabilities).toEqual(noVulnerabilities());
    -      });
    -   });
     });
    
  • packages/argv-parser/test/vulnerability-check.spec.ts+36 0 added
    @@ -0,0 +1,36 @@
    +import { describe, expect, it } from 'vitest';
    +
    +import { vulnerabilityCheck } from '../src/vulnerabilities/vulnerability-check';
    +import { aVulnerability, noVulnerabilities } from './__fixtures__/mocks';
    +
    +describe('vulnerability-check', () => {
    +   describe('permitted settings', () => {
    +      it('allows empty environment variables', () => {
    +         expect(vulnerabilityCheck(['config', 'user.name', 'bob'], {})).toEqual(
    +            noVulnerabilities()
    +         );
    +      });
    +
    +      it('allows innocuous environment variables', () => {
    +         expect(vulnerabilityCheck([], { 'PATH': '...', LANG: 'C', LC_ALL: 'C' })).toEqual(
    +            noVulnerabilities()
    +         );
    +      });
    +   });
    +
    +   it('triggers configuration warnings when using environment variables', () => {
    +      const parsed = vulnerabilityCheck(['config', 'filter.clean', 'malicious'], {
    +         'git_config_count': '2',
    +         git_config_key_0: 'CORE.FSMonitor',
    +         git_config_value_0: 'malicious',
    +         git_config_key_1: 'user.name',
    +         git_config_value_1: 'bob',
    +      });
    +
    +      expect(parsed).toEqual([
    +         aVulnerability('allowUnsafeFilter'),
    +         aVulnerability('allowUnsafeConfigEnvCount'),
    +         aVulnerability('allowUnsafeFsMonitor'),
    +      ]);
    +   });
    +});
    
  • simple-git/src/lib/plugins/block-unsafe-operations-plugin.ts+4 6 modified
    @@ -1,18 +1,16 @@
    -import type { SimpleGitPlugin } from './simple-git-plugin';
    +import { vulnerabilityCheck } from '@simple-git/argv-parser';
     
     import { GitPluginError } from '../errors/git-plugin-error';
     import type { SimpleGitPluginConfig } from '../types';
    -import { parseArgv } from '@simple-git/argv-parser';
    +import type { SimpleGitPlugin } from './simple-git-plugin';
     
     export function blockUnsafeOperationsPlugin(
        options: SimpleGitPluginConfig['unsafe'] = {}
     ): SimpleGitPlugin<'spawn.args'> {
        return {
           type: 'spawn.args',
    -      action(args) {
    -         const parsed = parseArgv(...args);
    -
    -         for (const vulnerability of parsed.vulnerabilities.vulnerabilities) {
    +      action(args, { env }) {
    +         for (const vulnerability of vulnerabilityCheck(args, env)) {
                 if (options[vulnerability.category] !== true) {
                    throw new GitPluginError(undefined, 'unsafe', vulnerability.message);
                 }
    
  • simple-git/src/lib/plugins/simple-git-plugin.ts+3 1 modified
    @@ -9,7 +9,9 @@ type SimpleGitTaskPluginContext = {
     export interface SimpleGitPluginTypes {
        'spawn.args': {
           data: string[];
    -      context: SimpleGitTaskPluginContext & {};
    +      context: SimpleGitTaskPluginContext & {
    +         env: Record<string, string | undefined>;
    +      };
        };
        'spawn.binary': {
           data: string;
    
  • simple-git/src/lib/runners/git-executor-chain.ts+4 5 modified
    @@ -81,11 +81,10 @@ export class GitExecutorChain implements SimpleGitExecutor {
     
        private async attemptRemoteTask<R>(task: RunnableTask<R>, logger: OutputLogger) {
           const binary = this._plugins.exec('spawn.binary', '', pluginContext(task, task.commands));
    -      const args = this._plugins.exec(
    -         'spawn.args',
    -         [...task.commands],
    -         pluginContext(task, task.commands)
    -      );
    +      const args = this._plugins.exec('spawn.args', [...task.commands], {
    +         ...pluginContext(task, task.commands),
    +         env: { ...this.env },
    +      });
     
           const raw = await this.gitResponse(
              task,
    
  • simple-git/src/lib/types/index.ts+10 58 modified
    @@ -2,6 +2,7 @@ import type { SpawnOptions } from 'child_process';
     
     import type { SimpleGitTask } from './tasks';
     import type { SimpleGitProgressEvent } from './handlers';
    +import { VulnerabilityCategoryFlags } from '@simple-git/argv-parser';
     
     export * from './handlers';
     export * from './tasks';
    @@ -129,64 +130,15 @@ export interface SimpleGitPluginConfig {
     
        spawnOptions: Pick<SpawnOptions, 'uid' | 'gid'>;
     
    -   unsafe: {
    -      /**
    -       * Allows potentially unsafe values to be supplied in the `binary` configuration option and
    -       * `git.customBinary()` method call.
    -       */
    -      allowUnsafeCustomBinary?: boolean;
    -
    -      /**
    -       * By default, `simple-git` prevents the use of inline configuration
    -       * options to override the protocols available for the `git` child
    -       * process to prevent accidental security vulnerabilities when
    -       * unsanitised user data is passed directly into operations such as
    -       * `git.addRemote`, `git.clone` or `git.raw`.
    -       *
    -       * Enable this override to use the `ext::` protocol (see examples on
    -       * [git-scm.com](https://git-scm.com/docs/git-remote-ext#_examples)).
    -       */
    -      allowUnsafeProtocolOverride?: boolean;
    -
    -      /**
    -       * Given the possibility of using `--upload-pack` and `--receive-pack` as
    -       * attack vectors, the use of these in any command (or the shorthand
    -       * `-u` option in a `clone` operation) are blocked by default.
    -       *
    -       * Enable this override to permit the use of these arguments.
    -       */
    -      allowUnsafePack?: boolean;
    -
    -      /**
    -       * Using a `-c` switch to enable custom SSH commands opens up a potential
    -       * attack vector for running arbitrary commands.
    -       */
    -      allowUnsafeSshCommand?: boolean;
    -
    -      /**
    -       * Using a `-c` switch to enable custom proxy command for the `git://` transport
    -       * exposes and attack vector for running arbitrary commands.
    -       */
    -      allowUnsafeGitProxy?: boolean;
    -
    -      /**
    -       * Using a `-c` switch to enable custom hooks path commands to be run automatically
    -       * exposes and attack vector for running arbitrary commands.
    -       */
    -      allowUnsafeHooksPath?: boolean;
    -
    -      /**
    -       * Using a `-c` switch to enable setting binary for processing diffs
    -       * exposes and attack vector for running arbitrary commands.
    -       */
    -      allowUnsafeDiffExternal?: boolean;
    -
    -      /**
    -       * Using a `-c` switch to enable setting the binary to which `git` will delegate
    -       * file content change detection.
    -       */
    -      allowUnsafeFsMonitor?: boolean;
    -   };
    +   unsafe: Partial<
    +      VulnerabilityCategoryFlags & {
    +         /**
    +          * Allows potentially unsafe values to be supplied in the `binary` configuration option and
    +          * `git.customBinary()` method call.
    +          */
    +         allowUnsafeCustomBinary: boolean;
    +      }
    +   >;
     }
     
     /**
    
  • simple-git/test/integration/plugin.unsafe.spec.ts+47 0 modified
    @@ -8,10 +8,57 @@ import {GitPluginError} from '../..';
     describe('plugin.unsafe', () => {
        let context: SimpleGitTestContext;
     
    +   function pwnedPath() {
    +      return join(context.root, 'pwn', 'touched');
    +   }
    +
    +   function isPwned() {
    +      return exists(pwnedPath());
    +   }
    +
        process.env.DEBUG = 'simple-git,simple-git:*';
     
        beforeEach(async () => (context = await createTestContext()));
     
    +   describe('Command injection through .env', () => {
    +      beforeEach( () => context.git.init());
    +      beforeEach(() => context.dir('pwn'));
    +
    +      it('blocks core.fsmonitor by default', async () => {
    +         const result = await promiseResult(
    +            newSimpleGit(context.root).raw(
    +               '-c', `core.fsmonitor=touch ${pwnedPath()}`, 'status'
    +            )
    +         );
    +
    +         assertGitError(result.error, 'allowUnsafeFsMonitor')
    +         expect(isPwned()).toBe(false);
    +
    +
    +         const unsafeResult = await promiseResult(
    +            newSimpleGit(context.root, { unsafe: { allowUnsafeFsMonitor: true }}).raw(
    +               '-c', `core.fsmonitor=touch ${pwnedPath()}`, 'status'
    +            )
    +         );
    +
    +         expect(unsafeResult.threw).toBe(false);
    +         expect(isPwned()).toBe(true);
    +      });
    +
    +      it('blocks filter.clean by default', async () => {
    +         await context.file('file');
    +
    +         const result = await promiseResult(
    +            newSimpleGit(context.root).raw(
    +               '-c', `filter.evil.clean=touch ${pwnedPath()}`, 'add', 'file.txt'
    +            )
    +         );
    +
    +         assertGitError(result.error, 'allowUnsafeFilter')
    +         expect(isPwned()).toBe(false);
    +      });
    +   });
    +
        describe('CVE-2022-25860: command execution using clone -u', () => {
           function pwnedPath() {
              return join(context.root, 'pwn', 'touched');
    
  • simple-git/test/unit/clone.spec.ts+6 17 modified
    @@ -1,12 +1,7 @@
    -import { promiseError } from '@kwsites/promise-result';
    -import { SimpleGit, TaskOptions } from 'typings';
    -import {
    -   assertExecutedCommands,
    -   assertGitError,
    -   closeWithSuccess,
    -   newSimpleGit,
    -} from './__fixtures__';
    -import { pathspec } from '@simple-git/args-pathspec';
    +import {promiseError} from '@kwsites/promise-result';
    +import {SimpleGit, TaskOptions} from 'typings';
    +import {assertExecutedCommands, assertGitError, closeWithSuccess, newSimpleGit,} from './__fixtures__';
    +import {pathspec} from '@simple-git/args-pathspec';
     
     describe('clone', () => {
        let git: SimpleGit;
    @@ -39,13 +34,13 @@ describe('clone', () => {
           ],
           ['mirror', 'explicitly set', ['r', 'l'], ['clone', '--mirror', '--', 'r', 'l']],
           ['clone', 'kitchen sink', ['https://abcdefghijklmnopqrstuvwxyz01234567890.repo', 'dir',
    -         ['--template=<template-directory>', '-l', '-s', '--no-hardlinks', '-q', '-n', '--bare', '--mirror',
    +         ['-l', '-s', '--no-hardlinks', '-q', '-n', '--bare', '--mirror',
              '-o', 'alternative-origin', '-b', 'specific-branch', '--separate-git-dir', 'other-path',
              '--depth', '1', '--no-single-branch', '--no-tags', '--recurse-submodules=foo',
              '--no-shallow-submodules', '--no-remote-submodules', '--jobs', '2', '--sparse',
              '--no-reject-shallow', '--filter=sub-path', '--also-filter-submodules']],
     
    -         ['clone', '--template=<template-directory>', '-l', '-s', '--no-hardlinks', '-q', '-n', '--bare', '--mirror',
    +         ['clone', '-l', '-s', '--no-hardlinks', '-q', '-n', '--bare', '--mirror',
              '-o', 'alternative-origin', '-b', 'specific-branch', '--separate-git-dir', 'other-path',
              '--depth', '1', '--no-single-branch', '--no-tags', '--recurse-submodules=foo',
              '--no-shallow-submodules', '--no-remote-submodules', '--jobs', '2', '--sparse',
    @@ -91,12 +86,6 @@ describe('clone', () => {
        });
     
        describe('failures', () => {
    -      it('disallows upload-pack as remote/branch', async () => {
    -         const error = await promiseError(git.clone('origin', '--upload-pack=touch ./foo'));
    -
    -         assertGitError(error, 'allowUnsafePack');
    -      });
    -
           it('disallows upload-pack as varargs', async () => {
              const error = await promiseError(
                 git.clone('origin', 'main', {
    

Vulnerability mechanics

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

References

5

News mentions

0

No linked articles in our index yet.