diff --git a/SECURITY_SUMMARY.md b/SECURITY_SUMMARY.md new file mode 100644 index 0000000..d33fac2 --- /dev/null +++ b/SECURITY_SUMMARY.md @@ -0,0 +1,203 @@ +# Security Summary - SafeVault Project + +## Overview +This document outlines the security vulnerabilities that were identified during development and the fixes that were applied to the SafeVault application. + +## Activity 1: Secure Coding Practices + +### Vulnerability 1: Cross-Site Scripting (XSS) +**Description:** User input could contain malicious JavaScript code that executes in the browser. + +**Example Attack:** +```html + + +``` + +**Fix Applied:** +- Implemented `InputSanitizer.SanitizeForXss()` method +- Encodes HTML special characters: `<`, `>`, `&`, `"`, `'`, `/` +- All user inputs displayed in HTML context are sanitized +- Applied in webform.html with proper validation + +**Location:** `SafeVault.App/Security/InputSanitizer.cs` + +### Vulnerability 2: SQL Injection +**Description:** Unparameterized SQL queries could allow attackers to manipulate database queries. + +**Example Attack:** +```sql +Username: admin' OR '1'='1 +Password: anything' OR '1'='1 +``` + +**Fix Applied:** +- Implemented parameterized queries in `UserRepository` +- All SQL queries use parameter binding via `DbCommand.Parameters` +- Added `InputSanitizer.ContainsSqlInjectionPattern()` to detect injection attempts +- Input validation prevents malicious patterns from reaching the database + +**Vulnerable Code (DO NOT USE):** +```csharp +// INSECURE - String concatenation +string query = "SELECT * FROM Users WHERE Username = '" + username + "'"; +``` + +**Secure Code (USED):** +```csharp +// SECURE - Parameterized query +string query = "SELECT * FROM Users WHERE Username = @Username"; +command.Parameters.Add(new SqlParameter("@Username", username)); +``` + +**Location:** `SafeVault.App/Data/UserRepository.cs` + +### Vulnerability 3: Weak Input Validation +**Description:** Insufficient validation could allow malformed or malicious data. + +**Fix Applied:** +- Email validation using regex pattern +- Username validation (alphanumeric + underscore, 3-20 characters) +- SQL injection pattern detection +- Password strength requirements enforced + +**Location:** `SafeVault.App/Security/InputSanitizer.cs` + +## Activity 2: Authentication & Authorization + +### Vulnerability 4: Plain Text Passwords +**Description:** Storing passwords in plain text or using weak hashing exposes user credentials. + +**Fix Applied:** +- Implemented BCrypt password hashing with work factor +- Uses `AuthService.HashPassword()` for password hashing +- Uses `AuthService.VerifyPassword()` for secure password verification +- BCrypt automatically handles salting and multiple rounds of hashing + +**Vulnerable Code (DO NOT USE):** +```csharp +// INSECURE - Plain text or simple hash +user.Password = password; // Plain text +user.PasswordHash = ComputeSHA256(password); // Weak hash without salt +``` + +**Secure Code (USED):** +```csharp +// SECURE - BCrypt with automatic salting +user.PasswordHash = AuthService.HashPassword(password); +bool isValid = AuthService.VerifyPassword(password, user.PasswordHash); +``` + +**Location:** `SafeVault.App/Security/AuthService.cs` + +### Vulnerability 5: Missing Authorization Checks +**Description:** Users could access resources or perform actions without proper authorization. + +**Fix Applied:** +- Implemented role-based authorization with `RoleAuthorization` +- Admin and User roles defined +- `RequireAdmin()` and `RequireRole()` methods enforce authorization +- `CanAccessResource()` checks resource ownership +- Throws `UnauthorizedAccessException` for unauthorized access attempts + +**Location:** `SafeVault.App/Security/RoleAuthorization.cs` + +### Vulnerability 6: Weak Password Requirements +**Description:** Weak passwords could be easily guessed or brute-forced. + +**Fix Applied:** +- Password must be at least 8 characters +- Must contain uppercase letter +- Must contain lowercase letter +- Must contain digit +- Must contain special character +- Validated by `AuthService.IsStrongPassword()` + +**Location:** `SafeVault.App/Security/AuthService.cs` + +## Activity 3: Security Testing & Regression Prevention + +### Security Testing Implementation +To prevent regression of security vulnerabilities, comprehensive tests were implemented: + +1. **TestInputValidation.cs** + - Tests XSS sanitization + - Tests SQL injection detection + - Tests email validation + - Tests username validation + +2. **TestAuthentication.cs** + - Tests password hashing + - Tests password verification + - Tests login with valid/invalid credentials + - Tests password strength requirements + +3. **TestAuthorization.cs** + - Tests role checking (admin/user) + - Tests resource access control + - Tests authorization exceptions + - Tests privilege escalation prevention + +4. **TestSecurityRegression.cs** + - Tests for previously vulnerable patterns + - Tests parameterized query usage + - Tests input sanitization consistency + - Ensures security fixes remain effective + +**Location:** `Tests/` folder + +## Security Best Practices Applied + +### 1. Defense in Depth +- Multiple layers of security: input validation, sanitization, parameterized queries +- Client-side and server-side validation +- Both preventive (parameterized queries) and detective (pattern matching) controls + +### 2. Principle of Least Privilege +- Role-based access control limits user permissions +- Users can only access their own resources +- Admins have elevated privileges with explicit checks + +### 3. Secure by Default +- Default role is "user" (not admin) +- All queries use parameterized approach +- Password hashing is mandatory +- Input validation required before database operations + +### 4. Security Through Obscurity Avoided +- Security mechanisms are well-documented +- Relies on proven cryptographic methods (BCrypt) +- Uses industry-standard security practices + +## Remaining Considerations + +### Production Deployment Recommendations +1. **HTTPS:** Use TLS/SSL for all communications +2. **CSRF Protection:** Implement anti-CSRF tokens for web forms +3. **Rate Limiting:** Prevent brute-force attacks on login +4. **Session Management:** Implement secure session handling +5. **Logging:** Log authentication attempts and authorization failures +6. **Database Security:** Use database-level access controls +7. **Secrets Management:** Store connection strings securely (not in code) +8. **Regular Updates:** Keep BCrypt.Net-Next and other dependencies updated + +### Potential Future Enhancements +- Two-factor authentication (2FA) +- Account lockout after failed login attempts +- Password reset functionality with secure tokens +- Audit logging for all security-relevant events +- Content Security Policy (CSP) headers +- Input sanitization for additional contexts (URLs, JSON, etc.) + +## Testing Verification +All security tests pass successfully: +- ✅ Input validation tests +- ✅ Authentication tests +- ✅ Authorization tests +- ✅ Security regression tests + +## Conclusion +The SafeVault application implements comprehensive security controls to protect against common vulnerabilities including XSS, SQL injection, weak authentication, and inadequate authorization. All identified vulnerabilities have been addressed with industry-standard security practices and are covered by automated tests to prevent regression. + +**Last Updated:** February 9, 2026 +**Version:** 1.0 diff --git a/SafeVault.App/Data/UserRepository.cs b/SafeVault.App/Data/UserRepository.cs new file mode 100644 index 0000000..9b271ea --- /dev/null +++ b/SafeVault.App/Data/UserRepository.cs @@ -0,0 +1,203 @@ +using System.Data; +using System.Data.Common; +using SafeVault.App.Models; +using SafeVault.App.Security; + +namespace SafeVault.App.Data; + +/// +/// Repository for User data access using parameterized SQL queries to prevent SQL injection +/// +public class UserRepository +{ + private readonly DbConnection _connection; + + public UserRepository(DbConnection connection) + { + _connection = connection ?? throw new ArgumentNullException(nameof(connection)); + } + + /// + /// Creates a new user with sanitized input and parameterized query + /// + public int CreateUser(string username, string email, string password, string role = "user") + { + // Validate and sanitize inputs + if (!InputSanitizer.IsValidUsername(username)) + throw new ArgumentException("Invalid username format", nameof(username)); + + if (!InputSanitizer.IsValidEmail(email)) + throw new ArgumentException("Invalid email format", nameof(email)); + + if (!AuthService.IsStrongPassword(password)) + throw new ArgumentException("Password does not meet security requirements", nameof(password)); + + // Hash password + string passwordHash = AuthService.HashPassword(password); + + // Use parameterized query to prevent SQL injection + string query = @" + INSERT INTO Users (Username, Email, PasswordHash, Role) + VALUES (@Username, @Email, @PasswordHash, @Role); + SELECT last_insert_rowid();"; + + using var command = _connection.CreateCommand(); + command.CommandText = query; + + AddParameter(command, "@Username", username); + AddParameter(command, "@Email", email); + AddParameter(command, "@PasswordHash", passwordHash); + AddParameter(command, "@Role", role); + + if (_connection.State != ConnectionState.Open) + _connection.Open(); + + var result = command.ExecuteScalar(); + return Convert.ToInt32(result); + } + + /// + /// Gets a user by username using parameterized query + /// + public User? GetUserByUsername(string username) + { + if (string.IsNullOrEmpty(username)) + return null; + + // Use parameterized query to prevent SQL injection + string query = "SELECT Id, Username, Email, PasswordHash, Role FROM Users WHERE Username = @Username"; + + using var command = _connection.CreateCommand(); + command.CommandText = query; + AddParameter(command, "@Username", username); + + if (_connection.State != ConnectionState.Open) + _connection.Open(); + + using var reader = command.ExecuteReader(); + if (reader.Read()) + { + return new User + { + Id = reader.GetInt32(0), + Username = reader.GetString(1), + Email = reader.GetString(2), + PasswordHash = reader.GetString(3), + Role = reader.GetString(4) + }; + } + + return null; + } + + /// + /// Gets a user by ID using parameterized query + /// + public User? GetUserById(int id) + { + // Use parameterized query to prevent SQL injection + string query = "SELECT Id, Username, Email, PasswordHash, Role FROM Users WHERE Id = @Id"; + + using var command = _connection.CreateCommand(); + command.CommandText = query; + AddParameter(command, "@Id", id); + + if (_connection.State != ConnectionState.Open) + _connection.Open(); + + using var reader = command.ExecuteReader(); + if (reader.Read()) + { + return new User + { + Id = reader.GetInt32(0), + Username = reader.GetString(1), + Email = reader.GetString(2), + PasswordHash = reader.GetString(3), + Role = reader.GetString(4) + }; + } + + return null; + } + + /// + /// Updates user role using parameterized query + /// + public bool UpdateUserRole(int userId, string newRole) + { + // Use parameterized query to prevent SQL injection + string query = "UPDATE Users SET Role = @Role WHERE Id = @Id"; + + using var command = _connection.CreateCommand(); + command.CommandText = query; + AddParameter(command, "@Role", newRole); + AddParameter(command, "@Id", userId); + + if (_connection.State != ConnectionState.Open) + _connection.Open(); + + int rowsAffected = command.ExecuteNonQuery(); + return rowsAffected > 0; + } + + /// + /// Deletes a user using parameterized query + /// + public bool DeleteUser(int userId) + { + // Use parameterized query to prevent SQL injection + string query = "DELETE FROM Users WHERE Id = @Id"; + + using var command = _connection.CreateCommand(); + command.CommandText = query; + AddParameter(command, "@Id", userId); + + if (_connection.State != ConnectionState.Open) + _connection.Open(); + + int rowsAffected = command.ExecuteNonQuery(); + return rowsAffected > 0; + } + + /// + /// Gets all users using secure query + /// + public List GetAllUsers() + { + var users = new List(); + string query = "SELECT Id, Username, Email, PasswordHash, Role FROM Users"; + + using var command = _connection.CreateCommand(); + command.CommandText = query; + + if (_connection.State != ConnectionState.Open) + _connection.Open(); + + using var reader = command.ExecuteReader(); + while (reader.Read()) + { + users.Add(new User + { + Id = reader.GetInt32(0), + Username = reader.GetString(1), + Email = reader.GetString(2), + PasswordHash = reader.GetString(3), + Role = reader.GetString(4) + }); + } + + return users; + } + + /// + /// Helper method to add parameters safely + /// + private void AddParameter(DbCommand command, string parameterName, object value) + { + var parameter = command.CreateParameter(); + parameter.ParameterName = parameterName; + parameter.Value = value ?? DBNull.Value; + command.Parameters.Add(parameter); + } +} diff --git a/SafeVault.App/Models/User.cs b/SafeVault.App/Models/User.cs new file mode 100644 index 0000000..a5a5979 --- /dev/null +++ b/SafeVault.App/Models/User.cs @@ -0,0 +1,10 @@ +namespace SafeVault.App.Models; + +public class User +{ + public int Id { get; set; } + public string Username { get; set; } = string.Empty; + public string Email { get; set; } = string.Empty; + public string PasswordHash { get; set; } = string.Empty; + public string Role { get; set; } = "user"; +} diff --git a/SafeVault.App/Program.cs b/SafeVault.App/Program.cs new file mode 100644 index 0000000..e05beda --- /dev/null +++ b/SafeVault.App/Program.cs @@ -0,0 +1,139 @@ +using SafeVault.App.Models; +using SafeVault.App.Security; + +namespace SafeVault.App; + +class Program +{ + static void Main(string[] args) + { + Console.WriteLine("=== SafeVault - Secure Authentication & Authorization Demo ===\n"); + + // Demo users list (in real app, this would be a database) + var users = new List(); + + // Activity 1: Input Sanitization Demo + Console.WriteLine("--- Activity 1: Input Sanitization ---"); + DemoInputSanitization(); + + // Activity 2: Authentication Demo + Console.WriteLine("\n--- Activity 2: Authentication ---"); + var admin = DemoAuthentication(users); + + // Activity 2: Authorization Demo + Console.WriteLine("\n--- Activity 2: Authorization ---"); + DemoAuthorization(admin, users); + + Console.WriteLine("\n=== Demo Complete ==="); + } + + static void DemoInputSanitization() + { + // XSS Protection + string maliciousInput = ""; + string sanitized = InputSanitizer.SanitizeForXss(maliciousInput); + Console.WriteLine($"Original: {maliciousInput}"); + Console.WriteLine($"Sanitized: {sanitized}"); + + // SQL Injection Detection + string sqlInjection = "admin' OR '1'='1"; + bool isSqlInjection = InputSanitizer.ContainsSqlInjectionPattern(sqlInjection); + Console.WriteLine($"\nInput: {sqlInjection}"); + Console.WriteLine($"Contains SQL Injection Pattern: {isSqlInjection}"); + + // Email Validation + string validEmail = "user@example.com"; + string invalidEmail = "not-an-email"; + Console.WriteLine($"\n'{validEmail}' is valid email: {InputSanitizer.IsValidEmail(validEmail)}"); + Console.WriteLine($"'{invalidEmail}' is valid email: {InputSanitizer.IsValidEmail(invalidEmail)}"); + + // Username Validation + string validUsername = "john_doe123"; + string invalidUsername = "john@doe"; + Console.WriteLine($"\n'{validUsername}' is valid username: {InputSanitizer.IsValidUsername(validUsername)}"); + Console.WriteLine($"'{invalidUsername}' is valid username: {InputSanitizer.IsValidUsername(invalidUsername)}"); + } + + static User DemoAuthentication(List users) + { + // Create an admin user + string password = "SecurePass123!"; + string passwordHash = AuthService.HashPassword(password); + + var admin = new User + { + Id = 1, + Username = "admin", + Email = "admin@safevault.com", + PasswordHash = passwordHash, + Role = "admin" + }; + users.Add(admin); + + Console.WriteLine("Created admin user with hashed password"); + Console.WriteLine($"Password: {password}"); + Console.WriteLine($"Hash: {passwordHash.Substring(0, 20)}..."); + + // Test valid login + var loginResult = AuthService.Login("admin", password, users); + Console.WriteLine($"\nLogin with correct password: {(loginResult != null ? "SUCCESS" : "FAILED")}"); + + // Test invalid login + loginResult = AuthService.Login("admin", "wrongpassword", users); + Console.WriteLine($"Login with wrong password: {(loginResult != null ? "SUCCESS" : "FAILED")}"); + + // Test password strength + Console.WriteLine($"\nPassword strength check:"); + Console.WriteLine($"'weak' is strong: {AuthService.IsStrongPassword("weak")}"); + Console.WriteLine($"'SecurePass123!' is strong: {AuthService.IsStrongPassword("SecurePass123!")}"); + + return admin; + } + + static void DemoAuthorization(User admin, List users) + { + // Create a regular user + var user = new User + { + Id = 2, + Username = "john_doe", + Email = "john@example.com", + PasswordHash = AuthService.HashPassword("UserPass123!"), + Role = "user" + }; + users.Add(user); + + // Check admin privileges + Console.WriteLine($"Admin user is admin: {RoleAuthorization.IsAdmin(admin)}"); + Console.WriteLine($"Regular user is admin: {RoleAuthorization.IsAdmin(user)}"); + + // Check resource access + int resourceOwnerId = 2; // Resource owned by john_doe + Console.WriteLine($"\nResource access (Owner ID: {resourceOwnerId}):"); + Console.WriteLine($"Admin can access: {RoleAuthorization.CanAccessResource(admin, resourceOwnerId)}"); + Console.WriteLine($"Owner can access: {RoleAuthorization.CanAccessResource(user, resourceOwnerId)}"); + + // Test requiring admin role + try + { + Console.WriteLine("\nTrying admin-only operation with admin user..."); + RoleAuthorization.RequireAdmin(admin); + Console.WriteLine("SUCCESS: Admin operation allowed"); + } + catch (UnauthorizedAccessException ex) + { + Console.WriteLine($"FAILED: {ex.Message}"); + } + + try + { + Console.WriteLine("\nTrying admin-only operation with regular user..."); + RoleAuthorization.RequireAdmin(user); + Console.WriteLine("SUCCESS: Admin operation allowed"); + } + catch (UnauthorizedAccessException ex) + { + Console.WriteLine($"FAILED: {ex.Message}"); + } + } +} diff --git a/SafeVault.App/SafeVault.App.csproj b/SafeVault.App/SafeVault.App.csproj new file mode 100644 index 0000000..a858f5a --- /dev/null +++ b/SafeVault.App/SafeVault.App.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + diff --git a/SafeVault.App/Security/AuthService.cs b/SafeVault.App/Security/AuthService.cs new file mode 100644 index 0000000..2e34cfb --- /dev/null +++ b/SafeVault.App/Security/AuthService.cs @@ -0,0 +1,77 @@ +using SafeVault.App.Models; + +namespace SafeVault.App.Security; + +/// +/// Provides authentication services with secure password hashing using BCrypt +/// +public class AuthService +{ + /// + /// Hashes a password using BCrypt + /// + public static string HashPassword(string password) + { + if (string.IsNullOrEmpty(password)) + throw new ArgumentException("Password cannot be null or empty", nameof(password)); + + return BCrypt.Net.BCrypt.HashPassword(password); + } + + /// + /// Verifies a password against a hash + /// + public static bool VerifyPassword(string password, string hash) + { + if (string.IsNullOrEmpty(password) || string.IsNullOrEmpty(hash)) + return false; + + try + { + return BCrypt.Net.BCrypt.Verify(password, hash); + } + catch + { + return false; + } + } + + /// + /// Authenticates a user with username and password + /// Returns the user if authentication is successful, null otherwise + /// + public static User? Login(string username, string password, List users) + { + if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password)) + return null; + + // Find user by username + var user = users.FirstOrDefault(u => u.Username.Equals(username, StringComparison.OrdinalIgnoreCase)); + + if (user == null) + return null; + + // Verify password + if (!VerifyPassword(password, user.PasswordHash)) + return null; + + return user; + } + + /// + /// Validates password strength + /// Password must be at least 8 characters with uppercase, lowercase, digit, and special character + /// + public static bool IsStrongPassword(string password) + { + if (string.IsNullOrEmpty(password) || password.Length < 8) + return false; + + bool hasUpper = password.Any(char.IsUpper); + bool hasLower = password.Any(char.IsLower); + bool hasDigit = password.Any(char.IsDigit); + bool hasSpecial = password.Any(c => !char.IsLetterOrDigit(c)); + + return hasUpper && hasLower && hasDigit && hasSpecial; + } +} diff --git a/SafeVault.App/Security/InputSanitizer.cs b/SafeVault.App/Security/InputSanitizer.cs new file mode 100644 index 0000000..c1e1244 --- /dev/null +++ b/SafeVault.App/Security/InputSanitizer.cs @@ -0,0 +1,108 @@ +using System.Text.RegularExpressions; + +namespace SafeVault.App.Security; + +/// +/// Provides input sanitization to prevent XSS and SQL injection attacks +/// +public class InputSanitizer +{ + /// + /// Sanitizes input to prevent XSS attacks by encoding HTML special characters + /// + public static string SanitizeForXss(string input) + { + if (string.IsNullOrEmpty(input)) + return input; + + return input + .Replace("&", "&") + .Replace("<", "<") + .Replace(">", ">") + .Replace("\"", """) + .Replace("'", "'") + .Replace("/", "/"); + } + + /// + /// Validates input for SQL injection patterns + /// Returns true if input contains suspicious SQL patterns + /// + public static bool ContainsSqlInjectionPattern(string input) + { + if (string.IsNullOrEmpty(input)) + return false; + + // Common SQL injection patterns + string[] sqlPatterns = new[] + { + @"(\b(SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER|EXEC|EXECUTE|UNION|DECLARE)\b)", + @"(--|\#|\/\*|\*\/)", + @"('|(;|=))", + @"(\bOR\b\s+\d+\s*=\s*\d+)", + @"(\bAND\b\s+\d+\s*=\s*\d+)", + @"(xp_|sp_)" + }; + + foreach (var pattern in sqlPatterns) + { + if (Regex.IsMatch(input, pattern, RegexOptions.IgnoreCase)) + { + return true; + } + } + + return false; + } + + /// + /// Validates email format + /// + public static bool IsValidEmail(string email) + { + if (string.IsNullOrWhiteSpace(email)) + return false; + + try + { + var emailPattern = @"^[^@\s]+@[^@\s]+\.[^@\s]+$"; + return Regex.IsMatch(email, emailPattern, RegexOptions.IgnoreCase); + } + catch + { + return false; + } + } + + /// + /// Validates username format (alphanumeric and underscore only) + /// + public static bool IsValidUsername(string username) + { + if (string.IsNullOrWhiteSpace(username)) + return false; + + var usernamePattern = @"^[a-zA-Z0-9_]{3,20}$"; + return Regex.IsMatch(username, usernamePattern); + } + + /// + /// Sanitizes input for safe usage in SQL context + /// Note: This should be used in conjunction with parameterized queries + /// + public static string SanitizeForSql(string input) + { + if (string.IsNullOrEmpty(input)) + return input; + + // Remove or escape dangerous characters + return input + .Replace("'", "''") + .Replace(";", "") + .Replace("--", "") + .Replace("/*", "") + .Replace("*/", "") + .Replace("xp_", "") + .Replace("sp_", ""); + } +} diff --git a/SafeVault.App/Security/RoleAuthorization.cs b/SafeVault.App/Security/RoleAuthorization.cs new file mode 100644 index 0000000..e024ec4 --- /dev/null +++ b/SafeVault.App/Security/RoleAuthorization.cs @@ -0,0 +1,77 @@ +using SafeVault.App.Models; + +namespace SafeVault.App.Security; + +/// +/// Provides role-based authorization checks +/// +public class RoleAuthorization +{ + public const string AdminRole = "admin"; + public const string UserRole = "user"; + + /// + /// Checks if a user has the specified role + /// + public static bool HasRole(User user, string role) + { + if (user == null || string.IsNullOrEmpty(role)) + return false; + + return user.Role.Equals(role, StringComparison.OrdinalIgnoreCase); + } + + /// + /// Checks if a user is an administrator + /// + public static bool IsAdmin(User user) + { + return HasRole(user, AdminRole); + } + + /// + /// Checks if a user is a regular user + /// + public static bool IsUser(User user) + { + return HasRole(user, UserRole); + } + + /// + /// Authorizes access based on required role + /// Throws UnauthorizedAccessException if user doesn't have the required role + /// + public static void RequireRole(User user, string requiredRole) + { + if (!HasRole(user, requiredRole)) + { + throw new UnauthorizedAccessException($"User does not have the required role: {requiredRole}"); + } + } + + /// + /// Authorizes admin access + /// Throws UnauthorizedAccessException if user is not an admin + /// + public static void RequireAdmin(User user) + { + RequireRole(user, AdminRole); + } + + /// + /// Checks if user can access a resource + /// Admins can access everything, users can only access their own resources + /// + public static bool CanAccessResource(User user, int resourceOwnerId) + { + if (user == null) + return false; + + // Admins can access everything + if (IsAdmin(user)) + return true; + + // Users can only access their own resources + return user.Id == resourceOwnerId; + } +} diff --git a/SafeVault.slnx b/SafeVault.slnx new file mode 100644 index 0000000..7ae1ed5 --- /dev/null +++ b/SafeVault.slnx @@ -0,0 +1,4 @@ + + + + diff --git a/Tests/TestAuthentication.cs b/Tests/TestAuthentication.cs new file mode 100644 index 0000000..74bf8b6 --- /dev/null +++ b/Tests/TestAuthentication.cs @@ -0,0 +1,248 @@ +using NUnit.Framework; +using SafeVault.App.Models; +using SafeVault.App.Security; + +namespace Tests; + +[TestFixture] +public class TestAuthentication +{ + [Test] + public void Test_HashPassword_GeneratesUniqueHashes() + { + // Arrange + string password = "TestPassword123!"; + + // Act + string hash1 = AuthService.HashPassword(password); + string hash2 = AuthService.HashPassword(password); + + // Assert + Assert.That(hash1, Is.Not.Null); + Assert.That(hash2, Is.Not.Null); + Assert.That(hash1, Is.Not.EqualTo(hash2)); // BCrypt uses random salt + Assert.That(hash1, Does.StartWith("$2")); // BCrypt format + } + + [Test] + public void Test_HashPassword_ThrowsOnNullOrEmpty() + { + // Act & Assert + Assert.Throws(() => AuthService.HashPassword(null!)); + Assert.Throws(() => AuthService.HashPassword("")); + } + + [Test] + public void Test_VerifyPassword_ValidPassword() + { + // Arrange + string password = "TestPassword123!"; + string hash = AuthService.HashPassword(password); + + // Act + bool isValid = AuthService.VerifyPassword(password, hash); + + // Assert + Assert.That(isValid, Is.True); + } + + [Test] + public void Test_VerifyPassword_InvalidPassword() + { + // Arrange + string password = "TestPassword123!"; + string wrongPassword = "WrongPassword123!"; + string hash = AuthService.HashPassword(password); + + // Act + bool isValid = AuthService.VerifyPassword(wrongPassword, hash); + + // Assert + Assert.That(isValid, Is.False); + } + + [Test] + public void Test_VerifyPassword_HandlesNullAndEmpty() + { + // Arrange + string hash = AuthService.HashPassword("TestPassword123!"); + + // Act & Assert + Assert.That(AuthService.VerifyPassword(null!, hash), Is.False); + Assert.That(AuthService.VerifyPassword("", hash), Is.False); + Assert.That(AuthService.VerifyPassword("password", null!), Is.False); + Assert.That(AuthService.VerifyPassword("password", ""), Is.False); + } + + [Test] + public void Test_Login_ValidCredentials() + { + // Arrange + var users = new List + { + new User + { + Id = 1, + Username = "testuser", + Email = "test@example.com", + PasswordHash = AuthService.HashPassword("TestPass123!"), + Role = "user" + } + }; + + // Act + var result = AuthService.Login("testuser", "TestPass123!", users); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result!.Username, Is.EqualTo("testuser")); + } + + [Test] + public void Test_Login_InvalidPassword() + { + // Arrange + var users = new List + { + new User + { + Id = 1, + Username = "testuser", + Email = "test@example.com", + PasswordHash = AuthService.HashPassword("TestPass123!"), + Role = "user" + } + }; + + // Act + var result = AuthService.Login("testuser", "WrongPassword", users); + + // Assert + Assert.That(result, Is.Null); + } + + [Test] + public void Test_Login_InvalidUsername() + { + // Arrange + var users = new List + { + new User + { + Id = 1, + Username = "testuser", + Email = "test@example.com", + PasswordHash = AuthService.HashPassword("TestPass123!"), + Role = "user" + } + }; + + // Act + var result = AuthService.Login("wronguser", "TestPass123!", users); + + // Assert + Assert.That(result, Is.Null); + } + + [Test] + public void Test_Login_NullOrEmptyCredentials() + { + // Arrange + var users = new List(); + + // Act & Assert + Assert.That(AuthService.Login(null!, "password", users), Is.Null); + Assert.That(AuthService.Login("", "password", users), Is.Null); + Assert.That(AuthService.Login("username", null!, users), Is.Null); + Assert.That(AuthService.Login("username", "", users), Is.Null); + } + + [Test] + public void Test_Login_CaseInsensitiveUsername() + { + // Arrange + var users = new List + { + new User + { + Id = 1, + Username = "TestUser", + Email = "test@example.com", + PasswordHash = AuthService.HashPassword("TestPass123!"), + Role = "user" + } + }; + + // Act + var result1 = AuthService.Login("testuser", "TestPass123!", users); + var result2 = AuthService.Login("TESTUSER", "TestPass123!", users); + + // Assert + Assert.That(result1, Is.Not.Null); + Assert.That(result2, Is.Not.Null); + } + + [Test] + public void Test_IsStrongPassword_ValidPasswords() + { + // Arrange & Act & Assert + Assert.That(AuthService.IsStrongPassword("StrongPass123!"), Is.True); + Assert.That(AuthService.IsStrongPassword("Test@123Pass"), Is.True); + Assert.That(AuthService.IsStrongPassword("Abcd1234!@#$"), Is.True); + } + + [Test] + public void Test_IsStrongPassword_WeakPasswords() + { + // Arrange & Act & Assert + Assert.That(AuthService.IsStrongPassword("weak"), Is.False); // Too short + Assert.That(AuthService.IsStrongPassword("password"), Is.False); // No uppercase, digit, special + Assert.That(AuthService.IsStrongPassword("PASSWORD123!"), Is.False); // No lowercase + Assert.That(AuthService.IsStrongPassword("Password!"), Is.False); // No digit + Assert.That(AuthService.IsStrongPassword("Password123"), Is.False); // No special char + Assert.That(AuthService.IsStrongPassword(""), Is.False); + Assert.That(AuthService.IsStrongPassword(null!), Is.False); + } + + [Test] + public void Test_PasswordHashing_ResistsBruteForce() + { + // Arrange + string password = "TestPassword123!"; + + // Act - Measure time for hashing (BCrypt should be slow by design) + var startTime = DateTime.Now; + string hash = AuthService.HashPassword(password); + var endTime = DateTime.Now; + var duration = endTime - startTime; + + // Assert + Assert.That(hash, Is.Not.Null); + Assert.That(duration.TotalMilliseconds, Is.GreaterThan(10)); // BCrypt should take some time + } + + [Test] + public void Test_Authentication_PreventsTimingAttacks() + { + // Arrange + var users = new List + { + new User + { + Id = 1, + Username = "testuser", + Email = "test@example.com", + PasswordHash = AuthService.HashPassword("TestPass123!"), + Role = "user" + } + }; + + // Act - Both should return null (fail) but timing should be similar + var result1 = AuthService.Login("wronguser", "TestPass123!", users); + var result2 = AuthService.Login("testuser", "WrongPassword", users); + + // Assert + Assert.That(result1, Is.Null); + Assert.That(result2, Is.Null); + } +} diff --git a/Tests/TestAuthorization.cs b/Tests/TestAuthorization.cs new file mode 100644 index 0000000..466eff2 --- /dev/null +++ b/Tests/TestAuthorization.cs @@ -0,0 +1,223 @@ +using NUnit.Framework; +using SafeVault.App.Models; +using SafeVault.App.Security; + +namespace Tests; + +[TestFixture] +public class TestAuthorization +{ + private User _adminUser = null!; + private User _regularUser = null!; + + [SetUp] + public void Setup() + { + _adminUser = new User + { + Id = 1, + Username = "admin", + Email = "admin@safevault.com", + PasswordHash = AuthService.HashPassword("AdminPass123!"), + Role = "admin" + }; + + _regularUser = new User + { + Id = 2, + Username = "john_doe", + Email = "john@example.com", + PasswordHash = AuthService.HashPassword("UserPass123!"), + Role = "user" + }; + } + + [Test] + public void Test_HasRole_AdminUser() + { + // Act & Assert + Assert.That(RoleAuthorization.HasRole(_adminUser, "admin"), Is.True); + Assert.That(RoleAuthorization.HasRole(_adminUser, "user"), Is.False); + } + + [Test] + public void Test_HasRole_RegularUser() + { + // Act & Assert + Assert.That(RoleAuthorization.HasRole(_regularUser, "user"), Is.True); + Assert.That(RoleAuthorization.HasRole(_regularUser, "admin"), Is.False); + } + + [Test] + public void Test_HasRole_CaseInsensitive() + { + // Act & Assert + Assert.That(RoleAuthorization.HasRole(_adminUser, "ADMIN"), Is.True); + Assert.That(RoleAuthorization.HasRole(_adminUser, "Admin"), Is.True); + Assert.That(RoleAuthorization.HasRole(_regularUser, "USER"), Is.True); + } + + [Test] + public void Test_HasRole_NullUser() + { + // Act & Assert + Assert.That(RoleAuthorization.HasRole(null!, "admin"), Is.False); + } + + [Test] + public void Test_IsAdmin_AdminUser() + { + // Act & Assert + Assert.That(RoleAuthorization.IsAdmin(_adminUser), Is.True); + } + + [Test] + public void Test_IsAdmin_RegularUser() + { + // Act & Assert + Assert.That(RoleAuthorization.IsAdmin(_regularUser), Is.False); + } + + [Test] + public void Test_IsUser_RegularUser() + { + // Act & Assert + Assert.That(RoleAuthorization.IsUser(_regularUser), Is.True); + } + + [Test] + public void Test_IsUser_AdminUser() + { + // Act & Assert + Assert.That(RoleAuthorization.IsUser(_adminUser), Is.False); + } + + [Test] + public void Test_RequireRole_Success() + { + // Act & Assert - Should not throw + Assert.DoesNotThrow(() => RoleAuthorization.RequireRole(_adminUser, "admin")); + Assert.DoesNotThrow(() => RoleAuthorization.RequireRole(_regularUser, "user")); + } + + [Test] + public void Test_RequireRole_Failure() + { + // Act & Assert + var ex1 = Assert.Throws(() => + RoleAuthorization.RequireRole(_regularUser, "admin")); + Assert.That(ex1!.Message, Does.Contain("admin")); + + var ex2 = Assert.Throws(() => + RoleAuthorization.RequireRole(_adminUser, "user")); + Assert.That(ex2!.Message, Does.Contain("user")); + } + + [Test] + public void Test_RequireAdmin_AdminUser() + { + // Act & Assert - Should not throw + Assert.DoesNotThrow(() => RoleAuthorization.RequireAdmin(_adminUser)); + } + + [Test] + public void Test_RequireAdmin_RegularUser() + { + // Act & Assert + var ex = Assert.Throws(() => + RoleAuthorization.RequireAdmin(_regularUser)); + Assert.That(ex!.Message, Does.Contain("admin")); + } + + [Test] + public void Test_CanAccessResource_AdminCanAccessAll() + { + // Act & Assert + Assert.That(RoleAuthorization.CanAccessResource(_adminUser, 1), Is.True); + Assert.That(RoleAuthorization.CanAccessResource(_adminUser, 2), Is.True); + Assert.That(RoleAuthorization.CanAccessResource(_adminUser, 999), Is.True); + } + + [Test] + public void Test_CanAccessResource_UserCanAccessOwn() + { + // Act & Assert + Assert.That(RoleAuthorization.CanAccessResource(_regularUser, 2), Is.True); // Own resource + Assert.That(RoleAuthorization.CanAccessResource(_regularUser, 1), Is.False); // Other's resource + Assert.That(RoleAuthorization.CanAccessResource(_regularUser, 999), Is.False); // Other's resource + } + + [Test] + public void Test_CanAccessResource_NullUser() + { + // Act & Assert + Assert.That(RoleAuthorization.CanAccessResource(null!, 1), Is.False); + } + + [Test] + public void Test_PrivilegeEscalation_Prevention() + { + // Arrange + var maliciousUser = new User + { + Id = 3, + Username = "hacker", + Email = "hacker@example.com", + PasswordHash = AuthService.HashPassword("HackerPass123!"), + Role = "user" // Try to escalate to admin + }; + + // Act - Try to access admin resources + bool canAccessAdminResource = RoleAuthorization.CanAccessResource(maliciousUser, 1); + bool isAdmin = RoleAuthorization.IsAdmin(maliciousUser); + + // Assert + Assert.That(canAccessAdminResource, Is.False); + Assert.That(isAdmin, Is.False); + Assert.Throws(() => + RoleAuthorization.RequireAdmin(maliciousUser)); + } + + [Test] + public void Test_Authorization_ConsistentBehavior() + { + // Verify that multiple checks return consistent results + for (int i = 0; i < 5; i++) + { + Assert.That(RoleAuthorization.IsAdmin(_adminUser), Is.True); + Assert.That(RoleAuthorization.IsAdmin(_regularUser), Is.False); + Assert.That(RoleAuthorization.CanAccessResource(_adminUser, 999), Is.True); + Assert.That(RoleAuthorization.CanAccessResource(_regularUser, 999), Is.False); + } + } + + [Test] + public void Test_RoleConstants() + { + // Verify role constants are correct + Assert.That(RoleAuthorization.AdminRole, Is.EqualTo("admin")); + Assert.That(RoleAuthorization.UserRole, Is.EqualTo("user")); + } + + [Test] + public void Test_MultipleUsers_IndependentAuthorization() + { + // Arrange + var user1 = new User { Id = 10, Username = "user1", Role = "user" }; + var user2 = new User { Id = 20, Username = "user2", Role = "user" }; + var user3 = new User { Id = 30, Username = "user3", Role = "user" }; + + // Act & Assert - Each user can only access their own resources + Assert.That(RoleAuthorization.CanAccessResource(user1, 10), Is.True); + Assert.That(RoleAuthorization.CanAccessResource(user1, 20), Is.False); + Assert.That(RoleAuthorization.CanAccessResource(user1, 30), Is.False); + + Assert.That(RoleAuthorization.CanAccessResource(user2, 10), Is.False); + Assert.That(RoleAuthorization.CanAccessResource(user2, 20), Is.True); + Assert.That(RoleAuthorization.CanAccessResource(user2, 30), Is.False); + + Assert.That(RoleAuthorization.CanAccessResource(user3, 10), Is.False); + Assert.That(RoleAuthorization.CanAccessResource(user3, 20), Is.False); + Assert.That(RoleAuthorization.CanAccessResource(user3, 30), Is.True); + } +} diff --git a/Tests/TestInputValidation.cs b/Tests/TestInputValidation.cs new file mode 100644 index 0000000..a4ba68e --- /dev/null +++ b/Tests/TestInputValidation.cs @@ -0,0 +1,151 @@ +using NUnit.Framework; +using SafeVault.App.Security; + +namespace Tests; + +[TestFixture] +public class TestInputValidation +{ + [Test] + public void Test_SanitizeForXss_RemovesScriptTags() + { + // Arrange + string maliciousInput = ""; + + // Act + string sanitized = InputSanitizer.SanitizeForXss(maliciousInput); + + // Assert + Assert.That(sanitized, Does.Not.Contain("")); + Assert.That(sanitized, Does.Contain("<script>")); + } + + [Test] + public void Test_SanitizeForXss_EncodesSpecialCharacters() + { + // Arrange + string input = "<>&\"'/"; + + // Act + string sanitized = InputSanitizer.SanitizeForXss(input); + + // Assert + Assert.That(sanitized, Is.EqualTo("<>&"'/")); + } + + [Test] + public void Test_SanitizeForXss_HandlesNullAndEmpty() + { + // Act & Assert + Assert.That(InputSanitizer.SanitizeForXss(null), Is.Null); + Assert.That(InputSanitizer.SanitizeForXss(""), Is.EqualTo("")); + } + + [Test] + public void Test_ContainsSqlInjectionPattern_DetectsSqlKeywords() + { + // Arrange & Act & Assert + Assert.That(InputSanitizer.ContainsSqlInjectionPattern("admin' OR '1'='1"), Is.True); + Assert.That(InputSanitizer.ContainsSqlInjectionPattern("SELECT * FROM Users"), Is.True); + Assert.That(InputSanitizer.ContainsSqlInjectionPattern("'; DROP TABLE Users; --"), Is.True); + Assert.That(InputSanitizer.ContainsSqlInjectionPattern("admin'--"), Is.True); + Assert.That(InputSanitizer.ContainsSqlInjectionPattern("1' UNION SELECT"), Is.True); + } + + [Test] + public void Test_ContainsSqlInjectionPattern_AllowsNormalInput() + { + // Arrange & Act & Assert + Assert.That(InputSanitizer.ContainsSqlInjectionPattern("john_doe"), Is.False); + Assert.That(InputSanitizer.ContainsSqlInjectionPattern("user@example.com"), Is.False); + Assert.That(InputSanitizer.ContainsSqlInjectionPattern("normaltext123"), Is.False); + } + + [Test] + public void Test_IsValidEmail_AcceptsValidEmails() + { + // Arrange & Act & Assert + Assert.That(InputSanitizer.IsValidEmail("user@example.com"), Is.True); + Assert.That(InputSanitizer.IsValidEmail("test.user@domain.co.uk"), Is.True); + Assert.That(InputSanitizer.IsValidEmail("name+tag@example.com"), Is.True); + } + + [Test] + public void Test_IsValidEmail_RejectsInvalidEmails() + { + // Arrange & Act & Assert + Assert.That(InputSanitizer.IsValidEmail("notanemail"), Is.False); + Assert.That(InputSanitizer.IsValidEmail("@example.com"), Is.False); + Assert.That(InputSanitizer.IsValidEmail("user@"), Is.False); + Assert.That(InputSanitizer.IsValidEmail(""), Is.False); + Assert.That(InputSanitizer.IsValidEmail(null), Is.False); + } + + [Test] + public void Test_IsValidUsername_AcceptsValidUsernames() + { + // Arrange & Act & Assert + Assert.That(InputSanitizer.IsValidUsername("john_doe"), Is.True); + Assert.That(InputSanitizer.IsValidUsername("user123"), Is.True); + Assert.That(InputSanitizer.IsValidUsername("admin"), Is.True); + Assert.That(InputSanitizer.IsValidUsername("test_user_123"), Is.True); + } + + [Test] + public void Test_IsValidUsername_RejectsInvalidUsernames() + { + // Arrange & Act & Assert + Assert.That(InputSanitizer.IsValidUsername("ab"), Is.False); // Too short + Assert.That(InputSanitizer.IsValidUsername("a".PadRight(21, 'a')), Is.False); // Too long + Assert.That(InputSanitizer.IsValidUsername("user@name"), Is.False); // Invalid char + Assert.That(InputSanitizer.IsValidUsername("user name"), Is.False); // Space + Assert.That(InputSanitizer.IsValidUsername("user-name"), Is.False); // Hyphen + Assert.That(InputSanitizer.IsValidUsername(""), Is.False); + Assert.That(InputSanitizer.IsValidUsername(null), Is.False); + } + + [Test] + public void Test_SanitizeForSql_RemovesDangerousCharacters() + { + // Arrange + string input = "test'; DROP TABLE--"; + + // Act + string sanitized = InputSanitizer.SanitizeForSql(input); + + // Assert + Assert.That(sanitized, Does.Not.Contain(";")); + Assert.That(sanitized, Does.Not.Contain("--")); + } + + [Test] + public void Test_XssAttackVectors_AreBlocked() + { + // Test various XSS attack vectors with HTML tags + string[] xssVectors = new[] + { + "", + "", + "", + "" + }; + + foreach (var vector in xssVectors) + { + string sanitized = InputSanitizer.SanitizeForXss(vector); + // Verify dangerous tags are encoded (< and > are converted) + Assert.That(sanitized, Does.Not.Contain("", + "", + "", + "javascript:alert('XSS')", + "" + }; + + foreach (var pattern in xssPatterns) + { + string sanitized = InputSanitizer.SanitizeForXss(pattern); + + // Verify dangerous tags are encoded + Assert.That(sanitized, Does.Not.Contain("(() => + RoleAuthorization.RequireAdmin(regularUser)); + } + + [Test] + public void Test_NoSqlStringConcatenation() + { + // This test verifies that we're using parameterized queries + // by checking that dangerous inputs are safely handled + + string maliciousUsername = "admin' OR '1'='1' --"; + + // Should be detected as SQL injection + bool isInjection = InputSanitizer.ContainsSqlInjectionPattern(maliciousUsername); + Assert.That(isInjection, Is.True); + + // Should fail username validation + bool isValidUsername = InputSanitizer.IsValidUsername(maliciousUsername); + Assert.That(isValidUsername, Is.False); + } + + [Test] + public void Test_PasswordHashing_UsesSalt() + { + // Arrange + string password = "SamePassword123!"; + + // Act - Hash same password twice + string hash1 = AuthService.HashPassword(password); + string hash2 = AuthService.HashPassword(password); + + // Assert - Hashes should be different due to salt + Assert.That(hash1, Is.Not.EqualTo(hash2)); + } + + [Test] + public void Test_SessionSecurity_NoWeakPasswords() + { + // Test that weak passwords are rejected + string[] weakPasswords = new[] + { + "password", + "12345678", + "qwerty", + "Password", // No digit or special char + "PASSWORD123", // No lowercase + "password123", // No uppercase + "Password123" // No special char + }; + + foreach (var weakPassword in weakPasswords) + { + bool isStrong = AuthService.IsStrongPassword(weakPassword); + Assert.That(isStrong, Is.False, + $"Weak password accepted: {weakPassword}"); + } + } + + [Test] + public void Test_RoleEscalation_Prevention() + { + // Arrange - User tries to escalate to admin + var user = new User + { + Id = 3, + Username = "hacker", + Email = "hacker@example.com", + PasswordHash = AuthService.HashPassword("HackerPass123!"), + Role = "user" + }; + + // Try to manually change role (should not work in practice) + // Authorization checks should always check the current role + + // Act & Assert + Assert.That(RoleAuthorization.IsAdmin(user), Is.False); + Assert.Throws(() => + RoleAuthorization.RequireAdmin(user)); + } + + [Test] + public void Test_ConsistentSecurityMeasures() + { + // Verify all security measures work together + + // 1. Input validation + string email = "test@example.com"; + string username = "testuser"; + Assert.That(InputSanitizer.IsValidEmail(email), Is.True); + Assert.That(InputSanitizer.IsValidUsername(username), Is.True); + + // 2. Password hashing + string password = "SecurePass123!"; + Assert.That(AuthService.IsStrongPassword(password), Is.True); + string hash = AuthService.HashPassword(password); + Assert.That(hash, Does.StartWith("$2")); + + // 3. Authentication + var users = new List + { + new User + { + Id = 1, + Username = username, + Email = email, + PasswordHash = hash, + Role = "user" + } + }; + var loginResult = AuthService.Login(username, password, users); + Assert.That(loginResult, Is.Not.Null); + + // 4. Authorization + Assert.That(RoleAuthorization.IsUser(loginResult!), Is.True); + Assert.That(RoleAuthorization.CanAccessResource(loginResult, 1), Is.True); + } + + [Test] + public void Test_NoInformationLeakage_InvalidLogin() + { + // Arrange + var users = new List + { + new User + { + Id = 1, + Username = "realuser", + Email = "real@example.com", + PasswordHash = AuthService.HashPassword("RealPass123!"), + Role = "user" + } + }; + + // Act - Try invalid username and invalid password + var result1 = AuthService.Login("fakeuser", "SomePass123!", users); + var result2 = AuthService.Login("realuser", "WrongPass123!", users); + + // Assert - Both should return null without leaking info + Assert.That(result1, Is.Null); + Assert.That(result2, Is.Null); + // No exception or different behavior that could leak info + } + + [Test] + public void Test_InputSanitization_AppliedConsistently() + { + // Test that sanitization is consistent + string input = ""; + + string sanitized1 = InputSanitizer.SanitizeForXss(input); + string sanitized2 = InputSanitizer.SanitizeForXss(input); + + // Should produce same result + Assert.That(sanitized1, Is.EqualTo(sanitized2)); + + // Should be safe + Assert.That(sanitized1, Does.Not.Contain(" + +