diff --git a/src/Api/AdminConsole/Models/Request/OrganizationDomainRequestModel.cs b/src/Api/AdminConsole/Models/Request/OrganizationDomainRequestModel.cs
index 46b253da31..3a2ada719f 100644
--- a/src/Api/AdminConsole/Models/Request/OrganizationDomainRequestModel.cs
+++ b/src/Api/AdminConsole/Models/Request/OrganizationDomainRequestModel.cs
@@ -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; }
}
diff --git a/src/Core/Utilities/DomainNameAttribute.cs b/src/Core/Utilities/DomainNameAttribute.cs
new file mode 100644
index 0000000000..9b571e96d7
--- /dev/null
+++ b/src/Core/Utilities/DomainNameAttribute.cs
@@ -0,0 +1,64 @@
+using System.ComponentModel.DataAnnotations;
+using System.Text.RegularExpressions;
+
+namespace Bit.Core.Utilities;
+
+///
+/// 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.
+///
+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);
+ }
+}
diff --git a/test/Core.Test/Utilities/DomainNameAttributeTests.cs b/test/Core.Test/Utilities/DomainNameAttributeTests.cs
new file mode 100644
index 0000000000..3f3190c9a1
--- /dev/null
+++ b/test/Core.Test/Utilities/DomainNameAttributeTests.cs
@@ -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("")] // XSS attempt
+ [InlineData("example.com