Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

[PM-28508] Fix No validation occurs for Expiration date on Self Host licenses#6655

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Open
cyprain-okeke wants to merge3 commits intomain
base:main
Choose a base branch
Loading
frombilling/pm-28508/no-validation-occurs-for-expiration-date-on-self-host-licenses

Conversation

@cyprain-okeke
Copy link
Contributor

🎟️ Tracking

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

📔 Objective

Self-hosted license validations were not properly checking expiration dates, allowing users to manipulate expiration dates in license files to be far into the future (e.g., year 3000) and avoid subscription expiration. This security vulnerability allowed tampered licenses to be uploaded and remain valid even after the periodic validation job ran.

Replication Steps

  1. Download a license from US_QA
  2. Modify theexpires field to a far future date (e.g., year 3000)
  3. Upload the license to self-hosted instance
  4. Actual Result: License uploads successfully without validation
  5. Wait for periodic validation job (runs every 12 hours)
  6. Actual Result: License remains valid

Solution

Implemented multiple layers of defense to prevent license expiration date tampering:

  1. Hash Verification: Added hash verification as the first check during license upload to detect tampering with license file content
  2. File Expires vs Token Expires Check: Primary defense that compares the file'sExpires property against the cryptographically signed token'sExpires claim
  3. 10-Year Limit Check: Secondary defense that rejects expiration dates more than 10 years in the future
  4. Periodic Validation: Enhanced the periodic validation job to catch tampered licenses and disable organizations

📸 Screenshots

⏰ 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

@claude
Copy link
Contributor

claudebot commentedNov 27, 2025
edited
Loading

Claude finished@cyprain-okeke's task ——View job


Review Update: Test Fixes Analysis

Changes Since Last Review

Two commits pushed since initial review (both test fixes):

  • 1150ebcd2 - EnhancedSelfHostedOrganizationSignUpCommandTests.cs with proper claim setup
  • 44ee22230 - FixedOrganizationLicenseTests.cs to use reasonable expiration dates

Status of Previous Findings

None of my previous findings have been addressed - the two new commits only fix broken tests caused by the new validation logic. All inline comments remain unresolved:

Previous Findings Summary (Click to expand)

