GCVE Workshop - 22 September 2026 (14:00-18:00), Luxembourg Before The Vulnopticon Conference - Registration

GHSA-C6MW-8XH8-GPQ6

Vulnerability from github – Published: 2026-09-04 18:11 – Updated: 2026-09-04 18:11
VLAI
Summary
CodeWhale: Argument Injection in `git_blame` Tool Allows Arbitrary File Read Without Approval
Details

Maintainer resolution

The CodeWhale maintainers validated this report. The affected package ranges are recorded in the advisory metadata. Version 0.8.64 contains the fix in commit 9a34b5034d29f05d1f28fa61b04719ca6a741020. Users should upgrade to 0.8.64 or later. The original reporter analysis is preserved below.

Argument Injection in git_blame Tool Allows Arbitrary File Read Without Approval

Overview

The git_blame tool in DeepSeek-TUI passes the model-supplied rev parameter unvalidated into the argv of git blame. git blame accepts --contents=<file>, which causes it to use the file's contents in place of the working tree and echo each line verbatim in the blame output. A rev value of --contents=/path/to/secret therefore exfiltrates the targeted file's contents into the tool result, which is returned to the model and displayed in the chat transcript.

The tool is registered with ApprovalRequirement::Auto and declares ToolCapability::ReadOnly. The read is in-scope for the capability label, but the target of the read is not the user expects git_blame to read files inside the workspace, not arbitrary paths on the host.

This is a sibling of the git_show argument-injection vulnerability filed separately, sharing the same root cause (missing --end-of-options sentinel and unvalidated rev).

Impact

Arbitrary file read at the privilege of the user running DeepSeek-TUI, via malicious repository content combined with prompt injection (the threat model already documented in CVE-2026-45311).

Reachable as the invoking user:

  • ~/.ssh/id_rsa, ~/.ssh/id_ed25519, and other private keys
  • ~/.aws/credentials, ~/.config/gh/hosts.yml, ~/.netrc
  • .env files anywhere in the filesystem
  • Any project file outside the workspace the tool would normally restrict to

The leaked contents land in the model's context. The same model that obeyed the prompt-injection in step one can be instructed to forward the leak via fetch_url (network-policy permitting), summarize it in chat, or write it into a tool output the attacker can later retrieve.

Technical Details

Root Cause

crates/tui/src/tools/git_history.rs:

// L314-316
fn approval_requirement(&self) -> ApprovalRequirement {
    ApprovalRequirement::Auto
}

// L322-358 (excerpt)
async fn execute(&self, input: Value, context: &ToolContext) -> Result<ToolResult, ToolError> {
    let path_str = required_str(&input, "path")?;
    let resolved_path = context.resolve_path(path_str)?;   // path is bounded to workspace
    ...
    let rev = optional_str(&input, "rev").unwrap_or("HEAD");   // rev is NOT bounded
    ...
    let mut args = vec![
        "blame".to_string(),
        "--date=iso".to_string(),
        format!("-L{start_line},{end_line}"),
    ];
    if porcelain { args.push("--line-porcelain".to_string()); }
    args.push(rev.to_string());            // unvalidated, no sentinel
    args.push("--".to_string());
    args.push(pathspec.display().to_string());
    ...
}

path correctly flows through context.resolve_path, which enforces workspace containment (spec.rs:342). rev does not and the -- separator after rev only ends pathspec parsing, it does not stop option parsing of rev itself.

The JSON schema for rev (L283-285) is {"type": "string"} with no constraints.

Why --contents Works

git blame --contents=<file> -- <pathspec> blames the working tree path as if its contents were the supplied file. Each line of the supplied file appears verbatim in the porcelain or human-readable output, prefixed with the attribution marker 00000000 (External file (--contents) <date> N). The full line content is preserved.

Two secondary primitives in the same parser also leak data, with smaller yield:

  • --ignore-revs-file=<file> : surfaces parse errors that disclose partial content when the file is not a valid revs list.
  • -S <file>, --reverse <rev1>..<rev2> : not directly exploitable for read but expand the option surface that argv injection can reach.

Proof of Concept

Argv assembled by the tool with input {"path": "a.txt", "rev": "--contents=/home/a/.ssh/id_rsa"}:

git blame --date=iso -L1,200 --contents=/home/a/.ssh/id_rsa -- a.txt

Reproduced against system git as a non-root user:

$ id
uid=1001(a) gid=1001(a) groups=1001(a)

$ echo "PRIVATEKEYDATA" > /home/a/.ssh/id_rsa
$ chmod 600 /home/a/.ssh/id_rsa

