CVE-2025-0495: Docker Buildx Credential Leak via OpenTelemetry Traces

Executive Summary

CVE-2025-0495 is a medium-severity vulnerability affecting Docker Buildx, a Docker CLI plugin that extends build capabilities using BuildKit. The vulnerability stems from the potential leakage of sensitive credentials, specifically authentication tokens for cache backends, into OpenTelemetry traces. When users configure cache backends by directly setting secrets as attribute values in cache-to/cache-from configurations via CLI arguments, these secrets can be inadvertently captured within OpenTelemetry traces. These traces, including the command-line arguments, are then stored in the BuildKit daemon's history records, potentially exposing the credentials to unauthorized access. This vulnerability does not affect secrets passed to the GitHub cache backend via environment variables or registry authentication. The issue has been resolved in Buildx v0.21.3 and newer.

Technical Details

  • Affected Systems: Systems using Docker Buildx.
  • Affected Software Versions: Docker Buildx versions prior to v0.21.3.
  • Component: OpenTelemetry tracing within Docker Buildx, specifically the tracing of CLI commands.
  • Vulnerability Class: CWE-532: Insertion of Sensitive Information into Log File.
  • Attack Vector: Local. An attacker needs local access to the BuildKit daemon's history records or the OpenTelemetry collector to potentially extract the leaked credentials.
  • Impact: Exposure of sensitive credentials (cache backend authentication tokens), potentially leading to unauthorized access to the cache backend and related resources.

The vulnerability arises because Buildx, in its earlier versions, was configured to trace the entire command-line invocation, including arguments, when OpenTelemetry tracing was enabled. This meant that if a user supplied a secret token directly as a command-line argument (e.g., --cache-to=type=registry,ref=my-registry,password=<secret_token>), the token would be included in the trace data.

Root Cause Analysis

The root cause of CVE-2025-0495 lies in the way Docker Buildx was capturing and storing OpenTelemetry traces. Specifically, the TraceCurrentCommand function in the util/tracing/trace.go file was capturing the entire command-line string, including all arguments, and storing it as an attribute in the OpenTelemetry span.

Before the patch, the TraceCurrentCommand function looked like this:

// util/tracing/trace.go (before patch)
func TraceCurrentCommand(ctx context.Context, name string) (context.Context, func(error), error) {
	opts := []sdktrace.TracerProviderOption{
		sdktrace.WithResource(detect.Resource()),
		sdktrace.WithBatcher(delegated.DefaultExporter),
	}

	tp := sdktrace.NewTracerProvider(opts...)
	ctx, span := tp.Tracer("").Start(ctx, name, trace.WithAttributes(
		attribute.String("command", strings.Join(os.Args, " ")),
	))

	return ctx, func(err error) {
		span.End()
		if err != nil {
			span.RecordError(err)
		}
		if err := tp.Shutdown(context.TODO()); err != nil {
			fmt.Println(err)
		}
	}, nil
}

As you can see, strings.Join(os.Args, " ") captures the entire command line, including any sensitive information passed as arguments. This entire string was then stored as the command attribute of the OpenTelemetry span.

The problem is exacerbated by the fact that OpenTelemetry traces are stored in two locations:

  1. BuildKit Daemon's History Records: BuildKit, the underlying build engine used by Buildx, stores a history of build operations, including OpenTelemetry traces. Access to these history records could allow an attacker to retrieve the leaked credentials.
  2. Custom OpenTelemetry Collector: If users configure a custom OpenTelemetry collector, the traces are sent to that collector. If the collector is not properly secured, the leaked credentials could be exposed.

Patch Analysis

The fix for CVE-2025-0495 involves modifying the TraceCurrentCommand function to avoid capturing the raw command-line arguments. Instead, the function now captures only the command name and specific, known configuration values as attributes.

The patched TraceCurrentCommand function in util/tracing/trace.go looks like this:

// util/tracing/trace.go (after patch)
func TraceCurrentCommand(ctx context.Context, args []string, attrs ...attribute.KeyValue) (context.Context, func(error), error) {
	opts := []sdktrace.TracerProviderOption{
		sdktrace.WithResource(detect.Resource()),
		sdktrace.WithBatcher(delegated.DefaultExporter),
	}

	tp := sdktrace.NewTracerProvider(opts...)
	ctx, span := tp.Tracer("").Start(ctx, strings.Join(args, " "), trace.WithAttributes(
		attrs...,
	))

	return ctx, func(err error) {
		span.End()
		if err != nil {
			span.RecordError(err)
		}
		if err := tp.Shutdown(context.TODO()); err != nil {
			fmt.Println(err)
		}
	}, nil
}

