Skip to content

Conversation

@Patrick-Pimentel-Bitwarden
Copy link
Contributor

@Patrick-Pimentel-Bitwarden Patrick-Pimentel-Bitwarden commented Nov 17, 2025

NEEDS TESTING ON FEATURE BRANCH

🎟️ Tracking

https://bitwarden.atlassian.net/browse/PM-22975

📔 Objective

The objective of this ticket is to create a client version validator that checks the header data on a token request and prevent users who have migrated to v2 from logging in with older client versions.

  1. Extracted out KM utilities for checking isV2Encryption from RotateUserAccountKeysCommand
  2. Added some clarity and comments in BaseRequestValidator
  3. Installed the validation for client version in the appropriate place to prevent account enumeration

📸 Screenshots

Validator.Working.mov
Non.Rotated.Key.Working.Fine.mov

⏰ Reminders before review

  • Contributor guidelines followed
  • All formatters and local linters executed and passed
  • Written new unit and / or integration tests where applicable
  • Protected functional changes with optionality (feature flags)
  • Used internationalization (i18n) for all UI strings
  • CI builds passed
  • Communicated to DevOps any deployment requirements
  • Updated any necessary documentation (Confluence, contributing docs) or informed the documentation team

🦮 Reviewer guidelines

  • 👍 (:+1:) or similar for great changes
  • 📝 (:memo:) or ℹ️ (:information_source:) for notes or general info
  • ❓ (:question:) for questions
  • 🤔 (:thinking:) or 💭 (:thought_balloon:) for more open inquiry that's not quite a confirmed issue and could potentially benefit from discussion
  • 🎨 (:art:) for suggestions / improvements
  • ❌ (:x:) or ⚠️ (:warning:) for more significant problems or concerns needing attention
  • 🌱 (:seedling:) or ♻️ (:recycle:) for future improvements or indications of technical debt
  • ⛏ (:pick:) for minor or nitpick changes

@github-actions
Copy link
Contributor

github-actions bot commented Nov 17, 2025

Logo
Checkmarx One – Scan Summary & Detailsd11bb73a-bf64-4541-947b-10776e3c3b86

New Issues (2)

Checkmarx found the following issues in this Pull Request

Severity Issue Source File / Package Checkmarx Insight
MEDIUM CSRF /src/Api/Vault/Controllers/CiphersController.cs: 1519
detailsMethod at line 1519 of /src/Api/Vault/Controllers/CiphersController.cs gets a parameter from a user request from id. This parameter value flows ...
ID: dMGF5qNfAN72zlvQcA1MgbhHv%2Fc%3D
Attack Vector
MEDIUM CSRF /src/Api/Vault/Controllers/CiphersController.cs: 1395
detailsMethod at line 1395 of /src/Api/Vault/Controllers/CiphersController.cs gets a parameter from a user request from id. This parameter value flows ...
ID: iOCFr11iI9znjDnv46yLfiS4aDY%3D
Attack Vector
Fixed Issues (1)

Great job! The following issues were fixed in this Pull Request

Severity Issue Source File / Package
MEDIUM CSRF /src/Api/Vault/Controllers/CiphersController.cs: 300

@claude
Copy link
Contributor

claude bot commented Nov 20, 2025

Claude finished @Patrick-Pimentel-Bitwarden's task —— View job


Code Review Complete

Tasks

  • Launch bitwarden-code-reviewer agent to analyze PR changes
  • Review findings and post feedback
  • Provide final summary

Comprehensive Code Review - PR #6588: Client Version Validator

Overall Assessment: REQUEST CHANGES

This PR implements critical security infrastructure to prevent v2 encryption users from logging in with incompatible old clients. The core approach is sound, with proper account enumeration prevention through post-credential validation. However, there are critical test coverage gaps and an important consideration about the breaking change impact.


Finding 1: Missing test coverage for User.HasV2Encryption() edge cases

Location: src/Core/Entities/User.cs:219
Severity: ❌ CRITICAL

Details and fix

The User.HasV2Encryption() method combines two conditions (v2 key shape + security version 2) but lacks dedicated test coverage for edge cases.

Missing test scenarios:

  1. User with v2 private key shape (XChaCha20Poly1305) but SecurityVersion = 1
  2. User with SecurityVersion = 2 but AES private key (type 2)
  3. User with invalid/malformed PrivateKey that throws ArgumentException
  4. User with empty string PrivateKey

