Skip to content

Commit ac3db8d

Browse files
Added email validation to CaptureFeedback methods (#4284)
Resolves #4246 - #4246
1 parent 33665ed commit ac3db8d

File tree

4 files changed

+145
-1
lines changed

4 files changed

+145
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
- ExtraData not captured for Breadcrumbs in MauiEventsBinder ([#4254](https://github.com/getsentry/sentry-dotnet/pull/4254))
1515
- NOTE: Required breaking changes to the public API of `Sentry.Maui.BreadcrumbEvent`, while keeping an _Obsolete_ constructor for backward compatibility.
1616
- InvalidOperationException sending attachments on Android with LLVM enabled ([#4276](https://github.com/getsentry/sentry-dotnet/pull/4276))
17+
- When CaptureFeedback methods are called with invalid email addresses, the email address will be removed and, if Debug mode is enabled, a warning will be logged. This is done to avoid losing the Feedback altogether (Sentry would reject Feedback that has an invalid email address) ([#4284](https://github.com/getsentry/sentry-dotnet/pull/4284))
1718

1819
### Dependencies
1920

src/Sentry/Internal/EmailValidator.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using System.Text.RegularExpressions;
2+
3+
namespace Sentry.Internal;
4+
5+
/// <summary>
6+
/// Helper class for email validation.
7+
/// </summary>
8+
internal static partial class EmailValidator
9+
{
10+
private const string EmailPattern = @"^[^@\s]+@[^@\s]+\.[^@\s]+$";
11+
12+
#if NET9_0_OR_GREATER
13+
[GeneratedRegex(EmailPattern)]
14+
private static partial Regex Email { get; }
15+
#elif NET8_0
16+
[GeneratedRegex(EmailPattern)]
17+
private static partial Regex EmailRegex();
18+
private static readonly Regex Email = EmailRegex();
19+
#else
20+
private static readonly Regex Email = new(EmailPattern, RegexOptions.Compiled);
21+
#endif
22+
23+
/// <summary>
24+
/// Validates an email address.
25+
/// </summary>
26+
/// <param name="email">The email address to validate.</param>
27+
/// <returns>True if the email is valid, false otherwise.</returns>
28+
public static bool IsValidEmail(string email) => Email.IsMatch(email);
29+
}

src/Sentry/Internal/Hub.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -573,6 +573,12 @@ public void CaptureFeedback(SentryFeedback feedback, Scope? scope = null, Sentry
573573

574574
try
575575
{
576+
if (!string.IsNullOrWhiteSpace(feedback.ContactEmail) && !EmailValidator.IsValidEmail(feedback.ContactEmail))
577+
{
578+
_options.LogWarning("Feedback email scrubbed due to invalid email format: '{0}'", feedback.ContactEmail);
579+
feedback.ContactEmail = null;
580+
}
581+
576582
scope ??= CurrentScope;
577583
CurrentClient.CaptureFeedback(feedback, scope, hint);
578584
}
@@ -620,6 +626,16 @@ public void CaptureUserFeedback(UserFeedback userFeedback)
620626

621627
try
622628
{
629+
if (!string.IsNullOrWhiteSpace(userFeedback.Email) && !EmailValidator.IsValidEmail(userFeedback.Email))
630+
{
631+
_options.LogWarning("Feedback email scrubbed due to invalid email format: '{0}'", userFeedback.Email);
632+
userFeedback = new UserFeedback(
633+
userFeedback.EventId,
634+
userFeedback.Name,
635+
null, // Scrubbed email
636+
userFeedback.Comments);
637+
}
638+
623639
CurrentClient.CaptureUserFeedback(userFeedback);
624640
}
625641
catch (Exception e)

test/Sentry.Tests/HubTests.cs

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.IO.Abstractions.TestingHelpers;
22
using Sentry.Internal.Http;
3+
using Sentry.Protocol;
34
using Sentry.Tests.Internals;
45

56
namespace Sentry.Tests;
@@ -1742,7 +1743,7 @@ public void CaptureUserFeedback_HubEnabled(bool enabled)
17421743
hub.Dispose();
17431744
}
17441745

1745-
var feedback = new UserFeedback(SentryId.Create(), "foo", "bar", "baz");
1746+
var feedback = new UserFeedback(SentryId.Create(), "foo", "bar@example.com", "baz");
17461747

17471748
// Act
17481749
hub.CaptureUserFeedback(feedback);
@@ -1890,6 +1891,103 @@ await transport.Received(1)
18901891
}
18911892

18921893
private static Scope GetCurrentScope(Hub hub) => hub.ScopeManager.GetCurrent().Key;
1894+
1895+
[Theory]
1896+
[InlineData(null)]
1897+
[InlineData("")]
1898+
[InlineData(" ")]
1899+
[InlineData("test@example.com")]
1900+
[InlineData("user.name@domain.com")]
1901+
[InlineData("user+tag@example.com")]
1902+
public void CaptureFeedback_ValidEmail_FeedbackRegistered(string email)
1903+
{
1904+
// Arrange
1905+
var hub = _fixture.GetSut();
1906+
var feedback = new SentryFeedback("Test feedback", email);
1907+
1908+
// Act
1909+
hub.CaptureFeedback(feedback);
1910+
1911+
// Assert
1912+
_fixture.Client.Received(1).CaptureFeedback(Arg.Any<SentryFeedback>(), Arg.Any<Scope>(), Arg.Any<SentryHint>());
1913+
}
1914+
1915+
[Theory]
1916+
[InlineData("invalid-email")]
1917+
[InlineData("missing@domain")]
1918+
[InlineData("@missing-local.com")]
1919+
[InlineData("spaces in@email.com")]
1920+
public void CaptureFeedback_InvalidEmail_FeedbackDropped(string email)
1921+
{
1922+
// Arrange
1923+
_fixture.Options.Debug = true;
1924+
_fixture.Options.DiagnosticLogger = Substitute.For<IDiagnosticLogger>();
1925+
_fixture.Options.DiagnosticLogger!.IsEnabled(Arg.Any<SentryLevel>()).Returns(true);
1926+
var hub = _fixture.GetSut();
1927+
var feedback = new SentryFeedback("Test feedback", email);
1928+
1929+
// Act
1930+
hub.CaptureFeedback(feedback);
1931+
1932+
// Assert
1933+
_fixture.Options.DiagnosticLogger.Received(1).Log(
1934+
SentryLevel.Warning,
1935+
Arg.Is<string>(s => s.Contains("invalid email format")),
1936+
null,
1937+
Arg.Any<object[]>());
1938+
_fixture.Client.Received(1).CaptureFeedback(Arg.Is<SentryFeedback>(f => f.ContactEmail.IsNull()),
1939+
Arg.Any<Scope>(), Arg.Any<SentryHint>());
1940+
}
1941+
1942+
[Theory]
1943+
[InlineData(null)]
1944+
[InlineData("")]
1945+
[InlineData(" ")]
1946+
[InlineData("test@example.com")]
1947+
[InlineData("user.name@domain.com")]
1948+
[InlineData("user+tag@example.com")]
1949+
public void CaptureUserFeedback_ValidEmail_FeedbackRegistered(string email)
1950+
{
1951+
#pragma warning disable CS0618 // Type or member is obsolete
1952+
// Arrange
1953+
var hub = _fixture.GetSut();
1954+
var feedback = new UserFeedback(SentryId.Create(), "Test name", email, "Test comment");
1955+
1956+
// Act
1957+
hub.CaptureUserFeedback(feedback);
1958+
1959+
// Assert
1960+
_fixture.Client.Received(1).CaptureUserFeedback(Arg.Any<UserFeedback>());
1961+
#pragma warning restore CS0618 // Type or member is obsolete
1962+
}
1963+
1964+
[Theory]
1965+
[InlineData("invalid-email")]
1966+
[InlineData("missing@domain")]
1967+
[InlineData("@missing-local.com")]
1968+
[InlineData("spaces in@email.com")]
1969+
public void CaptureUserFeedback_InvalidEmail_FeedbackDropped(string email)
1970+
{
1971+
#pragma warning disable CS0618 // Type or member is obsolete
1972+
// Arrange
1973+
_fixture.Options.Debug = true;
1974+
_fixture.Options.DiagnosticLogger = Substitute.For<IDiagnosticLogger>();
1975+
_fixture.Options.DiagnosticLogger!.IsEnabled(Arg.Any<SentryLevel>()).Returns(true);
1976+
var hub = _fixture.GetSut();
1977+
var feedback = new UserFeedback(SentryId.Create(), "Test name", email, "Test comment");
1978+
1979+
// Act
1980+
hub.CaptureUserFeedback(feedback);
1981+
1982+
// Assert
1983+
_fixture.Options.DiagnosticLogger.Received(1).Log(
1984+
SentryLevel.Warning,
1985+
Arg.Is<string>(s => s.Contains("invalid email format")),
1986+
null,
1987+
Arg.Any<object[]>());
1988+
_fixture.Client.Received(1).CaptureUserFeedback(Arg.Is<UserFeedback>(f => f.Email.IsNull()));
1989+
#pragma warning restore CS0618 // Type or member is obsolete
1990+
}
18931991
}
18941992

18951993
#if NET6_0_OR_GREATER

0 commit comments

Comments
 (0)