Finding 1 (💭): Clock skew tolerance of 60 seconds - questioned if sufficient
Finding 2 (💭): 10-year limit configurability - questioned if hardcoded value handles edge cases
Finding 3 (🎨): Security logging suggestion - add telemetry for tampering detection
Finding 4 (⚠️): Misleading comment - claims hash detects expiration tampering (it doesn't)
Finding 5 (🎨): Test coverage - add exact 10-year boundary test
Finding 6 (❌): Security asymmetry - licenses without token Expires claims have weaker protection
Finding 7 (⚠️): ObsoleteVerifyData weakness - suggested adding hash verification for defense-in-depth

Analysis of New Test Changes

Good practices observed:

  • Comprehensive test fixture enhancement withCreateMatchingClaims() helper
  • Proper claim-to-license synchronization ensuring VerifyData validation passes
  • FixedDateTime.MaxValue issue (year 9999) that violated new 10-year limit

New finding identified:

Finding 8 (⚠️): Test Maintenance Burden

The newCreateMatchingClaims() helper atSelfHostedOrganizationSignUpCommandTests.cs:344-412 duplicates 70+ lines of claim construction logic. This creates maintenance burden:

Why This Matters
  1. Duplication: Every new license property requires updates in multiple test files
  2. Brittle Tests: Tests break when license schema changes, even if functionality is correct
  3. No Single Source of Truth: Claim construction logic should be centralized

Recommendation: Consider extracting to a shared test helper (e.g.,LicenseTestHelpers.CreateMatchingClaimsFromLicense(license)) or using a builder pattern. This would:

  • Reduce test maintenance burden
  • Ensure consistency across test files
  • Make tests more resilient to schema changes

Summary

The changes aretest-only fixes to accommodate the new validation logic. The core security implementation remains unchanged from my initial review. All previous findings remain relevant and unaddressed.

Recommendation: Address inline comment findings, particularly Finding 4 (misleading comment) and Finding 6 (security asymmetry for licenses without Expires claims).

/// Maximum tolerance in seconds for comparing file expiration date with token expiration claim.
/// Allows for clock skew between systems.
/// </summary>
privateconstintExpirationToleranceSeconds=60;
Copy link
Contributor

Choose a reason for hiding this comment

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

💭Question: Is 60 seconds sufficient for clock skew tolerance in production environments?

Many production systems use 5 minutes (300 seconds) to account for moderate clock drift between servers. Systems using NTP typically maintain <1 second accuracy, but poorly configured systems can drift more.

Considerations:

  • 60 seconds may be too strict for some self-hosted deployments
  • However, being stricter reduces the attack window for time-based exploits
  • Consider documenting the expectation that self-hosted instances should use NTP

What was the rationale for choosing 60 seconds?

/// Maximum reasonable expiration period in years from the current date.
/// Used to prevent unreasonably far future dates (e.g., year 3000) from being accepted.
/// </summary>
privateconstintMaxReasonableExpirationYears=10;
Copy link
Contributor

Choose a reason for hiding this comment

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

💭Question: Should the 10-year limit be configurable for legitimate long-term enterprise licenses?

While 10 years is reasonable for most subscriptions, some considerations:

  1. Lifetime licenses: Some enterprises may have perpetual/lifetime license agreements
  2. Long-term contracts: 15-20 year contracts exist in some industries
  3. Hardcoded limit: Currently not configurable, which may cause issues for edge cases

Options:

  • Make this configurable viaGlobalSettings with 10 years as default
  • Increase to 15-20 years to cover more edge cases
  • Add special handling/flag for lifetime licenses
  • Document that licenses beyond 10 years require special configuration

What's the policy for handling exceptionally long-term license agreements?

varcomputedHash=Convert.ToBase64String(license.ComputeHash());
if(!computedHash.Equals(license.Hash,StringComparison.Ordinal))
{
awaitDisableOrganizationAsync(org,license,"License file has been tampered with (hash mismatch). The license file content does not match the original hash.");
Copy link
Contributor

Choose a reason for hiding this comment

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

🎨Security Logging Suggestion: When tampering is detected, consider adding security telemetry:

if(!computedHash.Equals(license.Hash,StringComparison.Ordinal)){_logger.LogWarning("License tampering detected for organization {OrganizationId}. Hash mismatch indicates file modification.",org.Id);awaitDisableOrganizationAsync(org,license,"License file has been tampered with (hash mismatch)...");continue;}

This would help detect:

  • Systematic tampering attempts across multiple organizations
  • Patterns indicating coordinated attacks
  • Troubleshooting legitimate issues (e.g., file corruption)

Security events for tampering detection are valuable for SOC 2 compliance and incident response.

thrownewBadRequestException("License is already in use by another organization.");
}

// Verify hash FIRST to detect tampering with license file content before any other validation
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️Misleading Comment: This comment states hash verification is "critical" for detecting tampering, but the hash doesn't includeExpires,Issued, orRefresh dates (see OrganizationLicense.cs:232-237).

Hash verification will NOT catch expiration date tampering. The actual protection against expiration tampering comes from:

  1. Token claims comparison inVerifyData (line 75)
  2. The 10-year limit check withinVerifyData

Recommendation: Update the comment to be more precise:

// Verify hash FIRST to detect tampering with license file content (seats, features, etc.)// Note: Hash does NOT include Expires/Issued/Refresh, so expiration tampering is caught by// token comparison in VerifyData() which compares file Expires against token Expires claim

varinstallationId=Guid.NewGuid();
varorganizationId=Guid.NewGuid();
varlicenseKey="test-license-key";
vartokenExpires=DateTime.UtcNow.AddYears(11);// More than 10 years
Copy link
Contributor

Choose a reason for hiding this comment

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

🎨Test Coverage Suggestion: Consider adding a test for the exact 10-year boundary:

[Fact]publicvoidVerifyData_FileExpiresIsExactly10Years_ReturnsTrue(){// Test that exactly 10 years is still validvartokenExpires=DateTime.UtcNow.AddYears(10);varfileExpires=tokenExpires;// ... rest of test setup// Assert: Should be valid}

This ensures the boundary condition is clearly defined and won't accidentally reject valid 10-year licenses due to off-by-one errors.

// Note: expires comes from the token (authoritative source), Expires comes from the file (may be tampered)

// If token has Expires claim, file Expires must match it
if(expires!=default(DateTime))
Copy link
Contributor

Choose a reason for hiding this comment

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

Critical Security Check: This is the PRIMARY defense against expiration date tampering. The hash check earlier (line 234-237) does NOT includeExpires, so this token comparison is essential.

However, there's a subtle issue with the fallback logic (lines 432-433): "If token doesn't have Expires claim, we can't validate file Expires against it. In this case, we rely on other validations (hash, 10-year limit, etc.)"

The problem: Hash verification doesn't include Expires, so for licenses without token Expires claims, only the 10-year limit protects against expiration tampering. This creates a security asymmetry.

Recommendation: Consider requiring Expires claim in new tokens, and potentially phasing out support for tokens without Expires claims after a deprecation period.

// Validate that the expiration date is not unreasonably far in the future
// This prevents dates like year 3000 from being accepted
varmaxReasonableExpiration=DateTime.UtcNow.AddYears(MaxReasonableExpirationYears);
if(Expires.Value>maxReasonableExpiration)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️Logic Ordering: The validation checks expiration limits (line 538) after checking if expired (line 530), but this is fine since both are failing conditions.

However, consider adding hash verification here too for defense-in-depth, since obsolete licenses without tokens have weaker tamper protection. See comment on line 343 for suggested implementation.

@github-actions
Copy link
Contributor

github-actionsbot commentedNov 27, 2025
edited
Loading

Logo
Checkmarx One – Scan Summary & Details8d852d50-b44a-438d-a22d-ea01a70c100f

Great job! No new security vulnerabilities introduced in this pull request

@codecov
Copy link

codecovbot commentedNov 27, 2025

Codecov Report

❌ Patch coverage is31.76471% with58 lines in your changes missing coverage. Please review.
✅ Project coverage is 53.27%. Comparing base (d3e4ac7) to head (44ee222).

Files with missing linesPatch %Lines
...lling/Services/Implementations/LicensingService.cs3.44%28 Missing⚠️
...ganizations/SelfHostedOrganizationSignUpCommand.cs0.00%8 Missing and 2 partials⚠️
...tions/Commands/UpdateOrganizationLicenseCommand.cs30.76%7 Missing and 2 partials⚠️
...e/Billing/Licenses/Extensions/LicenseExtensions.cs22.22%5 Missing and 2 partials⚠️
...illing/Organizations/Models/OrganizationLicense.cs83.33%2 Missing and 2 partials⚠️
Additional details and impacted files
@@           Coverage Diff           @@##             main    #6655   +/-   ##=======================================  Coverage   53.27%   53.27%           =======================================  Files        1906     1906             Lines       84973    85054   +81       Branches     7639     7660   +21     =======================================+ Hits        45268    45313   +45- Misses      37952    37978   +26- Partials     1753     1763   +10

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report?Share it here.

🚀 New features to boost your workflow:
  • ❄️Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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

Reviewers

@claudeclaude[bot]claude[bot] left review comments

@sbrown-livefrontsbrown-livefrontAwaiting requested review from sbrown-livefrontsbrown-livefront is a code owner automatically assigned from bitwarden/team-billing-dev

@r-tomer-tomeAwaiting requested review from r-tomer-tome is a code owner automatically assigned from bitwarden/team-admin-console-dev

At least 1 approving review is required to merge this pull request.

Assignees

No one assigned

Labels

None yet

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

2 participants

@cyprain-okeke

[8]ページ先頭

©2009-2025 Movatter.jp