Why this matters:
This method is the gatekeeper for enforcing client version requirements. Incorrect v2 detection could:

  • Block v1 users inappropriately (false positive)
  • Allow v2 users on old clients (false negative → data corruption risk)

Recommendation:
Add comprehensive unit tests in test/Core.Test/Entities/UserHasV2EncryptionTests.cs:

[Theory]
[InlineData(TestEncryptionConstants.V2PrivateKey, 2, true)]  // Valid v2
[InlineData(TestEncryptionConstants.V2PrivateKey, 1, false)] // v2 key, v1 version
[InlineData(TestEncryptionConstants.AES256_CBC_HMAC_Encstring, 2, false)] // v1 key, v2 version
[InlineData(null, 2, false)] // Null private key
[InlineData("", 2, false)] // Empty private key
[InlineData("invalid", 2, false)] // Malformed key (triggers exception)
public void HasV2Encryption_VariousStates_ReturnsExpected(string privateKey, int? securityVersion, bool expected)
{
    var user = new User 
    { 
        PrivateKey = privateKey, 
        SecurityVersion = securityVersion 
    };
    Assert.Equal(expected, user.HasV2Encryption());
}

This ensures the validator's core dependency behaves correctly under all conditions.


Finding 2: Blocking all clients without version header is a breaking change

Location: src/Identity/IdentityServer/RequestValidators/ClientVersionValidator.cs:48
Severity: ⚠️ IMPORTANT

Details and consideration

The validator rejects ALL authentication attempts when the Bitwarden-Client-Version header is missing:

if (clientVersion == null)
{
    FillRequestContextWithErrorData(requestContext, "version_header_missing", _versionHeaderMissing);
    return false;
}

Impact:

  • Current behavior: Any client without the header (old clients, custom integrations, CLI versions) cannot authenticate
  • Breaking change: YES - affects ALL clients that don't send this header, regardless of encryption version
  • User experience: Users with old but still-functional v1 clients will be locked out immediately

Security rationale from code comments:

"We want to establish a strict contract with clients that if they omit this header, then the server cannot guarantee that a client won't do harm to a user's data with stale encryption architecture."

Questions to address:

  1. Has Product confirmed this breaking change is acceptable for v1 users on old clients?
  2. Is there a grace period or phased rollout planned?
  3. Are there metrics on how many clients don't send this header?

Alternative approach (if breaking change is problematic):
Only require the header for v2 users:

Version? minVersion = user.HasV2Encryption() ? Constants.MinimumClientVersionForV2Encryption : null;

if (minVersion == null)
{
    return true; // v1 user - no version check needed
}

// v2 user - header is required
if (clientVersion == null)
{
    FillRequestContextWithErrorData(requestContext, "version_header_missing", _versionHeaderMissing);
    return false;
}

This maintains backward compatibility for v1 users while enforcing strict version requirements for v2 users.

If the breaking change is intentional: Document the rollout plan and client version requirements in the PR description.


Finding 3: Account enumeration prevention validation

Location: src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs:129-138
Severity: ✅ VERIFIED (Not an issue)

Verification details

The PR correctly prevents account enumeration by placing client version validation AFTER credential validation:

// 1. Validate credentials first
var valid = await ValidateContextAsync(context, validatorContext);
if (!valid) { return; }

// 1.5 Now check client version (after proving account ownership)
var clientVersionValid = await ValidateClientVersionAsync(context, validatorContext);

Why this is correct:

  • Attackers cannot probe for account existence by trying known-invalid client versions
  • Error response only occurs AFTER successful credential validation
  • Same error pattern used for other post-authentication checks (SSO, 2FA)

Feature flag path also safe:
The new validation order in DetermineValidationOrder() places ValidateClientVersionAsync() second, but AFTER ValidateGrantSpecificContext() which validates credentials first.

No changes needed - the implementation correctly prevents account enumeration.


Finding 4: Failed login event logging may misrepresent user intent

Location: src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs:372
Severity: 🎨 SUGGESTED

Details and improvement

When client version validation fails, the code logs a failed login event:

SetValidationErrorResult(context, validatorContext);
await LogFailedLoginEvent(validatorContext.User, EventType.User_FailedLogIn);

