mirror of
https://github.com/bitwarden/server.git
synced 2026-01-24 10:53:10 +08:00
PM-30799 added validation for DomainName (#6856)
This commit is contained in:
@@ -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; }
|
||||
}
|
||||
|
||||
64
src/Core/Utilities/DomainNameAttribute.cs
Normal file
64
src/Core/Utilities/DomainNameAttribute.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
84
test/Core.Test/Utilities/DomainNameAttributeTests.cs
Normal file
84
test/Core.Test/Utilities/DomainNameAttributeTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user