Skip to content

Security: Inadequate Password Masking in Script Execution Framework Exposes Credentials Across Multiple Log Levels #12005

@YLChen-007

Description

@YLChen-007

Description:

CLOUDSTACK VERSION
4.22.0.0

Summary

The Script.java class provides a command execution framework with password masking capabilities, but the masking logic is critically flawed. It only obscures parameters following specific flags (-y/-z) or certain URI patterns, leaving credentials passed through direct parameter addition (e.g., script.add(username, newPassword)) completely exposed. This results in plaintext password logging at both DEBUG and WARN levels throughout the command execution lifecycle.

Severity

Critical - Systematic password exposure across multiple log levels (DEBUG and WARN) affects all components using the Script framework. WARN-level logging is typically enabled in production, making this vulnerability active in live deployments.


Vulnerability Details


Root Cause Analysis

1. Inadequate Password Masking Logic

The cloud.utils.script.Script#buildCommandLine() method only masks parameters in very specific scenarios:

protected String buildCommandLine(String[] command) {
    StringBuilder builder = new StringBuilder();
    boolean obscureParam = false;
    for (int i = 0; i < command.length; i++) {
        String cmd = command[i];
        
        // Only handles specific URI patterns
        if (sanitizeViCmdParameter(cmd, builder) || 
            sanitizeRbdFileFormatCmdParameter(cmd, builder)) {
            continue;
        }
        
        if (obscureParam) {
            builder.append("******").append(" ");  // ✅ Masks ONLY if flag detected
            obscureParam = false;
        } else {
            builder.append(cmd).append(" ");  // ❌ All other params logged plaintext
        }
        
        // ❌ ONLY triggers masking for -y/-z flags
        if ("-y".equals(cmd) || "-z".equals(cmd)) {
            obscureParam = true;
            _passwordCommand = true;
        }
    }
    return builder.toString();
}

Critical Flaw: The masking logic assumes passwords are always preceded by -y or -z flags. Any other parameter passing pattern bypasses masking entirely.


2. Direct Parameter Addition Bypasses Masking

The CitrixUpdateHostPasswordCommandWrapper uses direct parameter addition:

// CitrixUpdateHostPasswordCommandWrapper.java (lines 34-45)
final Script script = new Script(scriptPath + scriptName, _timeout, logger);
script.add(username, newPassword);  // ❌ No flag, no masking
final String result = script.execute();

The add() method simply appends parameters to the command array:

public void add(String... params) {
    for (String param : params) {
        _command.add(param);  // ❌ Direct addition, no masking metadata
    }
}

Result: When buildCommandLine() processes these parameters, they don't follow -y/-z flags, so they're logged in plaintext.


3. Multiple Logging Points Expose Credentials in com.cloud.utils.script.Script#execute

** Point 1: DEBUG Level Logging **

public String execute(OutputInterpreter interpreter) {
    String[] command = _command.toArray(new String[_command.size()]);
    String commandLine = buildCommandLine(command);
    
    if (_logger.isDebugEnabled() && !avoidLoggingCommand) {
        // ❌ Logs full command with plaintext password
        _logger.debug(String.format("Executing command [%s].", 
                      commandLine.split(KeyStoreUtils.KS_FILENAME)[0]));
    }
    // ...
}

Exposure: Every command execution logs the full command line at DEBUG level unless avoidLoggingCommand is explicitly set.


** Point 2: WARN Level Logging on Timeout **

TimedOutLogger log = new TimedOutLogger(_process);
Task timedoutTask = new Task(log, ir);

_logger.trace(String.format("Running timed out task of process [%s] for command [%s].", 
              processPid, commandLine));

if (!_passwordCommand) {
    // ❌ Logs full command with password at WARN level
    _logger.warn(String.format("Process [%s] for command [%s] timed out. Output is [%s].", 
                 processPid, commandLine, timedoutTask.getResult()));
} else {
    _logger.warn(String.format("Process [%s] for command [%s] timed out.", 
                 processPid, commandLine));
}

Critical Issue: The _passwordCommand flag is only set to true when -y/-z flags are detected. Since CitrixUpdateHostPasswordCommandWrapper doesn't use these flags:

  • _passwordCommand remains false
  • Timeout warnings log the full command with plaintext password at WARN level
  • WARN logs are typically enabled in production

** Point 3: WARN Level Logging on Execution Failure **

_logger.debug(String.format("Exit value of process [%s] for command [%s] is [%s].", 
              processPid, commandLine, _process.exitValue()));

BufferedReader reader = new BufferedReader(new InputStreamReader(_process.getInputStream()), 128);

String error;
if (interpreter != null) {
    error = interpreter.processError(reader);
} else {
    error = String.valueOf(_process.exitValue());
}

// ❌ Logs full command with password at WARN level on failure
_logger.warn(String.format("Process [%s] for command [%s] encountered the error: [%s].", 
             processPid, commandLine, error));

Exposure: Any script execution failure logs the full command at WARN level, including plaintext passwords.


** Point 4: WARN Level Logging on Exception **

} catch (SecurityException ex) {
    // ❌ Logs command with password at WARN level
    _logger.warn(String.format("Exception [%s] occurred. This may be due to an attempt of " +
                 "executing command [%s] as non root.", ex.getMessage(), commandLine), ex);
    return stackTraceAsString(ex);
} catch (Exception ex) {
    // ❌ Logs command with password at WARN level
    _logger.warn(String.format("Exception [%s] occurred when attempting to run command [%s].", 
                 ex.getMessage(), commandLine), ex);
    return stackTraceAsString(ex);
}

Exposure: Any exception during script execution logs the full command at WARN level.


4. Missing avoidLoggingCommand Flag

The Script class provides an avoidLoggingCommand flag to suppress logging:

// Script.java
private boolean avoidLoggingCommand = false;

public void setAvoidLoggingCommand(boolean avoidLoggingCommand) {
    this.avoidLoggingCommand = avoidLoggingCommand;
}

Problem: CitrixUpdateHostPasswordCommandWrapper never calls setAvoidLoggingCommand(true):

// CitrixUpdateHostPasswordCommandWrapper.java
final Script script = new Script(scriptPath + scriptName, _timeout, logger);
script.add(username, newPassword);
// ❌ Missing: script.setAvoidLoggingCommand(true);
final String result = script.execute();

Result: Even the DEBUG-level protection is not applied.


Complete Vulnerability Chain

1. CitrixUpdateHostPasswordCommandWrapper creates Script instance
   ↓
2. Calls script.add(username, newPassword) without -y/-z flags
   ↓
3. Does NOT call script.setAvoidLoggingCommand(true)
   ↓
4. Script.execute() calls buildCommandLine()
   ↓
5. buildCommandLine() doesn't detect -y/-z flags
   ↓ 
6. Password NOT masked in command string
   ↓
7. _passwordCommand remains false
   ↓
8. Password logged at DEBUG level
   ↓
9. On timeout/failure/exception: Password logged at WARN level 

Exposed Sensitive Data

Data Type Exposure Point Log Level Production Impact
New host password DEBUG logging DEBUG If DEBUG enabled
New host password Timeout logging WARN Always in production
New host password Failure logging WARN Always in production
New host password Exception logging WARN Always in production

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions