1
0
mirror of https://github.com/bitwarden/server synced 2026-01-26 14:23:21 +00:00

Merge branch 'main' into ac/pm-28842/cap-password-minimum-length

This commit is contained in:
Rui Tomé
2026-01-23 14:43:37 +00:00
committed by GitHub
9 changed files with 225 additions and 12 deletions

View File

@@ -31,7 +31,7 @@ jobs:
persist-credentials: false
- name: Set up .NET
uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0
- name: Verify format
run: dotnet format --verify-no-changes
@@ -119,10 +119,10 @@ jobs:
fi
- name: Set up .NET
uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0
- name: Set up Node
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
cache: "npm"
cache-dependency-path: "**/package-lock.json"
@@ -294,7 +294,7 @@ jobs:
persist-credentials: false
- name: Set up .NET
uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0
- name: Log in to Azure
uses: bitwarden/gh-actions/azure-login@main
@@ -420,7 +420,7 @@ jobs:
persist-credentials: false
- name: Set up .NET
uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0
- name: Print environment
run: |

View File

@@ -49,7 +49,7 @@ jobs:
persist-credentials: false
- name: Set up .NET
uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0
- name: Restore tools
run: dotnet tool restore
@@ -156,7 +156,7 @@ jobs:
run: 'docker logs "$(docker ps --quiet --filter "name=mssql")"'
- name: Report test results
uses: dorny/test-reporter@fe45e9537387dac839af0d33ba56eed8e24189e8 # v2.3.0
uses: dorny/test-reporter@b082adf0eced0765477756c2a610396589b8c637 # v2.5.0
if: ${{ github.event.pull_request.head.repo.full_name == github.repository && !cancelled() }}
with:
name: Test Results
@@ -183,7 +183,7 @@ jobs:
persist-credentials: false
- name: Set up .NET
uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0
- name: Print environment
run: |

View File

@@ -32,7 +32,7 @@ jobs:
persist-credentials: false
- name: Set up .NET
uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0
- name: Install rust
uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # stable
@@ -59,7 +59,7 @@ jobs:
run: dotnet test ./bitwarden_license/test --configuration Debug --logger "trx;LogFileName=bw-test-results.trx" /p:CoverletOutputFormatter="cobertura" --collect:"XPlat Code Coverage"
- name: Report test results
uses: dorny/test-reporter@fe45e9537387dac839af0d33ba56eed8e24189e8 # v2.3.0
uses: dorny/test-reporter@b082adf0eced0765477756c2a610396589b8c637 # v2.5.0
if: ${{ github.event.pull_request.head.repo.full_name == github.repository && !cancelled() }}
with:
name: Test Results

View File

@@ -2,11 +2,13 @@
#nullable disable
using System.ComponentModel.DataAnnotations;
using Bit.Core.Utilities;
namespace Bit.Api.AdminConsole.Models.Request;
public class OrganizationDomainRequestModel
{
[Required]
[DomainNameValidator]
public string DomainName { get; set; }
}

View File