Issue:
A user with correct credentials on an outdated client isn't attempting a malicious login - they simply need to update their app. Logging this as a "failed login" could:

  • Inflate failed login counts in security monitoring
  • Trigger account lockouts if combined with rate limiting
  • Confuse security investigations

Suggestion:
Consider a separate event type or different logging approach:

await LogFailedLoginEvent(validatorContext.User, EventType.User_ClientVersionRejected);
// OR
_logger.LogInformation("User {UserId} blocked due to outdated client version {Version}", 
    user.Id, clientVersion);

This distinguishes between security-relevant failed logins and UX issues requiring user action.

Alternatively: If failed login counting is intentional (to prevent brute force with old clients), document this decision in comments.


Finding 5: MinimumClientVersionForV2Encryption constant location

Location: src/Core/KeyManagement/Constants.cs:5
Severity: ❓ QUESTION

Discussion point

The minimum client version is hardcoded:

public static readonly Version MinimumClientVersionForV2Encryption = new("2025.11.0");

Questions:

  1. Should this be configurable via GlobalSettings for emergency version bumps without deployment?
  2. Is there a process for updating this version when new encryption schemes are released?
  3. The comment in User.cs:243 mentions "when a new version comes along they will handle migration" - is there a plan for v3 encryption?

Consideration:
If this needs to be changed urgently (e.g., security vulnerability in 2025.11.0), the current approach requires a code deployment. Feature flags or configuration-based values provide more flexibility.

Not blocking for this PR, but worth considering for future-proofing.


Finding 6: Code quality - Extraction of EncryptionParsing utility

Location: src/Core/KeyManagement/Utilities/EncryptionParsing.cs
Severity: ✅ VERIFIED (Not an issue)

Verification details

The extraction of GetEncryptionType() from RotateUserAccountKeysCommand to a shared utility class is well-executed:

Strengths:

  1. Proper null checking with ArgumentNullException.ThrowIfNull
  2. Consistent error messages
  3. Comprehensive test coverage in EncryptionParsingTests.cs
  4. All call sites updated correctly

Test coverage verified:

  • Null input
  • Invalid formats (missing separator, non-numeric, undefined enum values)
  • All valid encryption types including v2 (XChaCha20Poly1305)

This refactoring improves code reusability and maintainability without introducing issues.


Finding 7: Integration test coverage

Location: test/Identity.IntegrationTest/Login/ClientVersionGateTests.cs
Severity: ✅ VERIFIED (Not an issue)

Verification details

Integration tests provide good end-to-end coverage:

Tested scenarios:

  1. V2 user with old client version (2025.10.0) → Blocked with correct error message
  2. V2 user with minimum client version (2025.11.0) → Succeeds
  3. Unit tests cover: null user, missing header, no private key, no security version

Good practices observed:

  • Tests use UseMockClientVersionValidator = false to test real validator
  • Proper v2 user setup (private key + security version + signature key pair)
  • Error message validation
  • HTTP status code verification

Minor gap: No integration test for v1 user on old client (should allow). This is covered by unit tests but would be valuable for regression prevention.

Not blocking, but consider adding:

[Theory, BitAutoData, RegisterFinishRequestModelCustomize]
public async Task TokenEndpoint_V1User_OnOldClientVersion_Succeeds(...)
{
    // v1 user (no v2 encryption) should authenticate regardless of client version
}

Security Compliance Checklist

Based on Bitwarden's security requirements from CLAUDE.md:

  • Zero-knowledge principles: Not compromised - validator only checks metadata, not vault data
  • Account enumeration prevention: Correctly implemented with post-credential validation
  • Secure defaults: Enforces security (blocks incompatible clients) by default
  • Input validation: Client version parsed safely with Version.TryParse()
  • ⚠️ Logging compliance: Failed login events logged, but may need refinement (Finding 4)
  • Error messages: Generic messages don't leak sensitive information

Recommendations Summary

Must fix before merge (Critical):

  1. Add comprehensive unit tests for User.HasV2Encryption() edge cases

Should address (Important):
2. Clarify and document the breaking change impact of requiring version header for ALL clients, or adjust implementation to only require for v2 users

Consider (Suggested):
3. Use distinct event type for client version rejections vs. failed login attempts
4. Document version constant update process or consider configurability

Questions for team:
5. Confirm breaking change acceptance for v1 users without version header
6. Plan for future encryption version updates (v3, v4, etc.)


Final Verdict

