diff --git a/source/Calamari.Common/Features/Scripting/Bash/BashScriptBootstrapper.cs b/source/Calamari.Common/Features/Scripting/Bash/BashScriptBootstrapper.cs index faf4b541b..fe1e87cb1 100644 --- a/source/Calamari.Common/Features/Scripting/Bash/BashScriptBootstrapper.cs +++ b/source/Calamari.Common/Features/Scripting/Bash/BashScriptBootstrapper.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -37,22 +37,37 @@ public static string FormatCommandArguments(string bootstrapFile) return commandArguments.ToString(); } - public static string PrepareConfigurationFile(string workingDirectory, IVariables variables) + public static string PrepareConfigurationFile(string workingDirectory, IVariables variables, Script script) { var configurationFile = Path.Combine(workingDirectory, "Configure." + Guid.NewGuid().ToString().Substring(10) + ".sh"); var builder = new StringBuilder(BootstrapScriptTemplate); - var encryptedVariables = EncryptVariables(variables); - var variableString = GetEncryptedVariablesKvp(variables); + var featureEnabled = FeatureToggle.BashParametersArrayFeatureToggle.IsEnabled(variables); + builder.Replace("#### BashParametersArrayFeatureToggle ####", featureEnabled ? "true" : "false"); + var encryptedVariables = EncryptVariables(variables); builder.Replace("#### VariableDeclarations ####", string.Join(LinuxNewLine, GetVariableSwitchConditions(encryptedVariables))); - - builder.Replace("#### BashParametersArrayFeatureToggle ####", FeatureToggle.BashParametersArrayFeatureToggle.IsEnabled(variables) ? "true" : "false"); - if (FeatureToggle.BashParametersArrayFeatureToggle.IsEnabled(variables)) + if (featureEnabled) { - builder.Replace("#### VARIABLESTRING.IV ####", variableString.iv); - builder.Replace("#### VARIABLESTRING.ENCRYPTED ####", variableString.encrypted); + var scriptUsesOctopusParameters = ScriptUsesOctopusParameters(script, variables); + // If the script doesn't use octopus_parameters at all, we don't want to bloat the bootstrap script with unused values. + if (scriptUsesOctopusParameters) + { + var variableString = GetEncryptedVariablesKvp(variables); + builder.Replace("#### VARIABLESTRING.IV ####", variableString.iv); + builder.Replace("#### VARIABLESTRING.ENCRYPTED ####", variableString.encrypted); + builder.Replace("#### SCRIPT_USES_OCTOPUS_PARAMETERS ####", "true"); + } + else + { + builder.Replace("#### SCRIPT_USES_OCTOPUS_PARAMETERS ####", "false"); + } + } + else + { + // Feature toggle is off, so the placeholder won't be used but still needs to be replaced + builder.Replace("#### SCRIPT_USES_OCTOPUS_PARAMETERS ####", "false"); } using (var file = new FileStream(configurationFile, FileMode.CreateNew, FileAccess.Write)) @@ -65,13 +80,30 @@ public static string PrepareConfigurationFile(string workingDirectory, IVariable File.SetAttributes(configurationFile, FileAttributes.Hidden); return configurationFile; } - - static string EncodeAsHex(string value) + + static bool ScriptUsesOctopusParameters(Script script, IVariables variables) { - var bytes = Encoding.UTF8.GetBytes(value); - return BitConverter.ToString(bytes).Replace("-", ""); + // Check if script actually uses octopus_parameters array access + // Pattern matches: octopus_parameters[key], octopus_parameters[@], etc. + const string pattern = @"octopus_parameters\["; + var regex = new System.Text.RegularExpressions.Regex(pattern); + + if (File.Exists(script.File) && regex.IsMatch(File.ReadAllText(script.File))) + return true; + + foreach (var variableName in variables.GetNames().Where(ScriptVariables.IsLibraryScriptModule)) + { + if (ScriptVariables.GetLibraryScriptModuleLanguage(variables, variableName) == ScriptSyntax.Bash) + { + var contents = variables.Get(variableName); + if (contents != null && regex.IsMatch(contents)) + return true; + } + } + + return false; } - + static (string encrypted, string iv) GetEncryptedVariablesKvp(IVariables variables) { var sb = new StringBuilder(); @@ -79,8 +111,8 @@ static string EncodeAsHex(string value) .Where(v => !ScriptVariables.IsLibraryScriptModule(v.Key)) .Where(v => !ScriptVariables.IsBuildInformationVariable(v.Key))) { - var value = variable.Value ?? "nul"; - sb.Append($"{EncodeAsHex(variable.Key)}").Append("$").AppendLine(EncodeAsHex(value)); + sb.Append(variable.Key).Append('\0'); + sb.Append(variable.Value ?? "").Append('\0'); } var encrypted = VariableEncryptor.Encrypt(sb.ToString()); diff --git a/source/Calamari.Common/Features/Scripting/Bash/BashScriptExecutor.cs b/source/Calamari.Common/Features/Scripting/Bash/BashScriptExecutor.cs index 4cff76288..4d69a1480 100644 --- a/source/Calamari.Common/Features/Scripting/Bash/BashScriptExecutor.cs +++ b/source/Calamari.Common/Features/Scripting/Bash/BashScriptExecutor.cs @@ -19,7 +19,7 @@ protected override IEnumerable PrepareExecution(Script script, Dictionary? environmentVars = null) { var workingDirectory = Path.GetDirectoryName(script.File); - var configurationFile = BashScriptBootstrapper.PrepareConfigurationFile(workingDirectory, variables); + var configurationFile = BashScriptBootstrapper.PrepareConfigurationFile(workingDirectory, variables, script); var (bootstrapFile, otherTemporaryFiles) = BashScriptBootstrapper.PrepareBootstrapFile(script, configurationFile, workingDirectory, variables); var invocation = new CommandLineInvocation( diff --git a/source/Calamari.Common/Features/Scripting/Bash/Bootstrap.sh b/source/Calamari.Common/Features/Scripting/Bash/Bootstrap.sh index 4c4c58704..2e1400ac5 100644 --- a/source/Calamari.Common/Features/Scripting/Bash/Bootstrap.sh +++ b/source/Calamari.Common/Features/Scripting/Bash/Bootstrap.sh @@ -264,42 +264,27 @@ function decrypt_and_parse_variables { local encrypted="$1" local iv="$2" - local decrypted - decrypted=$(decrypt_variable "$encrypted" "$iv") declare -gA octopus_parameters=() - local -a key_byte_lengths=() - local -a value_byte_lengths=() - local -a hex_parts=() + # Decrypt the inline base64-encoded ciphertext and read NUL-delimited + # key\0value\0 pairs via mapfile in one shot. + local -a _kv + mapfile -d "" _kv < <(echo "$encrypted" | openssl enc -aes-256-cbc -d -a -A -nosalt -K "$sensitiveVariableKey" -iv "$iv") - while IFS='$' read -r hex_key hex_value; do - hex_value="${hex_value//$'\n'/}" - key_byte_lengths+=( $(( ${#hex_key} / 2 )) ) - value_byte_lengths+=( $(( ${#hex_value} / 2 )) ) - hex_parts+=( "${hex_key}${hex_value}" ) - done <<< "$decrypted" - - local concatenated_hex - concatenated_hex=$(printf "%s" "${hex_parts[@]}") - - exec 3< <(echo -n "$concatenated_hex" | xxd -r -p) - - local idx - for idx in "${!key_byte_lengths[@]}"; do - local key_byte_len="${key_byte_lengths[idx]}" - local value_byte_len="${value_byte_lengths[idx]}" - local decoded_key decoded_value - - LC_ALL=C read -r -N "$key_byte_len" decoded_key <&3 - LC_ALL=C read -r -N "$value_byte_len" decoded_value <&3 - - [[ "$decoded_value" == "nul" ]] && decoded_value="" - if [[ -n "$decoded_key" ]]; then - octopus_parameters["$decoded_key"]="$decoded_value" - fi + local i + for (( i = 0; i < ${#_kv[@]}; i += 2 )); do + [[ -n "${_kv[i]}" ]] && octopus_parameters["${_kv[i]}"]="${_kv[i+1]:-}" done +} - exec 3<&- +function _ensure_octopus_parameters_loaded { + # Load octopus_parameters if feature toggle is enabled and Bash version supports it + bashParametersArrayFeatureToggle=#### BashParametersArrayFeatureToggle #### + if [[ "$bashParametersArrayFeatureToggle" == "true" ]]; then + if (( ${BASH_VERSINFO[0]:-0} > 4 || (${BASH_VERSINFO[0]:-0} == 4 && ${BASH_VERSINFO[1]:-0} > 2) )); then + decrypt_and_parse_variables "#### VARIABLESTRING.ENCRYPTED ####" "#### VARIABLESTRING.IV ####" + fi + fi } # ----------------------------------------------------------------------------- @@ -366,16 +351,9 @@ function report_kubernetes_manifest_file report_kubernetes_manifest "$MANIFEST" "$NAMESPACE" } -bashParametersArrayFeatureToggle=#### BashParametersArrayFeatureToggle #### +scriptUsesOctopusParameters=#### SCRIPT_USES_OCTOPUS_PARAMETERS #### -if [ "$bashParametersArrayFeatureToggle" = true ]; then - if (( ${BASH_VERSINFO[0]:-0} > 4 || (${BASH_VERSINFO[0]:-0} == 4 && ${BASH_VERSINFO[1]:-0} > 2) )); then - if command -v xxd > /dev/null; then - decrypt_and_parse_variables "#### VARIABLESTRING.ENCRYPTED ####" "#### VARIABLESTRING.IV ####" - else - echo "xxd is not installed, this is required to use octopus_parameters" - fi - else - echo "Bash version 4.2 or later is required to use octopus_parameters" - fi +# Eagerly load octopus_parameters only if the user script actually uses it +if [ "$scriptUsesOctopusParameters" = true ]; then + _ensure_octopus_parameters_loaded fi diff --git a/source/Calamari.Tests/Fixtures/Bash/BashFixture.cs b/source/Calamari.Tests/Fixtures/Bash/BashFixture.cs index e54c051a8..5cd47e718 100644 --- a/source/Calamari.Tests/Fixtures/Bash/BashFixture.cs +++ b/source/Calamari.Tests/Fixtures/Bash/BashFixture.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Text; @@ -335,6 +335,7 @@ public void ShouldSupportStrictVariableUnset(FeatureToggle? featureToggle) static string specialCharacters => "! \" # $ % & ' ( ) * + , - . / : ; < = > ? @ [ \\ ] ^ _ ` { | } ~ \n\u00b1 \u00d7 \u00f7 \u2211 \u220f \u2202 \u221e \u222b \u2248 \u2260 \u2264 \u2265 \u221a \u221b \u2206 \u2207 \u221d \n$ \u00a2 \u00a3 \u00a5 \u20ac \u20b9 \u20a9 \u20b1 \u20aa \u20bf \n• ‣ … ′ ″ ‘ ’ “ ” ‽ ¡ ¿ – — ― \n( ) [ ] { } ⟨ ⟩ « » ‘ ’ “ ” \n\u2190 \u2191 \u2192 \u2193 \u2194 \u2195 \u2196 \u2197 \u2198 \u2199 \u2b05 \u2b06 \u2b07 \u27a1 \u27f3 \nα β γ δ ε ζ η θ ι κ λ μ ν ξ ο π ρ σ τ υ φ χ ψ ω \n\u00a9 \u00ae \u2122 § ¶ † ‡ µ #"; + [TestCase(FeatureToggle.BashParametersArrayFeatureToggle)] [RequiresBashDotExeIfOnWindows] public void ShouldBeAbleToEnumerateVariableValues(FeatureToggle? featureToggle) { @@ -354,7 +355,29 @@ public void ShouldBeAbleToEnumerateVariableValues(FeatureToggle? featureToggle) ["VariableName \n 11"] = "Value \n 11", ["VariableName.prop.anotherprop 12"] = "Value.prop.12", ["VariableName`prop`anotherprop` 13"] = "Value`prop`13", - [specialCharacters] = specialCharacters + [specialCharacters] = specialCharacters, + // Emoji / 4-byte UTF-8 codepoints + ["EmojiKey 🎉💡🔥"] = "EmojiValue 😀🌍🚀", + // CJK (Chinese / Japanese / Korean) + ["CJK 中文 日本語 한국어"] = "中文值 你好世界", + // Arabic RTL text + ["Arabic مفتاح"] = "قيمة عربية", + // Bash command-injection attempts in both key and value + ["InjectionAttempt $(echo injected)"] = "$(echo injected) `echo injected` ${HOME}", + // Empty value + ["EmptyValueKey"] = "", + // Leading and trailing whitespace in value + ["LeadingTrailingSpaces"] = " value padded with spaces ", + // Multiple '=' signs in value (parsers that split on '=' can mis-handle this) + ["MultipleEquals"] = "a=b=c=d", + // Zero-width space (U+200B) – invisible but load-bearing + ["ZeroWidth\u200bKey"] = "zero\u200bwidth\u200bvalue", + // ANSI escape sequence – terminal control injection attempt + ["AnsiEscapeKey"] = "\x1b[31mRed\x1b[0m", + // Supplementary-plane Unicode (mathematical script + musical symbols) + ["SupplementaryPlane 𝒜𝄞"] = "value 𝐀𝁆", + // Combining diacritical mark (NFD 'é' vs NFC U+00E9) + ["CombiningDiacritical Caf\u0301"] = "Caf\u00e9", }.AddFeatureToggleToDictionary(new List { FeatureToggle.BashParametersArrayFeatureToggle })); output.AssertSuccess(); @@ -383,6 +406,18 @@ public void ShouldBeAbleToEnumerateVariableValues(FeatureToggle? featureToggle) output.AssertOutput("Key: VariableName.prop.anotherprop 12, Value: Value.prop.12"); output.AssertOutput("Key: VariableName`prop`anotherprop` 13, Value: Value`prop`13"); output.AssertOutput($"Key: {specialCharacters}, Value: {specialCharacters}"); + + output.AssertOutput("Key: EmojiKey 🎉💡🔥, Value: EmojiValue 😀🌍🚀"); + output.AssertOutput("Key: CJK 中文 日本語 한국어, Value: 中文值 你好世界"); + output.AssertOutput("Key: Arabic مفتاح, Value: قيمة عربية"); + output.AssertOutput("Key: InjectionAttempt $(echo injected), Value: $(echo injected) `echo injected` ${HOME}"); + output.AssertOutput("Key: EmptyValueKey, Value: "); + output.AssertOutput("Key: LeadingTrailingSpaces, Value: value padded with spaces "); + output.AssertOutput("Key: MultipleEquals, Value: a=b=c=d"); + output.AssertOutput($"Key: ZeroWidth\u200bKey, Value: zero\u200bwidth\u200bvalue"); + output.AssertOutput($"Key: AnsiEscapeKey, Value: \x1b[31mRed\x1b[0m"); + output.AssertOutput($"Key: SupplementaryPlane 𝒜𝄞, Value: value 𝐀𝁆"); + output.AssertOutput($"Key: CombiningDiacritical Caf\u0301, Value: Caf\u00e9"); } }