Skip to content

gitoxide credential helper protocol accepts bare carriage return in URL context

Low
Byron published GHSA-6wr8-3w4h-j9wj Jul 15, 2026

Package

cargo gix-credentials (Rust)

Affected versions

<= 0.38.1

Patched versions

>= 0.38.2

Description

Summary

gitoxide's credential-helper request serialization accepts a bare carriage return in URL context values. A caller-supplied URL reaches the helper line protocol as a url= field, but validation rejects only NUL and LF, so CR-sensitive credential helpers can parse one URL value as multiple helper fields. In a configuration with such a helper, an attacker-controlled URL can select credentials for a trusted host and cause those credentials to be returned to the caller handling the attacker URL.

Details

The public credential lookup constructor stores caller-provided URL bytes directly in the request context: Action::get_for_url() builds Action::Get(Context { url: Some(url.into()), .. }) in gix-credentials/src/helper/mod.rs:85. When the action is sent to a helper, Context::write_to() serializes that optional URL through the generic key/value writer at gix-credentials/src/protocol/context/serde.rs:33; write_key() writes key, =, the raw value, and then LF at gix-credentials/src/protocol/context/serde.rs:14. The validation immediately before serialization rejects NUL and LF in keys and values, but it does not reject \r, as shown in gix-credentials/src/protocol/context/serde.rs:146. helper::raw() then starts the configured helper, writes the serialized action to helper stdin, and later decodes helper stdout into an Outcome in gix-credentials/src/helper/invoke.rs:43 and gix-credentials/src/helper/invoke.rs:29; the process plumbing that exposes stdin/stdout to the external helper is in gix-credentials/src/program/mod.rs:117. The result is the same credential-helper line-protocol shape as CVE-2024-52006: URL-controlled bytes are serialized after an incomplete record-terminator check, leaving bare CR available to helpers whose line reader treats it as a separator.

Reproduction

poc.zip

bash ./poc/run.sh
GITOXIDE_CR_HELPER_INJECTION: host=trusted.example

The fingerprint is printed by the local credential-helper fixture only after it receives an injected host=trusted.example field through gitoxide's helper stdin. A build failure or non-zero exit without that fingerprint would be setup noise or an unrelated failure, not this CR field-injection path.

Impact

An unauthenticated remote attacker needs a workflow where a victim application or user passes an attacker-controlled Git or credential URL to gitoxide for credential lookup. Exploitation also depends on a configured external credential helper that treats bare carriage return as a line terminator and has stored credentials for the injected trusted host; helpers that split only on LF are not demonstrated affected. Under those preconditions, the attacker can supply a URL such as https://evil.example\rhost=trusted.example\rprotocol=https, bypass the LF/NUL-only validation, and cause the helper to return credentials for trusted.example to the gitoxide caller handling evil.example. The local PoC demonstrates credential selection and return to the in-process caller; direct disclosure to the attacker depends on the surrounding application using, logging, or returning that credential in the attacker-controlled workflow.

Suggested fix

diff --git a/gix-credentials/src/protocol/context/serde.rs b/gix-credentials/src/protocol/context/serde.rs
index 18a44c5..7e2aec2 100644
--- a/gix-credentials/src/protocol/context/serde.rs
+++ b/gix-credentials/src/protocol/context/serde.rs
@@ -144,7 +144,13 @@ pub mod decode {
 }
 
 fn validate(key: &str, value: &BStr) -> Result<(), Error> {
-    if key.contains('\0') || key.contains('\n') || value.contains(&0) || value.contains(&b'\n') {
+    if key.contains('\0')
+        || key.contains('\n')
+        || key.contains('\r')
+        || value.contains(&0)
+        || value.contains(&b'\n')
+        || value.contains(&b'\r')
+    {
         return Err(Error::Encoding {
             key: key.to_owned(),
             value: value.to_owned(),

Reported by Team Atlanta.

Severity

Low

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
High
Privileges required
None
User interaction
Required
Scope
Unchanged
Confidentiality
Low
Integrity
None
Availability
None

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:N/A:N

CVE ID

No known CVE

Weaknesses

No CWEs

Credits