The key changes are:

  • os.Args is no longer used: The function no longer captures the entire command line using strings.Join(os.Args, " ").
  • args parameter: The function now accepts a slice of strings (args) representing the command and its primary arguments. This allows the caller to specify which parts of the command line should be included in the trace.
  • attrs parameter: The function also accepts a variable number of attribute.KeyValue pairs (attrs). This allows the caller to specify specific configuration values to be included as attributes in the trace.

The changes in commands/bake.go and commands/build.go demonstrate how the new TraceCurrentCommand function is used:

File: commands/bake.go
@@ -66,7 +66,11 @@ type bakeOptions struct {
 func runBake(ctx context.Context, dockerCli command.Cli, targets []string, in bakeOptions, cFlags commonFlags) (err error) {
 	tmp := dockerCli.MeterProvider()

-	ctx, end, err := tracing.TraceCurrentCommand(ctx, "bake")
+	ctx, end, err := tracing.TraceCurrentCommand(ctx, append([]string{"bake"}, targets...),
+		attribute.String("builder", in.builder),
+		attribute.StringSlice("targets", targets),
+		attribute.StringSlice("files", in.files),
+	)
 	if err != nil {
 		return err
 	}

In commands/bake.go, instead of tracing the entire "bake" command, the code now traces the command name ("bake") along with the target names, builder name, and files used. The append([]string{"bake"}, targets...) creates a new slice containing the string "bake" followed by all the elements of the targets slice. This ensures that the command name and target names are included in the trace, but sensitive information passed as flags is excluded. The attribute.String and attribute.StringSlice functions create OpenTelemetry attributes with the specified keys and values.

File: commands/build.go
@@ -285,7 +285,11 @@ func (o *buildOptionsHash) String() string {
 func runBuild(ctx context.Context, dockerCli command.Cli, options buildOptions) (err error) {
 	tmp := dockerCli.MeterProvider()

-	ctx, end, err := tracing.TraceCurrentCommand(ctx, "build")
+	ctx, end, err := tracing.TraceCurrentCommand(ctx, []string{"build", options.contextPath},
+		attribute.String("builder", options.builder),
+		attribute.String("context", options.contextPath),
+		attribute.String("dockerfile", options.dockerfileName),
+	)
 	if err != nil {
 		return err
 	}

Similarly, in commands/build.go, the code now traces the "build" command along with the context path, builder name, and Dockerfile name. Again, sensitive information passed as flags is excluded.

These changes ensure that sensitive credentials passed as command-line arguments are no longer captured in OpenTelemetry traces, mitigating the vulnerability.

Exploitation Techniques

An attacker with access to the BuildKit daemon's history records or a misconfigured OpenTelemetry collector could potentially exploit this vulnerability. The exploitation process would involve the following steps:

  1. Gain Access: The attacker needs to gain access to either the BuildKit daemon's history records (typically stored on the file system) or the OpenTelemetry collector. This could be achieved through various means, such as exploiting a separate vulnerability, social engineering, or insider access.
  2. Analyze Traces: The attacker would then need to analyze the stored OpenTelemetry traces to identify commands that used the cache-to or cache-from options with secrets passed directly as attribute values.
  3. Extract Credentials: Once a vulnerable command is identified, the attacker can extract the leaked credentials from the trace data.
  4. Abuse Credentials: With the extracted credentials, the attacker can then authenticate to the cache backend and potentially access or modify cached data, or even pivot to other systems.

Example Attack Scenario:

Let's assume a user executes the following command:

docker buildx build --cache-to=type=registry,ref=my-registry,username=myuser,password=supersecret .

In vulnerable versions of Buildx, the OpenTelemetry trace would contain the entire command line, including the password=supersecret part. An attacker with access to the traces could then extract the supersecret password and use it to authenticate to the my-registry registry.

PoC (Theoretical):

Since the vulnerability lies in the logging of the command, a direct exploit isn't a program to run, but rather a process of accessing the logs. This is a theoretical demonstration of how an attacker might extract the credentials, assuming they have access to the BuildKit history:

  1. Access BuildKit History: The attacker gains access to the BuildKit's local state directory. The location depends on the BuildKit configuration, but a common location is /var/lib/buildkit/.
  2. Locate Trace Files: Within the BuildKit directory, the attacker searches for files containing OpenTelemetry trace data. These files might be in a specific format (e.g., JSON) and could be located in subdirectories related to build history.
  3. Parse Trace Data: The attacker uses a script (e.g., Python) to parse the trace data and extract the command-line arguments.
# This is a theoretical PoC and requires adaptation to the specific BuildKit trace format.
import json

def extract_credentials(trace_file):
    try:
        with open(trace_file, 'r') as f:
            trace_data = json.load(f)
            # Assuming the trace data contains a "command" field with the full command line.
            command = trace_data.get("command", "")
            if "cache-to" in command and "password=" in command:
                start_index = command.find("password=") + len("password=")
                end_index = command.find(" ", start_index)  # Find the next space
                if end_index == -1:
                    end_index = len(command)
                password = command[start_index:end_index]
                print(f"Found potential password: {password}")
                return password
            else:
                print("No potential password found in this trace.")
                return None
    except FileNotFoundError:
        print(f"Error: Trace file not found: {trace_file}")
        return None
    except json.JSONDecodeError:
        print(f"Error: Invalid JSON format in trace file: {trace_file}")
        return None

# Example usage:
trace_file = "/path/to/buildkit/trace/file.json"  # Replace with the actual path
password = extract_credentials(trace_file)

if password:
    print(f"Extracted password: {password}")
    # Now the attacker can use the password to authenticate to the registry.
else:
    print("No password found in the trace file.")

Real-World Impacts:

The real-world impact of this vulnerability could be significant, especially in environments where Buildx is used to build and deploy sensitive applications. If an attacker gains access to the cache backend credentials, they could:

  • Access Sensitive Data: Access cached layers containing proprietary code, configuration files, or other sensitive data.
  • Modify Cached Data: Inject malicious code into cached layers, potentially compromising future builds.
  • Supply Chain Attacks: Compromise the entire build pipeline, leading to the distribution of malicious software to end-users.

Mitigation Strategies

To mitigate the risk of CVE-2025-0495, the following strategies are recommended:

  1. Upgrade Buildx: Upgrade to Buildx v0.21.3 or newer. This version contains the fix for the vulnerability.

  2. Avoid Passing Secrets as CLI Arguments: The most effective mitigation is to avoid passing cache backend credentials directly as command-line arguments. Instead, use environment variables or registry authentication. For example, instead of:

    docker buildx build --cache-to=type=registry,ref=my-registry,username=myuser,password=supersecret .
    

    Use:

    export DOCKER_REGISTRY_USER=myuser
    export DOCKER_REGISTRY_PASSWORD=supersecret
    docker buildx build --cache-to=type=registry,ref=my-registry .
    

    Or configure registry authentication using docker login.

  3. Secure OpenTelemetry Collector: If you are using a custom OpenTelemetry collector, ensure that it is properly secured. Restrict access to the collector and encrypt the data in transit and at rest.

  4. Restrict Access to BuildKit History Records: Limit access to the BuildKit daemon's history records to authorized personnel only. Regularly audit access logs to detect any suspicious activity.

  5. Regular Security Audits: Conduct regular security audits of your Docker Buildx configuration and usage to identify and address any potential vulnerabilities.

  6. Implement Least Privilege: Ensure that users and processes have only the minimum necessary privileges to perform their tasks. This can help to limit the impact of a successful attack.

Timeline of Discovery and Disclosure

  • Vulnerability Discovered: Unknown.
  • Reported to Docker: Unknown.
  • Patch Released: Buildx v0.21.3 (Date Unknown, but prior to March 17, 2025).
  • CVE Assigned: CVE-2025-0495 (March 17, 2025).
  • Public Disclosure: March 17, 2025.

References

Comparative Analysis

While CVE-2025-0495 is specific to Docker Buildx and OpenTelemetry tracing, it shares similarities with other vulnerabilities that involve the leakage of sensitive information into log files or other diagnostic data. For example, many web applications have been vulnerable to similar issues, where user input containing passwords or API keys is inadvertently logged.

The evolution of security practices has led to increased awareness of these types of vulnerabilities. Modern frameworks and libraries often provide built-in mechanisms for sanitizing or masking sensitive data before it is logged. However, as CVE-2025-0495 demonstrates, these vulnerabilities can still occur if developers are not careful about how they handle sensitive information.

The fix for CVE-2025-0495, which involves avoiding the capture of raw command-line arguments and instead capturing only specific, known configuration values, is a good example of a defense-in-depth approach. By limiting the amount of sensitive information that is captured in the first place, the risk of leakage is significantly reduced.

Read more