diff --git a/DSPythonNet3/DSPythonNet3Evaluator.cs b/DSPythonNet3/DSPythonNet3Evaluator.cs
index e57f073..fd29bd5 100644
--- a/DSPythonNet3/DSPythonNet3Evaluator.cs
+++ b/DSPythonNet3/DSPythonNet3Evaluator.cs
@@ -351,6 +351,8 @@ public static object EvaluatePythonScript(
private static bool isPythonInstalled = false;
///
/// Makes sure Python is installed on the system and its location added to the path.
+ /// Extracts non-pip wheels directly from the embedded resource stream into site-packages,
+ /// adds a zip-slip guard (normalize & validate paths), and uses pip only for pywin32.
/// NOTE: Calling SetupPython multiple times will add the install location to the path many times,
/// potentially causing the environment variable to overflow.
///
@@ -370,21 +372,66 @@ internal static async Task InstallPythonAsync()
Assembly wheelsAssembly = context.LoadFromAssemblyPath(Path.Join(Path.GetDirectoryName(assembly.Location), "DSPythonNet3Wheels.dll"));
+ string sitePkgs = Path.Combine(Python.Included.Installer.EmbeddedPythonHome, "Lib", "site-packages");
+ var normalizedBase = Path.GetFullPath(sitePkgs) + Path.DirectorySeparatorChar;
+
+ Directory.CreateDirectory(sitePkgs);
+
List pipWheelInstall = new List();
- await Task.WhenAll(wheelsAssembly.GetManifestResourceNames().Where(x =>
+
+ // Extract non-pip wheels directly from the resource stream
+ foreach (var resName in wheelsAssembly.GetManifestResourceNames())
{
- bool isWheel = x.Contains(".whl");
- if (isWheel && x.Contains("pywin32-"))
+ bool isWheel = resName.EndsWith(".whl");
+ if (!isWheel) continue;
+
+ if (resName.Contains("pywin32-"))
{
- pipWheelInstall.Add(x);
- return false;
+ pipWheelInstall.Add(resName);
+ continue;
}
- return isWheel;
- }).Select(wheel => Python.Included.Installer.InstallWheel(wheelsAssembly, wheel))).ConfigureAwait(false);
+ using (var stream = wheelsAssembly.GetManifestResourceStream(resName))
+ {
+ if (stream == null || stream.Length == 0)
+ {
+ continue;
+ }
+
+ using (var zip = new System.IO.Compression.ZipArchive(stream, System.IO.Compression.ZipArchiveMode.Read, false))
+ {
+ foreach (var entry in zip.Entries)
+ {
+ if (string.IsNullOrEmpty(entry.Name)) continue;
+
+ var relPath = entry.FullName.Replace('/', Path.DirectorySeparatorChar);
+ var tentative = Path.Combine(sitePkgs, relPath);
+ var destPath = Path.GetFullPath(tentative);
+
+ if (!destPath.StartsWith(normalizedBase, StringComparison.OrdinalIgnoreCase))
+ {
+ dynamoLogger?.LogWarning($"[PyInit] Skipped suspicious wheel entry: {entry.FullName}", WarningLevel.Mild);
+ continue;
+ }
+
+ var destDir = Path.GetDirectoryName(destPath);
+ if (!string.IsNullOrEmpty(destDir))
+ {
+ Directory.CreateDirectory(destDir);
+ }
+
+ using (var inStream = entry.Open())
+ using (var outStream = new FileStream(destPath, FileMode.Create, FileAccess.Write, FileShare.None))
+ {
+ await inStream.CopyToAsync(outStream).ConfigureAwait(false);
+ }
+ }
+ }
+ }
+ }
foreach (var pipWheelResource in pipWheelInstall)
- {
+ {
var pipWheelName = pipWheelResource.Remove(0, "DSPythonNet3Wheels.Resources.".Count());
string wheelPath = Path.Combine(Python.Included.Installer.EmbeddedPythonHome, "Lib", pipWheelName);
using (Stream? stream = wheelsAssembly.GetManifestResourceStream(pipWheelResource))