Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions src/NSmartProxy.SecurityTests/NSmartProxy.SecurityTests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="nunit" Version="4.1.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\NSmartProxy.Infrastructure\NSmartProxy.Infrastructure.csproj" />
<ProjectReference Include="..\NSmartProxy\NSmartProxy.csproj" />
</ItemGroup>

</Project>
124 changes: 124 additions & 0 deletions src/NSmartProxy.SecurityTests/SecurityIssuesTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
using System;
using System.IO;
using System.Security.Cryptography.X509Certificates;
using NSmartProxy.Extension;
using NUnit.Framework;

namespace NSmartProxy.SecurityTests
{
[TestFixture]
public class SecurityIssuesTests
{
[Test]
public void CAGen_UsesHardcodedPassword_SecurityVulnerability()
{
// Arrange
var certificateName = "test";

// Act
var certificate = CAGen.GenerateCA(certificateName);

// Assert
// This test demonstrates that the certificate can be accessed with the hardcoded password
var exportedCert = certificate.Export(X509ContentType.Pfx, "WeNeedASaf3rPassword");
Assert.That(exportedCert, Is.Not.Null);

// This is a security issue - the password is hardcoded and predictable
Assert.That(exportedCert.Length, Is.GreaterThan(0), "Certificate should be exportable with hardcoded password");
}

[Test]
public void CAGen_HardcodedPasswordIsExposed_SecurityIssue()
{
// This test verifies that the hardcoded password is indeed a security risk
var certificateName = "test";
var certificate = CAGen.GenerateCA(certificateName);

// The fact that we can export with this specific password proves it's hardcoded
Assert.That(() =>
{
var export = certificate.Export(X509ContentType.Pfx, "WeNeedASaf3rPassword");
}, Throws.Nothing, "Certificate exports with hardcoded password - this is a security vulnerability");
}

[Test]
public void CAGen_SupportsCustomPassword_SecurityImprovement()
{
// Test that the new secure password parameter works
var certificateName = "test";
var customPassword = "MySecurePassword123!";

var certificate = CAGen.GenerateCA(certificateName, null, customPassword);

// Should be able to export with custom password
Assert.That(() =>
{
var export = certificate.Export(X509ContentType.Pfx, customPassword);
}, Throws.Nothing, "Certificate should export with custom password");

// Should NOT be able to export with old hardcoded password
Assert.That(() =>
{
var export = certificate.Export(X509ContentType.Pfx, "WeNeedASaf3rPassword");
}, Throws.Exception, "Certificate should NOT export with old hardcoded password");
}

[Test]
public void CAGen_GeneratesRandomPasswordWhenNoneProvided_SecurityImprovement()
{
// Test that random passwords are generated when none provided
var certificateName = "test";

var certificate1 = CAGen.GenerateCA(certificateName);
var certificate2 = CAGen.GenerateCA(certificateName);

// Both certificates should exist but should not be exportable with hardcoded password
Assert.That(certificate1, Is.Not.Null);
Assert.That(certificate2, Is.Not.Null);

// Verify they can't be exported with old hardcoded password
Assert.That(() =>
{
var export = certificate1.Export(X509ContentType.Pfx, "WeNeedASaf3rPassword");
}, Throws.Exception, "Certificate should NOT export with old hardcoded password");
}

[Test]
public void PathTraversal_FileWriteVulnerability_Demonstration()
{
// This test demonstrates the potential for path traversal attacks
// Note: This is for demonstration purposes - the actual API would need to be called

var baseLogPath = "./temp";
var maliciousFileName = "../../../etc/passwd"; // Potential path traversal
var targetPath = baseLogPath + "/" + maliciousFileName;

// This shows how the current code could be vulnerable to path traversal
Assert.That(targetPath.Contains("../"), Is.True, "Path contains traversal characters - potential security risk");
}

[Test]
public void PathSanitization_PreventDirectoryTraversal_SecurityImprovement()
{
// Test that Path.GetFileName properly sanitizes filenames
var maliciousFileName = "../../../etc/passwd";
var sanitizedFileName = Path.GetFileName(maliciousFileName);

// Should only contain the filename, not directory traversal
Assert.That(sanitizedFileName, Is.EqualTo("passwd"));
Assert.That(sanitizedFileName.Contains("../"), Is.False, "Sanitized filename should not contain directory traversal");
}

[Test]
public void DefaultCredentials_AdminAccountExists_SecurityIssue()
{
// This test documents the existence of default admin credentials
var defaultUsername = "admin";
var defaultPassword = "admin";

// These are the default credentials created in HttpServer_APIs constructor
Assert.That(defaultUsername, Is.EqualTo("admin"), "Default admin username is predictable");
Assert.That(defaultPassword, Is.EqualTo("admin"), "Default admin password is weak and predictable");
}
}
}
15 changes: 12 additions & 3 deletions src/NSmartProxy/Extension/CAGen.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using NSmartProxy.Infrastructure;

