Skip to content

Commit d1c42b6

Browse files
fix: Replace hCaptcha hostname security check with informational telemetry (#1117)
## Why The `ExpectedHostname` config in `CaptchaOptions` was being used to validate the `hostname` field returned by hCaptcha's siteverify API -- failing the captcha if it didn't match. However, [hCaptcha's own docs](https://docs.hcaptcha.com/#verify-the-user-response-server-side) explicitly state: > "the hostname field is derived from the user's browser, and should not be used for authentication of any kind; it is primarily useful as a statistical metric. Additionally... the hostname field may be returned as 'not-provided' rather than the usual value" This means the check provided false security (browser-controlled, trivially spoofable) and could silently reject legitimate users under high traffic when hCaptcha returns `"not-provided"`. ## What changed - **`CaptchaOptions.cs`**: Removed `ExpectedHostname` property. The `HCaptcha__ExpectedHostname` environment variable can now be removed from all deployed environments. - **`CaptchaService.cs`**: Replaced the hostname mismatch block (which set `result.Success = false`) with an informational log that fires on every successful verification. The expected hostname is now derived from `SiteSettings.BaseUrl` rather than a separate config value. ## Telemetry Every successful captcha verification now logs: ``` hCaptcha hostname: reported={ReportedHostname}, expected={ExpectedHostname} ``` This enables App Insights / Log Analytics queries to bucket hostname distributions, for example: ```kql traces | where message startswith "hCaptcha hostname" | extend reported = tostring(customDimensions.ReportedHostname), expected = tostring(customDimensions.ExpectedHostname) | summarize count() by reported, expected ```
1 parent 670b0f5 commit d1c42b6

2 files changed

Lines changed: 9 additions & 15 deletions

File tree

EssentialCSharp.Web/Services/CaptchaOptions.cs

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,4 @@ public class CaptchaOptions
2626
/// </summary>
2727
public string JavaScriptUrl { get; set; } = "https://js.hcaptcha.com/1/api.js";
2828

29-
/// <summary>
30-
/// If set, the hostname returned in the hCaptcha siteverify response is compared against this value.
31-
/// Set to null (default) to skip hostname validation — required when using test keys or in development.
32-
/// In production, set to the canonical hostname (e.g., "essentialcsharp.com").
33-
/// </summary>
34-
public string? ExpectedHostname { get; set; }
3529
}

EssentialCSharp.Web/Services/CaptchaService.cs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@
44

55
namespace EssentialCSharp.Web.Services;
66

7-
public partial class CaptchaService(IHttpClientFactory clientFactory, IOptions<CaptchaOptions> optionsAccessor, ILogger<CaptchaService> logger) : ICaptchaService
7+
public partial class CaptchaService(IHttpClientFactory clientFactory, IOptions<CaptchaOptions> optionsAccessor, IOptions<SiteSettings> siteSettingsAccessor, ILogger<CaptchaService> logger) : ICaptchaService
88
{
99
private IHttpClientFactory ClientFactory { get; } = clientFactory;
1010
private CaptchaOptions Options { get; } = optionsAccessor.Value;
11+
private SiteSettings SiteSettings { get; } = siteSettingsAccessor.Value;
1112

1213
// Explicit overload used by integration tests: https://docs.hcaptcha.com/#verify-the-user-response-server-side
1314
public async Task<HCaptchaResult?> VerifyAsync(string secret, string response, string sitekey, CancellationToken cancellationToken = default)
@@ -49,13 +50,12 @@ public partial class CaptchaService(IHttpClientFactory clientFactory, IOptions<C
4950

5051
HCaptchaResult? result = await PostVerification(postData, cancellationToken);
5152

52-
if (result is { Success: true } && Options.ExpectedHostname is { Length: > 0 } expectedHostname)
53+
if (result is { Success: true })
5354
{
54-
if (!string.Equals(result.Hostname, expectedHostname, StringComparison.OrdinalIgnoreCase))
55-
{
56-
LogHostnameMismatch(logger, expectedHostname, result.Hostname);
57-
result.Success = false;
58-
}
55+
string expectedHostname = Uri.TryCreate(SiteSettings.BaseUrl, UriKind.Absolute, out Uri? baseUri)
56+
? baseUri.Host
57+
: SiteSettings.BaseUrl;
58+
LogHostnameVerified(logger, result.Hostname, expectedHostname);
5959
}
6060

6161
return result;
@@ -81,8 +81,8 @@ public partial class CaptchaService(IHttpClientFactory clientFactory, IOptions<C
8181
}
8282
}
8383

84-
[LoggerMessage(Level = LogLevel.Warning, Message = "hCaptcha hostname mismatch: expected {Expected}, got {Actual}")]
85-
private static partial void LogHostnameMismatch(ILogger<CaptchaService> logger, string expected, string? actual);
84+
[LoggerMessage(Level = LogLevel.Information, Message = "hCaptcha hostname: reported={ReportedHostname}, expected={ExpectedHostname}")]
85+
private static partial void LogHostnameVerified(ILogger<CaptchaService> logger, string reportedHostname, string expectedHostname);
8686

8787
[LoggerMessage(Level = LogLevel.Error, Message = "hCaptcha siteverify request failed")]
8888
private static partial void LogSiteverifyFailed(ILogger<CaptchaService> logger, Exception exception);

0 commit comments

Comments
 (0)