Security is paramount in Bitwarden Server. This guide outlines security considerations for contributors and developers.
Reporting Security Vulnerabilities
DO NOT create public GitHub issues for security vulnerabilities.
Bitwarden has a responsible disclosure policy:
HackerOne Program
Report security vulnerabilities through Bitwarden’s HackerOne program:
hackerone.com/bitwarden
Bitwarden welcomes security researchers and offers rewards for valid findings.
Private Disclosure
For sensitive reports, you can:
-
Encrypt your report using the PGP key:
- Key ID:
0xDE6887086F892325FEC04CC0D847525B6931381F
- Available in public keyserver pools
-
Contact Bitwarden at https://bitwarden.com/contact
What NOT to Do
- Denial of service attacks against production systems
- Spamming users or systems
- Social engineering of Bitwarden staff
- Physical attempts against Bitwarden property or data centers
Security Disclosure Policy
From SECURITY.md:
- Report vulnerabilities as soon as possible
- Bitwarden will make every effort to quickly resolve issues
- Provide reasonable time before public disclosure
- Avoid privacy violations, data destruction, or service degradation
- Only interact with accounts you own or have explicit permission to test
Secure Coding Practices
Never Commit Secrets
NEVER commit:
- API keys or tokens
- Passwords or connection strings with credentials
- Private keys or certificates
.env files with real secrets
secrets.json with production values
Use placeholders instead:
{
"globalSettings": {
"sqlServer": {
"connectionString": "Server=localhost;Database=vault_dev;User Id=SA;Password=YOUR_PASSWORD_HERE"
},
"installation": {
"id": "00000000-0000-0000-0000-000000000001",
"key": "YOUR_INSTALLATION_KEY_HERE"
}
}
}
Authentication & Authorization
Check Authorization
Always verify user permissions:
public class CiphersController : Controller
{
[HttpGet("{id}")]
public async Task<CipherResponseModel> Get(Guid id)
{
var cipher = await _cipherRepository.GetByIdAsync(id);
if (cipher == null)
{
throw new NotFoundException();
}
// CRITICAL: Check if user can access this cipher
if (!await _cipherService.CanAccessAsync(cipher, _userService.GetUserId()))
{
throw new NotFoundException(); // Don't reveal existence
}
return new CipherResponseModel(cipher);
}
}
Use Policy-Based Authorization
[Authorize(Policy = "OrganizationAdmin")]
public class OrganizationController : Controller
{
// Only admins can access
}
Validate User Context
public async Task<Organization> GetOrganizationAsync(Guid organizationId)
{
var userId = _userService.GetProperUserId(User);
if (!userId.HasValue)
{
throw new UnauthorizedException();
}
var orgUser = await _organizationUserRepository.GetByOrganizationAsync(
organizationId, userId.Value);
if (orgUser == null || orgUser.Status != OrganizationUserStatusType.Confirmed)
{
throw new NotFoundException();
}
return await _organizationRepository.GetByIdAsync(organizationId);
}
public async Task<User> GetUserByEmailAsync(string email)
{
// Validate input
if (string.IsNullOrWhiteSpace(email))
{
throw new BadRequestException("Email is required.");
}
if (!email.Contains('@'))
{
throw new BadRequestException("Invalid email format.");
}
// Normalize input
var normalizedEmail = email.ToLowerInvariant().Trim();
return await _userRepository.GetByEmailAsync(normalizedEmail);
}
Use Data Annotations
public class RegisterRequestModel
{
[Required]
[EmailAddress]
[StringLength(256)]
public string Email { get; set; }
[Required]
[StringLength(300, MinimumLength = 8)]
public string MasterPasswordHash { get; set; }
[StringLength(50)]
public string? MasterPasswordHint { get; set; }
}
Prevent Injection Attacks
SQL Injection - Use parameterized queries:
// Good: Parameterized (Dapper)
var user = await connection.QuerySingleOrDefaultAsync<User>(
"SELECT * FROM [User] WHERE Email = @Email",
new { Email = email });
// Good: Parameterized (EF Core)
var user = await dbContext.Users
.FirstOrDefaultAsync(u => u.Email == email);
// Bad: String concatenation (DON'T DO THIS)
var user = await connection.QuerySingleOrDefaultAsync<User>(
$"SELECT * FROM [User] WHERE Email = '{email}'");
Password Handling
Client-Side Hashing
Bitwarden uses client-side password hashing:
// The server receives an already-hashed password
public async Task<IdentityResult> RegisterUserAsync(
User user,
string masterPasswordHash) // Already hashed by client
{
// Hash again for server storage (server-side hash)
user.MasterPassword = _cryptoService.HashPassword(
masterPasswordHash,
user.Email);
await _userRepository.CreateAsync(user);
}
Key Points:
- Client sends
PBKDF2(password, email)
- Server stores
PBKDF2(clientHash, email) (double-hashed)
- Server never sees the actual password
Never Log Passwords
// Good: Don't log sensitive data
_logger.LogInformation("User {UserId} logged in", userId);
// Bad: Logs password hash
_logger.LogInformation("Login attempt: {Email} {Password}",
email, passwordHash); // DON'T DO THIS
Cryptography
Don’t Roll Your Own Crypto
Use established libraries:
// Good: Use .NET's crypto libraries
using var aes = Aes.Create();
aes.Key = key;
aes.IV = iv;
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
// Don't implement your own encryption algorithms
Use Strong Random Numbers
// Good: Cryptographically secure random
using var rng = RandomNumberGenerator.Create();
var randomBytes = new byte[32];
rng.GetBytes(randomBytes);
// Bad: Not cryptographically secure
var random = new Random(); // Don't use for security!
var value = random.Next();
Proper Key Derivation
public byte[] DeriveKey(string password, byte[] salt, int iterations)
{
using var pbkdf2 = new Rfc2898DeriveBytes(
password,
salt,
iterations,
HashAlgorithmName.SHA256);
return pbkdf2.GetBytes(32); // 256-bit key
}
Data Protection
Protect Sensitive Data at Rest
Use ASP.NET Core Data Protection:
public class UserRepository
{
private readonly IDataProtector _dataProtector;
public UserRepository(IDataProtectionProvider dataProtectionProvider)
{
_dataProtector = dataProtectionProvider
.CreateProtector(Constants.DatabaseFieldProtectorPurpose);
}
public async Task<User> CreateAsync(User user)
{
// Protect sensitive fields before storage
user.ApiKey = _dataProtector.Protect(user.ApiKey);
await _connection.ExecuteAsync(
"[dbo].[User_Create]",
user,
commandType: CommandType.StoredProcedure);
return user;
}
}
Protect Secrets in Configuration
// Use User Secrets for local development
// dotnet user-secrets set "ApiKey" "your-key-here"
// Use Azure Key Vault or similar for production
services.AddAzureKeyVault();
API Security
Rate Limiting
[HttpPost("login")]
[RateLimit(Name = "Login", Seconds = 60, MaxRequests = 5)]
public async Task<IdentityResult> Login([FromBody] LoginRequestModel model)
{
// Login logic
}
CORS Configuration
services.AddCors(options =>
{
options.AddPolicy("AllowedOrigins", builder =>
{
builder.WithOrigins(
"https://vault.bitwarden.com",
"https://vault.bitwarden.eu")
.AllowAnyHeader()
.AllowAnyMethod();
});
});
HTTPS Enforcement
app.UseHttpsRedirection();
app.UseHsts();
Error Handling
// Good: Generic error message
if (user == null)
{
throw new NotFoundException();
}
// Bad: Reveals information
if (user == null)
{
throw new NotFoundException("User with email [email protected] not found");
}
Log Errors Securely
try
{
await ProcessPaymentAsync(payment);
}
catch (Exception ex)
{
// Good: Log exception details server-side
_logger.LogError(ex, "Payment processing failed for user {UserId}", userId);
// Return generic error to client
throw new BadRequestException("Payment processing failed.");
}
Session Management
Secure Session Tokens
services.AddAuthentication()
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = issuer,
ValidAudience = audience,
IssuerSigningKey = new SymmetricSecurityKey(key),
ClockSkew = TimeSpan.Zero // Strict expiration
};
});
Implement Logout
public async Task LogoutAsync(string refreshToken)
{
// Revoke refresh token
await _refreshTokenRepository.DeleteByTokenAsync(refreshToken);
// Update security stamp to invalidate existing tokens
await _userService.RefreshSecurityStampAsync(user);
}
Security Testing
Write Security Tests
[Theory]
[BitAutoData]
public async Task GetCipher_UnauthorizedUser_ThrowsNotFoundException(
SutProvider<CipherService> sutProvider,
Cipher cipher,
Guid unauthorizedUserId)
{
// Arrange
cipher.UserId = Guid.NewGuid(); // Different user
sutProvider.GetDependency<ICipherRepository>()
.GetByIdAsync(cipher.Id)
.Returns(cipher);
// Act & Assert
await Assert.ThrowsAsync<NotFoundException>(
() => sutProvider.Sut.GetAsync(cipher.Id, unauthorizedUserId));
}
Test Authorization
[Theory]
[BitAutoData]
public async Task UpdateOrganization_NonAdmin_ThrowsUnauthorizedException(
SutProvider<OrganizationService> sutProvider,
Organization org,
OrganizationUser orgUser)
{
// Arrange: User is not admin
orgUser.Type = OrganizationUserType.User;
// Act & Assert
await Assert.ThrowsAsync<UnauthorizedException>(
() => sutProvider.Sut.UpdateAsync(org, orgUser));
}
Dependency Security
Keep Dependencies Updated
# Check for outdated packages
dotnet list package --outdated
# Check for vulnerable packages
dotnet list package --vulnerable
Review Dependencies
Before adding new dependencies:
- Check package popularity and maintenance
- Review recent security advisories
- Verify package signatures
- Review license compatibility
Production Security
Environment Variables
Never hardcode production values:
// Good: Read from configuration
var apiKey = configuration["ExternalApi:ApiKey"];
// Bad: Hardcoded
var apiKey = "sk_live_abc123def456"; // DON'T DO THIS
Secrets Management
Use proper secrets management:
- Development: User Secrets,
.env files (not committed)
- Production: Azure Key Vault, AWS Secrets Manager, etc.
app.Use(async (context, next) =>
{
context.Response.Headers.Add("X-Content-Type-Options", "nosniff");
context.Response.Headers.Add("X-Frame-Options", "SAMEORIGIN");
context.Response.Headers.Add("X-XSS-Protection", "1; mode=block");
context.Response.Headers.Add("Referrer-Policy", "strict-origin-when-cross-origin");
await next();
});
Security Checklist
Before submitting security-sensitive code:
Resources
See Also