namespace NSmartProxy.Extension
{
Expand All @@ -14,8 +15,9 @@ public class CAGen
/// </summary>
/// <param name="CertificateName"></param>
/// <param name="hosts"></param>
/// <param name="password">证书密码,如果为空则生成随机密码</param>
/// <returns></returns>
public static X509Certificate2 GenerateCA(string CertificateName,string hosts = null)
public static X509Certificate2 GenerateCA(string CertificateName, string hosts = null, string password = null)
{
SubjectAlternativeNameBuilder sanBuilder = new SubjectAlternativeNameBuilder();
sanBuilder.AddIpAddress(IPAddress.Loopback);
Expand Down Expand Up @@ -52,8 +54,15 @@ public static X509Certificate2 GenerateCA(string CertificateName,string hosts =
//certificate.FriendlyName = CertificateName;
//return certificate;

return new X509Certificate2(certificate.Export(X509ContentType.Pfx, "WeNeedASaf3rPassword"),
"WeNeedASaf3rPassword", X509KeyStorageFlags.Exportable);
// 使用提供的密码或生成安全的随机密码
if (string.IsNullOrEmpty(password))
{
password = RandomHelper.NextString(32, true); // 生成32位强密码
Server.Logger?.Debug($"Generated secure random password for certificate: {CertificateName}");
}

return new X509Certificate2(certificate.Export(X509ContentType.Pfx, password),
password, X509KeyStorageFlags.Exportable);

}
}
Expand Down
47 changes: 39 additions & 8 deletions src/NSmartProxy/Extension/HttpServer_APIs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Security;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using NSmartProxy.Authorize;
Expand Down Expand Up @@ -38,7 +39,13 @@ public HttpServerApis(IServerContext serverContext, IDbOperator dbOperator, stri
//如果库中没有任何记录,则增加默认用户
if (Dbop.GetLength() < 1)
{
AddUserV2("admin", "admin", "1");
// 生成随机管理员密码以提高安全性
var randomPassword = RandomHelper.NextString(16, true);
AddUserV2("admin", randomPassword, "1");

// 记录默认凭据到日志(仅记录用户名,不记录密码)
Server.Logger.Info($"创建了默认管理员账户: admin,密码已随机生成。请立即登录并修改密码!");
Server.Logger.Info($"Default admin account created with random password. Please login and change password immediately!");
}
}

Expand Down Expand Up @@ -348,7 +355,8 @@ public void UpdateUser(string oldUserName, string newUserName, string userPwd, s
User user = Dbop.Get(oldUserName)?.ToObject<User>();
user.isAdmin = isAdmin;
user.userName = newUserName;
if (userPwd != "XXXXXXXX")
// 使用更安全的密码检查逻辑:只有在密码非空且不是特殊标记时才更新
if (!string.IsNullOrEmpty(userPwd) && userPwd != "XXXXXXXX" && userPwd.Trim().Length > 0)
{
user.userPwd = EncryptHelper.SHA256(userPwd);
}
Expand Down Expand Up @@ -795,11 +803,23 @@ private string FormatExtension(X509ExtensionCollection cert2Extensions)
public string GenerateCA(string hosts)
{
var caName = RandomHelper.NextString(10, false);
X509Certificate2 ca = CAGen.GenerateCA(caName, hosts);
var export = ca.Export(X509ContentType.Pfx);
// 生成安全的随机密码
var certificatePassword = RandomHelper.NextString(32, true);
X509Certificate2 ca = CAGen.GenerateCA(caName, hosts, certificatePassword);
var export = ca.Export(X509ContentType.Pfx, certificatePassword);
string baseLogPath = "./temp";
string fileName = "_" + caName + ".pfx";
string targetPath = baseLogPath + "/" + fileName;
// 防止路径遍历攻击:仅使用文件名,移除目录路径
string safeFileName = "_" + Path.GetFileName(caName) + ".pfx";
string targetPath = Path.Combine(baseLogPath, safeFileName);

// 确保目标路径在安全目录内
string fullBasePath = Path.GetFullPath(baseLogPath);
string fullTargetPath = Path.GetFullPath(targetPath);
if (!fullTargetPath.StartsWith(fullBasePath))
{
throw new SecurityException("Invalid file path detected - potential directory traversal attack");
}

DirectoryInfo dir = new DirectoryInfo(baseLogPath);
if (!dir.Exists)
{
Expand All @@ -808,15 +828,26 @@ public string GenerateCA(string hosts)

// File.Move(fileInfo.FullName, baseLogPath + "/" + port + ".pfx");
File.WriteAllBytes(targetPath, export);
return fileName;
return safeFileName;
}

[FileUpload]
[Secure]
public string UploadTempFile(FileInfo fileInfo)
{
string baseLogPath = "./temp";
string targetPath = baseLogPath + "/" + fileInfo.Name;
// 防止路径遍历攻击:仅使用文件名,移除目录路径
string safeFileName = Path.GetFileName(fileInfo.Name);
string targetPath = Path.Combine(baseLogPath, safeFileName);

// 确保目标路径在安全目录内
string fullBasePath = Path.GetFullPath(baseLogPath);
string fullTargetPath = Path.GetFullPath(targetPath);
if (!fullTargetPath.StartsWith(fullBasePath))
{
throw new SecurityException("Invalid file path detected - potential directory traversal attack");
}

DirectoryInfo dir = new DirectoryInfo(baseLogPath);
if (!dir.Exists)
{
Expand Down