diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NpmPackagesUpdater.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NpmPackagesUpdater.cs index 9f2cb4d2212..9b7340fdd7d 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NpmPackagesUpdater.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NpmPackagesUpdater.cs @@ -359,18 +359,38 @@ protected virtual List GetAbpPackagesFromPackageJson(JObject fileObje var properties = dependencies.Properties().ToList(); - abpPackages - .AddRange( - properties.Where( - p => p.Name.StartsWith("@abp/") - || p.Name.StartsWith("@volo/") - || p.Name.StartsWith("@volosoft/")).ToList() - ); + foreach (var p in properties.Where( + p => p.Name.StartsWith("@abp/") + || p.Name.StartsWith("@volo/") + || p.Name.StartsWith("@volosoft/"))) + { + if (IsValidNpmPackageName(p.Name)) + { + abpPackages.Add(p); + } + else + { + Logger.LogWarning($"Skipping invalid npm package name: {NpmHelper.SanitizeForLog(p.Name)}"); + } + } } return abpPackages; } + public static bool IsValidNpmPackageName(string packageName) + { + try + { + NpmHelper.EnsureSafePackageName(packageName); + return true; + } + catch (CliUsageException) + { + return false; + } + } + protected virtual async Task RunInstallLibsAsync(string fileDirectory) { Logger.LogInformation("Installing client-side packages..."); @@ -380,13 +400,13 @@ protected virtual async Task RunInstallLibsAsync(string fileDirectory) protected virtual void RunYarn(string fileDirectory) { Logger.LogInformation($"Running Yarn on {fileDirectory}"); - CmdHelper.RunCmd($"npx yarn", fileDirectory); + CmdHelper.RunCmd($"npx yarn --ignore-scripts", fileDirectory); } protected virtual void RunNpmInstall(string fileDirectory) { Logger.LogInformation($"Running npm install on {fileDirectory}"); - CmdHelper.RunCmd($"npm install", fileDirectory); + CmdHelper.RunCmd($"npm install --ignore-scripts", fileDirectory); } protected virtual List GetPackageVersionList(JProperty package, string workingDirectory = null) diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/ProjectNpmPackageAdder.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/ProjectNpmPackageAdder.cs index 9b7b17d57e2..8e8d52f7be3 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/ProjectNpmPackageAdder.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/ProjectNpmPackageAdder.cs @@ -72,6 +72,9 @@ public async Task AddNpmPackageAsync(string directory, NpmPackageInfo npmPackage return; } + NpmHelper.EnsureSafePackageName(npmPackage.Name); + NpmHelper.EnsureSafeVersion(version); + Logger.LogInformation($"Installing '{npmPackage.Name}' package to the project '{packageJsonFilePath}'..."); if (!File.ReadAllText(packageJsonFilePath).Contains($"\"{npmPackage.Name}\"")) @@ -81,7 +84,7 @@ public async Task AddNpmPackageAsync(string directory, NpmPackageInfo npmPackage using (DirectoryHelper.ChangeCurrentDirectory(directory)) { Logger.LogInformation("yarn add " + npmPackage.Name + versionPostfix); - CmdHelper.RunCmd("npx yarn add " + npmPackage.Name + versionPostfix); + CmdHelper.RunCmd("npx yarn add " + npmPackage.Name + versionPostfix + " --ignore-scripts"); } } else @@ -130,6 +133,8 @@ await SourceCodeDownloadService.DownloadNpmPackageAsync( public async Task AddMvcPackageAsync(string directory, NpmPackageInfo npmPackage, string version = null, bool skipInstallingLibs = false) { + NpmHelper.EnsureSafePackageName(npmPackage.Name); + var packageJsonFilePath = Path.Combine(directory, "package.json"); if (!File.Exists(packageJsonFilePath) || File.ReadAllText(packageJsonFilePath).Contains($"\"{npmPackage.Name}\"")) @@ -144,12 +149,14 @@ public async Task AddMvcPackageAsync(string directory, NpmPackageInfo npmPackage version = DetectAbpVersionOrNull(Path.Combine(directory, "package.json")); } + NpmHelper.EnsureSafeVersion(version); + var versionPostfix = version != null ? $"@{version}" : string.Empty; using (DirectoryHelper.ChangeCurrentDirectory(directory)) { Logger.LogInformation("yarn add " + npmPackage.Name + versionPostfix); - CmdHelper.RunCmd("npx yarn add " + npmPackage.Name + versionPostfix); + CmdHelper.RunCmd("npx yarn add " + npmPackage.Name + versionPostfix + " --ignore-scripts"); if (skipInstallingLibs) { diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Utils/NpmHelper.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Utils/NpmHelper.cs index d3dd24c53e6..35bdf1dd37c 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Utils/NpmHelper.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Utils/NpmHelper.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.RegularExpressions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using NuGet.Versioning; @@ -53,26 +54,64 @@ public bool IsYarnAvailable() public void RunNpmInstall(string directory, params string[] args) { Logger.LogInformation($"Running npm install on {directory}"); - CmdHelper.RunCmd($"npm install {args.JoinAsString(" ")}", directory); + CmdHelper.RunCmd($"npm install --ignore-scripts {args.JoinAsString(" ")}", directory); } public void RunYarn(string directory) { Logger.LogInformation($"Running Yarn on {directory}"); - CmdHelper.RunCmd($"npx yarn", directory); + CmdHelper.RunCmd($"npx yarn --ignore-scripts", directory); } [Obsolete("This method is deprecated. Use 'YarnAddPackage' instead (it uses 'npx', so there is no need for 'yarn' to be globally installed.")] public void NpmInstallPackage(string package, string version, string directory) { + EnsureSafePackageName(package); + EnsureSafeVersion(version); var packageVersion = !string.IsNullOrWhiteSpace(version) ? $"@{version}" : string.Empty; - CmdHelper.RunCmd("npm install " + package + packageVersion, workingDirectory: directory); + CmdHelper.RunCmd("npm install --ignore-scripts " + package + packageVersion, workingDirectory: directory); } public void YarnAddPackage(string package, string version, string directory) { + EnsureSafePackageName(package); + EnsureSafeVersion(version); var packageVersion = !string.IsNullOrWhiteSpace(version) ? $"@{version}" : string.Empty; - CmdHelper.RunCmd("npx yarn add " + package + packageVersion, workingDirectory: directory); + CmdHelper.RunCmd("npx yarn add " + package + packageVersion + " --ignore-scripts", workingDirectory: directory); + } + + private static readonly Regex SafePackageNameRegex = new( + @"^(@[a-zA-Z0-9][a-zA-Z0-9._-]*/)?[a-zA-Z0-9][a-zA-Z0-9._-]*$", + RegexOptions.Compiled); + + private static readonly Regex SafeVersionRegex = new( + @"^[a-zA-Z0-9._~^+\-]+$", + RegexOptions.Compiled); + + public static void EnsureSafePackageName(string packageName) + { + if (string.IsNullOrWhiteSpace(packageName) || !SafePackageNameRegex.IsMatch(packageName)) + { + throw new CliUsageException($"Invalid npm package name detected: {SanitizeForLog(packageName)}"); + } + } + + public static void EnsureSafeVersion(string version) + { + if (!string.IsNullOrWhiteSpace(version) && !SafeVersionRegex.IsMatch(version)) + { + throw new CliUsageException($"Invalid npm package version detected: {SanitizeForLog(version)}"); + } + } + + public static string SanitizeForLog(string value) + { + if (value == null) + { + return "(null)"; + } + + return Regex.Replace(value, @"[\x00-\x1F\x7F]", "?"); } public string GetInstalledNpmPackages() diff --git a/framework/test/Volo.Abp.Cli.Core.Tests/Volo/Abp/Cli/ProjectModification/NpmPackagesUpdater_Tests.cs b/framework/test/Volo.Abp.Cli.Core.Tests/Volo/Abp/Cli/ProjectModification/NpmPackagesUpdater_Tests.cs new file mode 100644 index 00000000000..3d5edd6d89f --- /dev/null +++ b/framework/test/Volo.Abp.Cli.Core.Tests/Volo/Abp/Cli/ProjectModification/NpmPackagesUpdater_Tests.cs @@ -0,0 +1,64 @@ +using Shouldly; +using Volo.Abp.Cli.ProjectModification; +using Volo.Abp.Cli.Utils; +using Xunit; + +namespace Volo.Abp.Cli; + +public class NpmPackagesUpdater_Tests +{ + [Theory] + [InlineData("@abp/ng.core", true)] + [InlineData("@abp/ng.theme.shared", true)] + [InlineData("@abp/ng.components", true)] + [InlineData("@volo/abp.ng.lepton-x.core", true)] + [InlineData("@volo/abp.commercial.ng.ui", true)] + [InlineData("@volosoft/abp.ng.theme.lepton", true)] + [InlineData("@abp/core && calc.exe", false)] + [InlineData("@abp/core; rm -rf /", false)] + [InlineData("@abp/core | curl evil.com", false)] + [InlineData("@abp/core`whoami`", false)] + [InlineData("@abp/core$(id)", false)] + [InlineData("@abp/core\nnewline", false)] + [InlineData("@abp/ space", false)] + [InlineData("@abp/", false)] + [InlineData("@abp/ng core", false)] + [InlineData(null, false)] + [InlineData("", false)] + public void IsValidNpmPackageName(string packageName, bool expected) + { + NpmPackagesUpdater.IsValidNpmPackageName(packageName).ShouldBe(expected); + } + + [Theory] + [InlineData("1.0.0", false)] + [InlineData("^8.0.0", false)] + [InlineData("~8.0.0", false)] + [InlineData("8.0.0-preview.1", false)] + [InlineData("8.0.0-preview20260401", false)] + [InlineData("8.0.0+build.123", false)] + [InlineData("latest", false)] + [InlineData("next", false)] + [InlineData(null, false)] + [InlineData("", false)] + [InlineData("1.0.0 && calc.exe", true)] + [InlineData("1.0.0; rm -rf /", true)] + [InlineData("1.0.0 | curl evil.com", true)] + [InlineData("1.0.0`whoami`", true)] + [InlineData("1.0.0$(id)", true)] + [InlineData("1.0.0\nnewline", true)] + [InlineData(">1.0.0", true)] + [InlineData("<2.0.0", true)] + [InlineData("1.0.0|2.0.0", true)] + public void EnsureSafeVersion(string version, bool shouldThrow) + { + if (shouldThrow) + { + Should.Throw(() => NpmHelper.EnsureSafeVersion(version)); + } + else + { + Should.NotThrow(() => NpmHelper.EnsureSafeVersion(version)); + } + } +}