$ git blame --date=iso -L1,5 "--contents=/home/a/.ssh/id_rsa" -- a.txt
00000000 (External file (--contents) 2026-05-19 07:05:51 -0400 1) PRIVATEKEYDATA

A non-readable target (/etc/shadow, owned by root with mode 0640) returns Permission denied, confirming the read is bounded by uid as expected; this is not a privilege boundary bypass, it is the desktop user's own filesystem view being exposed past the workspace boundary the tool's path argument otherwise enforces.

End-to-end exploitation is identical to the git_show companion: malicious repo → AGENTS.md injection → model calls git_blame with the crafted rev → auto-approval → leaked content returned in tool output and consumed by the model.

Remediation

Same shape as the git_show fix:

args.push("--end-of-options".to_string());
args.push(rev.to_string());
args.push("--".to_string());
args.push(pathspec.display().to_string());

Plus a leading-hyphen rejection on rev. A regression test should pin both rev = "--contents=/etc/passwd" and rev = "--ignore-revs-file=/etc/passwd" as rejected inputs.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "crates.io",
        "name": "deepseek-tui"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0.3.27"
            },
            {
              "last_affected": "0.8.41"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "package": {
        "ecosystem": "npm",
        "name": "deepseek-tui"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0.3.27"
            },
            {
              "fixed": "0.8.41"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "package": {
        "ecosystem": "crates.io",
        "name": "codewhale-tui"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0.8.41"
            },
            {
              "fixed": "0.8.64"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "package": {
        "ecosystem": "npm",
        "name": "codewhale"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0.8.41"
            },
            {
              "fixed": "0.8.64"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-75912"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-200",
      "CWE-88"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-09-04T18:11:45Z",
    "nvd_published_at": "2026-08-18T16:18:23Z",
    "severity": "HIGH"
  },
  "details": "### Maintainer resolution\n\nThe CodeWhale maintainers validated this report. The affected package ranges are recorded in the advisory metadata. Version 0.8.64 contains the fix in commit 9a34b5034d29f05d1f28fa61b04719ca6a741020. Users should upgrade to 0.8.64 or later. The original reporter analysis is preserved below.\n\n# Argument Injection in `git_blame` Tool Allows Arbitrary File Read Without Approval\n\n## Overview\n\nThe `git_blame` tool in DeepSeek-TUI passes the model-supplied `rev` parameter unvalidated into the argv of `git blame`. `git blame` accepts `--contents=\u003cfile\u003e`, which causes it to use the file\u0027s contents in place of the working tree and echo each line verbatim in the blame output. A `rev` value of `--contents=/path/to/secret` therefore exfiltrates the targeted file\u0027s contents into the tool result, which is returned to the model and displayed in the chat transcript.\n\nThe tool is registered with `ApprovalRequirement::Auto` and declares `ToolCapability::ReadOnly`. The read is in-scope for the capability label, but the *target* of the read is not the user expects `git_blame` to read files inside the workspace, not arbitrary paths on the host.\n\nThis is a sibling of the `git_show` argument-injection vulnerability filed separately, sharing the same root cause (missing `--end-of-options` sentinel and unvalidated `rev`).\n\n## Impact\n\nArbitrary file read at the privilege of the user running DeepSeek-TUI, via malicious repository content combined with prompt injection (the threat model already documented in CVE-2026-45311).\n\nReachable as the invoking user:\n\n- `~/.ssh/id_rsa`, `~/.ssh/id_ed25519`, and other private keys\n- `~/.aws/credentials`, `~/.config/gh/hosts.yml`, `~/.netrc`\n- `.env` files anywhere in the filesystem\n- Any project file outside the workspace the tool would normally restrict to\n\nThe leaked contents land in the model\u0027s context. The same model that obeyed the prompt-injection in step one can be instructed to forward the leak via `fetch_url` (network-policy permitting), summarize it in chat, or write it into a tool output the attacker can later retrieve.\n\n## Technical Details\n\n### Root Cause\n\n`crates/tui/src/tools/git_history.rs`:\n\n```rust\n// L314-316\nfn approval_requirement(\u0026self) -\u003e ApprovalRequirement {\n    ApprovalRequirement::Auto\n}\n\n// L322-358 (excerpt)\nasync fn execute(\u0026self, input: Value, context: \u0026ToolContext) -\u003e Result\u003cToolResult, ToolError\u003e {\n    let path_str = required_str(\u0026input, \"path\")?;\n    let resolved_path = context.resolve_path(path_str)?;   // path is bounded to workspace\n    ...\n    let rev = optional_str(\u0026input, \"rev\").unwrap_or(\"HEAD\");   // rev is NOT bounded\n    ...\n    let mut args = vec![\n        \"blame\".to_string(),\n        \"--date=iso\".to_string(),\n        format!(\"-L{start_line},{end_line}\"),\n    ];\n    if porcelain { args.push(\"--line-porcelain\".to_string()); }\n    args.push(rev.to_string());            // unvalidated, no sentinel\n    args.push(\"--\".to_string());\n    args.push(pathspec.display().to_string());\n    ...\n}\n```\n\n`path` correctly flows through `context.resolve_path`, which enforces workspace containment (`spec.rs:342`). `rev` does not and the `--` separator after `rev` only ends pathspec parsing, it does not stop option parsing of `rev` itself.\n\nThe JSON schema for `rev` (L283-285) is `{\"type\": \"string\"}` with no constraints.\n\n### Why `--contents` Works\n\n`git blame --contents=\u003cfile\u003e -- \u003cpathspec\u003e` blames the working tree path *as if its contents were the supplied file*. Each line of the supplied file appears verbatim in the porcelain or human-readable output, prefixed with the attribution marker `00000000 (External file (--contents) \u003cdate\u003e N)`. The full line content is preserved.\n\nTwo secondary primitives in the same parser also leak data, with smaller\nyield:\n\n- `--ignore-revs-file=\u003cfile\u003e` : surfaces parse errors that disclose partial content when the file is not a valid revs list.\n- `-S \u003cfile\u003e`, `--reverse \u003crev1\u003e..\u003crev2\u003e` : not directly exploitable for read but expand the option surface that argv injection can reach.\n\n## Proof of Concept\n\nArgv assembled by the tool with input\n`{\"path\": \"a.txt\", \"rev\": \"--contents=/home/a/.ssh/id_rsa\"}`:\n\n```\ngit blame --date=iso -L1,200 --contents=/home/a/.ssh/id_rsa -- a.txt\n```\n\nReproduced against system `git` as a non-root user:\n\n```\n$ id\nuid=1001(a) gid=1001(a) groups=1001(a)\n\n$ echo \"PRIVATEKEYDATA\" \u003e /home/a/.ssh/id_rsa\n$ chmod 600 /home/a/.ssh/id_rsa\n\n$ git blame --date=iso -L1,5 \"--contents=/home/a/.ssh/id_rsa\" -- a.txt\n00000000 (External file (--contents) 2026-05-19 07:05:51 -0400 1) PRIVATEKEYDATA\n```\n\nA non-readable target (`/etc/shadow`, owned by root with mode 0640) returns `Permission denied`, confirming the read is bounded by uid as expected; this is not a privilege boundary bypass, it is the desktop user\u0027s own filesystem view being exposed past the workspace boundary the tool\u0027s `path` argument otherwise enforces.\n\nEnd-to-end exploitation is identical to the `git_show` companion: malicious repo \u2192 `AGENTS.md` injection \u2192 model calls `git_blame` with the crafted `rev` \u2192 auto-approval \u2192 leaked content returned in tool output and consumed by the model.\n\n## Remediation\n\nSame shape as the `git_show` fix:\n\n```rust\nargs.push(\"--end-of-options\".to_string());\nargs.push(rev.to_string());\nargs.push(\"--\".to_string());\nargs.push(pathspec.display().to_string());\n```\n\nPlus a leading-hyphen rejection on `rev`. A regression test should pin both `rev = \"--contents=/etc/passwd\"` and `rev = \"--ignore-revs-file=/etc/passwd\"` as rejected inputs.",
  "id": "GHSA-c6mw-8xh8-gpq6",
  "modified": "2026-09-04T18:11:45Z",
  "published": "2026-09-04T18:11:45Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/Hmbown/CodeWhale/security/advisories/GHSA-c6mw-8xh8-gpq6"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-75912"
    },
    {
      "type": "WEB",
      "url": "https://github.com/Hmbown/CodeWhale/commit/9a34b5034d29f05d1f28fa61b04719ca6a741020"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/Hmbown/CodeWhale"
    },
    {
      "type": "WEB",
      "url": "https://www.vulncheck.com/advisories/codewhale-before-argument-injection-via-git-blame"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:N/A:N",
      "type": "CVSS_V3"
    },
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:H/VI:N/VA:N/SC:H/SI:N/SA:N",
      "type": "CVSS_V4"
    }
  ],
  "summary": "CodeWhale: Argument Injection in `git_blame` Tool Allows Arbitrary File Read Without Approval"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…

Loading…