- Notifications
You must be signed in to change notification settings - Fork1.5k
[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
base:main
Are you sure you want to change the base?
Conversation
claudebot commentedNov 27, 2025 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
Claude finished@cyprain-okeke's task ——View job Review Update: Test Fixes AnalysisChanges Since Last ReviewTwo commits pushed since initial review (both test fixes):
Status of Previous FindingsNone 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 Analysis of New Test ChangesGood practices observed:
New finding identified: Finding 8 ( |
| /// Maximum tolerance in seconds for comparing file expiration date with token expiration claim. | ||
| /// Allows for clock skew between systems. | ||
| /// </summary> | ||
| privateconstintExpirationToleranceSeconds=60; |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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:
- Lifetime licenses: Some enterprises may have perpetual/lifetime license agreements
- Long-term contracts: 15-20 year contracts exist in some industries
- Hardcoded limit: Currently not configurable, which may cause issues for edge cases
Options:
- Make this configurable via
GlobalSettingswith 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."); |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Expires,Issued, orRefresh dates (see OrganizationLicense.cs:232-237).
Hash verification will NOT catch expiration date tampering. The actual protection against expiration tampering comes from:
- Token claims comparison in
VerifyData(line 75) - The 10-year limit check within
VerifyData
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 |
There was a problem hiding this comment.
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)) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
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-actionsbot commentedNov 27, 2025 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
Great job! No new security vulnerabilities introduced in this pull request |
Codecov Report❌ Patch coverage is 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. 🚀 New features to boost your workflow:
|

🎟️ 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
expiresfield to a far future date (e.g., year 3000)Solution
Implemented multiple layers of defense to prevent license expiration date tampering:
Expiresproperty against the cryptographically signed token'sExpiresclaim📸 Screenshots
⏰ Reminders before review
🦮 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