The PR implements a critical security control with proper account enumeration prevention and good test coverage for most components. However, the missing test coverage for the core HasV2Encryption() method is a blocking issue given its importance in preventing data corruption. The breaking change for clients without version headers also needs explicit acknowledgment and documentation.

Once test coverage is added and the breaking change is documented/adjusted, this will be ready for merge.


@Patrick-Pimentel-Bitwarden Patrick-Pimentel-Bitwarden changed the title Auth/pm 22975/client version validator feat(auth-validator): [Auth/PM-22975] Client Version Validator Nov 20, 2025
@Patrick-Pimentel-Bitwarden Patrick-Pimentel-Bitwarden marked this pull request as ready for review November 20, 2025 19:55
@Patrick-Pimentel-Bitwarden
Copy link
Contributor Author

I am seeing 100 failures in tests, double checking what is going on, consider holding off on approving.


namespace Bit.Core.KeyManagement.Queries;

public class IsV2EncryptionUserQuery(IUserSignatureKeyPairRepository userSignatureKeyPairRepository)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, so having thought about this a bit, I think we should remove this query. We can do either user.GetSecurityVersion() >= 2 or implement "IsV2User" on the user class which does the same.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I took this feedback and tweaked it a little. I introduced a narrower scoped check:

user.IsSecurityVersionTwo()

I felt this was more defensive because now we have breadcrumbed places where we are doing branching logic based on a V2 encryption requirement.

While I am happy to be using this simpler approach than the IsV2User query I want to recognize this is a deviation from the ticket's original tech breakdown and should this be discussed to confirm this wouldn't create any issues?

See the changes in 753670d

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So after a round of discussion around the software engineering approach to this solution we arrived at the work that was done f719763

Have a look @quexten and let me know what you think. This approach keeps some attachment to looking at the private key structure which we feel is a more defensive approach than just looking at the SecurityVersion column.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it's a little confusing, but a V2 is every version after and including 2, not just 2. But it's OK to leave it as is right now, since we don't yet have a direct plan for implementing one of the features that would migrate a V2 user to a security version that is larger than 2.

I'm ok with the solution as-is, thanks for spending the time here!

@Patrick-Pimentel-Bitwarden Patrick-Pimentel-Bitwarden marked this pull request as ready for review December 3, 2025 15:02
@Patrick-Pimentel-Bitwarden Patrick-Pimentel-Bitwarden marked this pull request as draft December 3, 2025 15:03
@Patrick-Pimentel-Bitwarden Patrick-Pimentel-Bitwarden marked this pull request as ready for review December 3, 2025 20:18
var sut = new GetMinimumClientVersionForUserQuery();
var version = await sut.Run(new User
{
SecurityVersion = 1,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we add a test for security version being null?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added that test and more in cff2f5d

quexten
quexten previously approved these changes Dec 4, 2025
Copy link
Contributor

@quexten quexten left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving for KM, one optional request for a test

Copy link
Contributor

@JaredSnider-Bitwarden JaredSnider-Bitwarden left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really nice work on this! I have a few questions and change requests below:


// Simple stubs for other encstring versions used by parsing tests
public const string AES256_CBC_B64_Encstring = "0.stub";
public const string AES128_CBC_HMACSHA256_B64_Encstring = "1.stub";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noting: We don't support this encstring type anymore, it does not exist in the real world and client / sdk support was removed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's remove it then

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or mark it as deprecated

public const string AES256_CBC_HMAC_EmptySuffix = "2.";
public const string RSA2048_OAEPSHA256_B64_Encstring = "3.stub";
public const string RSA2048_OAEPSHA1_B64_Encstring = "4.stub";
public const string RSA2048_OAEPSHA256_HMACSHA256_B64_Encstring = "5.stub";
Copy link
Contributor

@quexten quexten Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noting: Between the RSA types, only type 4 is known to exist in the real world.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you like me to leave this comment amongst the testing constants? Or is there another action you would like me to take?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's only add ones that we expect to use

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or we can just keep these as examples

public const string RSA2048_OAEPSHA256_B64_Encstring = "3.stub";
public const string RSA2048_OAEPSHA1_B64_Encstring = "4.stub";
public const string RSA2048_OAEPSHA256_HMACSHA256_B64_Encstring = "5.stub";
public const string RSA2048_OAEPSHA1_HMACSHA256_B64_Encstring = "6.stub";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we add a Type 7 string?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants