Skip to content

Commit b623e38

Browse files
PM-30799 added validation for DomainName (#6856)
1 parent 867e616 commit b623e38

3 files changed

Lines changed: 150 additions & 0 deletions

File tree

src/Api/AdminConsole/Models/Request/OrganizationDomainRequestModel.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22
#nullable disable
33

44
using System.ComponentModel.DataAnnotations;
5+
using Bit.Core.Utilities;
56

67
namespace Bit.Api.AdminConsole.Models.Request;
78

89
public class OrganizationDomainRequestModel
910
{
1011
[Required]
12+
[DomainNameValidator]
1113
public string DomainName { get; set; }
1214
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
using System.ComponentModel.DataAnnotations;
2+
using System.Text.RegularExpressions;
3+
4+
namespace Bit.Core.Utilities;
5+
6+
/// <summary>
7+
/// https://bitwarden.atlassian.net/browse/VULN-376
8+
/// Domain names are vulnerable to XSS attacks if not properly validated.
9+
/// Domain names can contain letters, numbers, dots, and hyphens.
10+
/// Domain names maybe internationalized (IDN) and contain unicode characters.
11+
/// </summary>
12+
public class DomainNameValidatorAttribute : ValidationAttribute
13+
{
14+
// RFC 1123 compliant domain name regex
15+
// - Allows alphanumeric characters and hyphens
16+
// - Cannot start or end with a hyphen
17+
// - Each label (part between dots) must be 1-63 characters
18+
// - Total length should not exceed 253 characters
19+
// - Supports internationalized domain names (IDN) - which is why this regex includes unicode ranges
20+
private static readonly Regex _domainNameRegex = new(
21+
@"^(?:[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])?$",
22+
RegexOptions.Compiled | RegexOptions.IgnoreCase
23+
);
24+
25+
public DomainNameValidatorAttribute()
26+
: base("The {0} field is not a valid domain name.")
27+
{ }
28+
29+
public override bool IsValid(object? value)
30+
{
31+
if (value == null)
32+
{
33+
return true; // Use [Required] for null checks
34+
}
35+
36+
var domainName = value.ToString();
37+
38+
if (string.IsNullOrWhiteSpace(domainName))
39+
{
40+
return false;
41+
}
42+
43+
// Reject if contains any whitespace (including leading/trailing spaces, tabs, newlines)
44+
if (domainName.Any(char.IsWhiteSpace))
45+
{
46+
return false;
47+
}
48+
49+
// Check length constraints
50+
if (domainName.Length > 253)
51+
{
52+
return false;
53+
}
54+
55+
// Check for control characters or other dangerous characters
56+
if (domainName.Any(c => char.IsControl(c) || c == '<' || c == '>' || c == '"' || c == '\'' || c == '&'))
57+
{
58+
return false;
59+
}
60+
61+
// Validate against domain name regex
62+
return _domainNameRegex.IsMatch(domainName);
63+
}
64+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
using Bit.Core.Utilities;
2+
using Xunit;
3+
4+
namespace Bit.Core.Test.Utilities;
5+
6+
public class DomainNameValidatorAttributeTests
7+
{
8+
[Theory]
9+
[InlineData("example.com")] // basic domain
10+
[InlineData("sub.example.com")] // subdomain
11+
[InlineData("sub.sub2.example.com")] // multiple subdomains
12+
[InlineData("example-dash.com")] // domain with dash
13+
[InlineData("123example.com")] // domain starting with number
14+
[InlineData("example123.com")] // domain with numbers
15+
[InlineData("e.com")] // short domain
16+
[InlineData("very-long-subdomain-name.example.com")] // long subdomain
17+
[InlineData("wörldé.com")] // unicode domain (IDN)
18+
public void IsValid_ReturnsTrueWhenValid(string domainName)
19+
{
20+
var sut = new DomainNameValidatorAttribute();
21+
22+
var actual = sut.IsValid(domainName);
23+
24+
Assert.True(actual);
25+
}
26+
27+
[Theory]
28+
[InlineData("<script>alert('xss')</script>")] // XSS attempt
29+
[InlineData("example.com<script>")] // XSS suffix
30+
[InlineData("<img src=x>")] // HTML tag
31+
[InlineData("example.com\t")] // trailing tab
32+
[InlineData("\texample.com")] // leading tab
33+
[InlineData("exam\tple.com")] // middle tab
34+
[InlineData("example.com\n")] // newline
35+
[InlineData("example.com\r")] // carriage return
36+
[InlineData("example.com\b")] // backspace
37+
[InlineData("exam ple.com")] // space in domain
38+
[InlineData("example.com ")] // trailing space (after trim, becomes valid, but with space it's invalid)
39+
[InlineData(" example.com")] // leading space (after trim, becomes valid, but with space it's invalid)
40+
[InlineData("example&.com")] // ampersand
41+
[InlineData("example'.com")] // single quote
42+
[InlineData("example\".com")] // double quote
43+
[InlineData(".example.com")] // starts with dot
44+
[InlineData("example.com.")] // ends with dot
45+
[InlineData("example..com")] // double dot
46+
[InlineData("-example.com")] // starts with dash
47+
[InlineData("example-.com")] // label ends with dash
48+
[InlineData("")] // empty string
49+
[InlineData(" ")] // whitespace only
50+
[InlineData("http://example.com")] // URL scheme
51+
[InlineData("example.com/path")] // path component
52+
[InlineData("user@example.com")] // email format
53+
public void IsValid_ReturnsFalseWhenInvalid(string domainName)
54+
{
55+
var sut = new DomainNameValidatorAttribute();
56+
57+
var actual = sut.IsValid(domainName);
58+
59+
Assert.False(actual);
60+
}
61+
62+
[Fact]
63+
public void IsValid_ReturnsTrueWhenNull()
64+
{
65+
var sut = new DomainNameValidatorAttribute();
66+
67+
var actual = sut.IsValid(null);
68+
69+
// Null validation should be handled by [Required] attribute
70+
Assert.True(actual);
71+
}
72+
73+
[Fact]
74+
public void IsValid_ReturnsFalseWhenTooLong()
75+
{
76+
var sut = new DomainNameValidatorAttribute();
77+
// Create a domain name longer than 253 characters
78+
var longDomain = new string('a', 250) + ".com";
79+
80+
var actual = sut.IsValid(longDomain);
81+
82+
Assert.False(actual);
83+
}
84+
}

0 commit comments

Comments
 (0)