// ============================================================================= // 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; /// /// Tests for injection vulnerabilities including: /// - SQL Injection (SQLi) /// - NoSQL Injection /// - Command Injection /// - LDAP Injection /// - XPath Injection /// [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 { ["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 GetSqlInjectionPayloads() { var data = new TheoryData(); foreach (var payload in MaliciousPayloads.SqlInjection.Common) { data.Add(payload); } return data; } public static TheoryData GetCommandInjectionPayloads() { var data = new TheoryData(); foreach (var payload in MaliciousPayloads.CommandInjection.Generic) { data.Add(payload); } return data; } public static TheoryData GetNoSqlInjectionPayloads() { var data = new TheoryData(); foreach (var payload in MaliciousPayloads.SqlInjection.NoSql) { data.Add(payload); } return data; } [GeneratedRegex(@"@\w+")] private static partial Regex QueryPatternRegex(); } /// /// Input sanitizer for testing injection prevention. /// In production, this would be the actual sanitization service. /// 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)); } }