diff --git a/Common/Python/PythonInitializer.cs b/Common/Python/PythonInitializer.cs index 8810fff1d3c6..cf9f219eb631 100644 --- a/Common/Python/PythonInitializer.cs +++ b/Common/Python/PythonInitializer.cs @@ -140,9 +140,13 @@ public static bool AddPythonPaths(IEnumerable paths) // Insert any pending path additions if (!_pendingPathAdditions.IsNullOrEmpty()) { - var code = string.Join(";", _pendingPathAdditions - .Select(s => $"sys.path.insert({insertionIndex}, '{s}')")).Replace('\\', '/'); - PythonEngine.Exec(code, locals: locals); + // Pass each path as a Python object to avoid code injection via path strings + // containing single quotes or newlines (CWE-94) + foreach (var path in _pendingPathAdditions) + { + using var pyPath = new PyString(path.Replace('\\', '/')); + sys.path.insert(insertionIndex, pyPath); + } _pendingPathAdditions.Clear(); } diff --git a/Optimizer.Launcher/ConsoleLeanOptimizer.cs b/Optimizer.Launcher/ConsoleLeanOptimizer.cs index 768fc1a7628f..19919ddc75a1 100644 --- a/Optimizer.Launcher/ConsoleLeanOptimizer.cs +++ b/Optimizer.Launcher/ConsoleLeanOptimizer.cs @@ -85,14 +85,28 @@ protected override string RunLean(ParameterSet parameterSet, string backtestName var resultDirectory = Path.Combine(_rootResultDirectory, backtestId); Directory.CreateDirectory(resultDirectory); - // Use ProcessStartInfo class + // Use ProcessStartInfo class — use ArgumentList for safe per-argument escaping (CWE-88) var startInfo = new ProcessStartInfo { FileName = _leanLocation, WorkingDirectory = Directory.GetParent(_leanLocation).FullName, - Arguments = $"--results-destination-folder \"{resultDirectory}\" --algorithm-id \"{backtestId}\" --optimization-id \"{optimizationId}\" --parameters {parameterSet} --backtest-name \"{backtestName}\" {_extraLeanArguments}", WindowStyle = ProcessWindowStyle.Minimized }; + startInfo.ArgumentList.Add("--results-destination-folder"); + startInfo.ArgumentList.Add(resultDirectory); + startInfo.ArgumentList.Add("--algorithm-id"); + startInfo.ArgumentList.Add(backtestId); + startInfo.ArgumentList.Add("--optimization-id"); + startInfo.ArgumentList.Add(optimizationId); + startInfo.ArgumentList.Add("--parameters"); + startInfo.ArgumentList.Add(parameterSet.ToString()); + startInfo.ArgumentList.Add("--backtest-name"); + startInfo.ArgumentList.Add(backtestName); + // Append any extra arguments individually — split preserving quoted tokens + foreach (var arg in SplitArguments(_extraLeanArguments)) + { + startInfo.ArgumentList.Add(arg); + } var process = new Process { @@ -154,5 +168,35 @@ protected override void SendUpdate() Log.Trace(message); } } + + private static IEnumerable SplitArguments(string arguments) + { + if (string.IsNullOrWhiteSpace(arguments)) + yield break; + + var current = new System.Text.StringBuilder(); + var inQuotes = false; + foreach (var ch in arguments) + { + if (ch == '"') + { + inQuotes = !inQuotes; + } + else if (ch == ' ' && !inQuotes) + { + if (current.Length > 0) + { + yield return current.ToString(); + current.Clear(); + } + } + else + { + current.Append(ch); + } + } + if (current.Length > 0) + yield return current.ToString(); + } } }