@@ -67,8 +67,9 @@ public class CollectionsController : Controller
{
var collections = await _collectionRepository.GetManyByOrganizationIdWithAccessAsync(_currentContext.OrganizationId.Value);
var collectionResponses = collections.Select(c =>
new CollectionResponseModel(c.Item1, c.Item2.Groups));
var collectionResponses = collections
.Where(c => c.Item1.Type != CollectionType.DefaultUserCollection)
.Select(c => new CollectionResponseModel(c.Item1, c.Item2.Groups));
var response = new ListResponseModel<CollectionResponseModel>(collectionResponses);
return new JsonResult(response);

View File

@@ -174,6 +174,7 @@ public static class FeatureFlagKeys
public const string MacOsNativeCredentialSync = "macos-native-credential-sync";
public const string WindowsDesktopAutotype = "windows-desktop-autotype";
public const string WindowsDesktopAutotypeGA = "windows-desktop-autotype-ga";
public const string NotificationUndeterminedCipherScenarioLogic = "undetermined-cipher-scenario-logic";
/* Billing Team */
public const string TrialPayment = "PM-8163-trial-payment";

View File

@@ -0,0 +1,64 @@
using System.ComponentModel.DataAnnotations;
using System.Text.RegularExpressions;
namespace Bit.Core.Utilities;
/// <summary>
/// https://bitwarden.atlassian.net/browse/VULN-376
/// Domain names are vulnerable to XSS attacks if not properly validated.
/// Domain names can contain letters, numbers, dots, and hyphens.
/// Domain names maybe internationalized (IDN) and contain unicode characters.
/// </summary>
public class DomainNameValidatorAttribute : ValidationAttribute
{
// RFC 1123 compliant domain name regex
// - Allows alphanumeric characters and hyphens
// - Cannot start or end with a hyphen
// - Each label (part between dots) must be 1-63 characters
// - Total length should not exceed 253 characters
// - Supports internationalized domain names (IDN) - which is why this regex includes unicode ranges
private static readonly Regex _domainNameRegex = new(
@"^(?:[a-zA-Z0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF](?:[a-zA-Z0-9\-\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]{0,61}[a-zA-Z0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])?\.)*[a-zA-Z0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF](?:[a-zA-Z0-9\-\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]{0,61}[a-zA-Z0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])?$",
RegexOptions.Compiled | RegexOptions.IgnoreCase
);
public DomainNameValidatorAttribute()
: base("The {0} field is not a valid domain name.")
{ }
public override bool IsValid(object? value)
{
if (value == null)
{
return true; // Use [Required] for null checks
}
var domainName = value.ToString();
if (string.IsNullOrWhiteSpace(domainName))
{
return false;
}
// Reject if contains any whitespace (including leading/trailing spaces, tabs, newlines)
if (domainName.Any(char.IsWhiteSpace))
{
return false;
}
// Check length constraints
if (domainName.Length > 253)
{
return false;
}
// Check for control characters or other dangerous characters
if (domainName.Any(c => char.IsControl(c) || c == '<' || c == '>' || c == '"' || c == '\'' || c == '&'))
{
return false;
}
// Validate against domain name regex
return _domainNameRegex.IsMatch(domainName);
}
}

View File

@@ -6,6 +6,7 @@ using Bit.Api.Models.Public.Response;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Enums;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Models.Data;
using Bit.Core.Platform.Push;
@@ -114,4 +115,64 @@ public class CollectionsControllerTests : IClassFixture<ApiApplicationFactory>,
Assert.NotEmpty(result.Item2.Groups);
Assert.NotEmpty(result.Item2.Users);
}
[Fact]
public async Task List_ExcludesDefaultUserCollections_IncludesGroupsAndUsers()
{
// Arrange
var collectionRepository = _factory.GetService<ICollectionRepository>();
var groupRepository = _factory.GetService<IGroupRepository>();
var defaultCollection = new Collection
{
OrganizationId = _organization.Id,
Name = "My Items",
Type = CollectionType.DefaultUserCollection
};
await collectionRepository.CreateAsync(defaultCollection, null, null);
var group = await groupRepository.CreateAsync(new Group
{
OrganizationId = _organization.Id,
Name = "Test Group",
ExternalId = $"test-group-{Guid.NewGuid()}",
});
var (_, user) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(
_factory,
_organization.Id,
OrganizationUserType.User);
var sharedCollection = await OrganizationTestHelpers.CreateCollectionAsync(
_factory,
_organization.Id,
"Shared Collection with Access",
externalId: "shared-collection-with-access",
groups:
[
new CollectionAccessSelection { Id = group.Id, ReadOnly = false, HidePasswords = false, Manage = true }
],
users:
[
new CollectionAccessSelection { Id = user.Id, ReadOnly = true, HidePasswords = true, Manage = false }
]);
// Act
var response = await _client.GetFromJsonAsync<ListResponseModel<CollectionResponseModel>>("public/collections");
// Assert
Assert.NotNull(response);
Assert.DoesNotContain(response.Data, c => c.Id == defaultCollection.Id);
var collectionResponse = response.Data.First(c => c.Id == sharedCollection.Id);
Assert.NotNull(collectionResponse.Groups);
Assert.Single(collectionResponse.Groups);
var groupResponse = collectionResponse.Groups.First();
Assert.Equal(group.Id, groupResponse.Id);
Assert.False(groupResponse.ReadOnly);
Assert.False(groupResponse.HidePasswords);
Assert.True(groupResponse.Manage);
}
}

View File

@@ -0,0 +1,84 @@
using Bit.Core.Utilities;
using Xunit;
namespace Bit.Core.Test.Utilities;
public class DomainNameValidatorAttributeTests
{
[Theory]
[InlineData("example.com")] // basic domain
[InlineData("sub.example.com")] // subdomain
[InlineData("sub.sub2.example.com")] // multiple subdomains
[InlineData("example-dash.com")] // domain with dash
[InlineData("123example.com")] // domain starting with number
[InlineData("example123.com")] // domain with numbers
[InlineData("e.com")] // short domain
[InlineData("very-long-subdomain-name.example.com")] // long subdomain
[InlineData("wörldé.com")] // unicode domain (IDN)
public void IsValid_ReturnsTrueWhenValid(string domainName)
{
var sut = new DomainNameValidatorAttribute();
var actual = sut.IsValid(domainName);
Assert.True(actual);
}
[Theory]
[InlineData("<script>alert('xss')</script>")] // XSS attempt
[InlineData("example.com<script>")] // XSS suffix
[InlineData("<img src=x>")] // HTML tag
[InlineData("example.com\t")] // trailing tab
[InlineData("\texample.com")] // leading tab
[InlineData("exam\tple.com")] // middle tab
[InlineData("example.com\n")] // newline
[InlineData("example.com\r")] // carriage return
[InlineData("example.com\b")] // backspace
[InlineData("exam ple.com")] // space in domain
[InlineData("example.com ")] // trailing space (after trim, becomes valid, but with space it's invalid)
[InlineData(" example.com")] // leading space (after trim, becomes valid, but with space it's invalid)
[InlineData("example&.com")] // ampersand
[InlineData("example'.com")] // single quote
[InlineData("example\".com")] // double quote
[InlineData(".example.com")] // starts with dot
[InlineData("example.com.")] // ends with dot
[InlineData("example..com")] // double dot
[InlineData("-example.com")] // starts with dash
[InlineData("example-.com")] // label ends with dash
[InlineData("")] // empty string
[InlineData(" ")] // whitespace only
[InlineData("http://example.com")] // URL scheme
[InlineData("example.com/path")] // path component
[InlineData("user@example.com")] // email format
public void IsValid_ReturnsFalseWhenInvalid(string domainName)
{
var sut = new DomainNameValidatorAttribute();
var actual = sut.IsValid(domainName);
Assert.False(actual);
}
[Fact]
public void IsValid_ReturnsTrueWhenNull()
{
var sut = new DomainNameValidatorAttribute();
var actual = sut.IsValid(null);
// Null validation should be handled by [Required] attribute
Assert.True(actual);
}
[Fact]
public void IsValid_ReturnsFalseWhenTooLong()
{
var sut = new DomainNameValidatorAttribute();
// Create a domain name longer than 253 characters
var longDomain = new string('a', 250) + ".com";
var actual = sut.IsValid(longDomain);
Assert.False(actual);
}
}