Summary
When uv is absent from the worker PATH, PythonDependenciesResolver downloads https://astral.sh/uv/install.sh at task runtime and immediately executes it with sh. No checksum or cryptographic signature is verified before execution. A compromised CDN, poisoned DNS, or MITM on an unprotected network path could substitute arbitrary shell code that runs with full Kestra worker OS permissions.
Severity
HIGH — exploitable via external supply chain compromise; no local attacker access required; execution scope is the full Kestra worker process.
Standards
OWASP A03:2025 (Software Supply Chain Failures), OWASP A08:2025 (Software and Data Integrity Failures), SLSA
Affected Code
File: plugin-script-python/src/main/java/io/kestra/plugin/scripts/python/internals/PythonDependenciesResolver.java (lines 292–306)
// Triggered when `uv` binary is not detected on the worker
script = workingDir.createFile("install-uv.sh");
try (InputStream in = URI.create("https://astral.sh/uv/install.sh").toURL().openStream()) {
Files.copy(in, script, StandardCopyOption.REPLACE_EXISTING);
}
// No hash verification here ↓
execCommandAndGetStdOut(List.of("chmod", "+x", script.toString()));
execCommandAndGetStdOut(List.of("sh", script.toString()), builder -> {
Map<String, String> env = builder.environment();
env.clear();
env.put("HOME", HOME_ENV);
env.put("PATH", PATH_ENV);
env.put("UV_INSTALL_DIR", workingDir.path().toString());
env.put("UV_NO_MODIFY_PATH", "true");
return builder;
});
Threat Scenario
An attacker who compromises the astral.sh CDN, intercepts DNS for astral.sh, or performs a MITM on a worker running in a network with TLS inspection can serve a modified install.sh. The script executes with the worker's OS identity — enabling exfiltration of secrets from the worker environment, lateral movement to services reachable from the worker, or silent corruption of task outputs.
Remediation
Preferred (eliminate the download path):
- Pre-install
uv in the Docker image used by the Python task runner. The download path is never triggered, removing the attack surface entirely.
If runtime download must be retained:
- Pin the expected SHA-256 of the installer as a compile-time constant.
- After download, verify:
MessageDigest.getInstance("SHA-256") → compare hex → abort if mismatch.
- Log the hash and installer URL to the task log for auditability.
- Expose the expected hash as a configuration property so operators can update it without a code change.
// Fixed pattern
byte[] downloaded = Files.readAllBytes(script);
byte[] actual = MessageDigest.getInstance("SHA-256").digest(downloaded);
String actualHex = HexFormat.of().formatHex(actual);
if (!EXPECTED_UV_INSTALLER_SHA256.equals(actualHex)) {
throw new KestraRuntimeException(
"uv installer integrity check failed: expected " + EXPECTED_UV_INSTALLER_SHA256
+ " got " + actualHex);
}
Acceptance Criteria
Fix
Kestra Plugin Coding Standards
References
Part of security audit EPIC: #388
View as Artifact
Summary
When
uvis absent from the worker PATH,PythonDependenciesResolverdownloadshttps://astral.sh/uv/install.shat task runtime and immediately executes it withsh. No checksum or cryptographic signature is verified before execution. A compromised CDN, poisoned DNS, or MITM on an unprotected network path could substitute arbitrary shell code that runs with full Kestra worker OS permissions.Severity
HIGH — exploitable via external supply chain compromise; no local attacker access required; execution scope is the full Kestra worker process.
Standards
OWASP A03:2025 (Software Supply Chain Failures), OWASP A08:2025 (Software and Data Integrity Failures), SLSA
Affected Code
File:
plugin-script-python/src/main/java/io/kestra/plugin/scripts/python/internals/PythonDependenciesResolver.java(lines 292–306)Threat Scenario
An attacker who compromises the astral.sh CDN, intercepts DNS for astral.sh, or performs a MITM on a worker running in a network with TLS inspection can serve a modified
install.sh. The script executes with the worker's OS identity — enabling exfiltration of secrets from the worker environment, lateral movement to services reachable from the worker, or silent corruption of task outputs.Remediation
Preferred (eliminate the download path):
uvin the Docker image used by the Python task runner. The download path is never triggered, removing the attack surface entirely.If runtime download must be retained:
MessageDigest.getInstance("SHA-256")→ compare hex → abort if mismatch.Acceptance Criteria
Fix
Kestra Plugin Coding Standards
Property<T>— no legacy@PluginProperty(dynamic = true)on new code@PluginProperty(secret = true)runContext.logger()only — no sensitive data in log statements./gradlew buildReferences
Part of security audit EPIC: #388
View as Artifact