Documentation Index
Fetch the complete documentation index at: https://mintlify.com/bitwarden/server/llms.txt
Use this file to discover all available pages before exploring further.
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 john@example.com 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