Skip to content

Host Header Request Injection Hardening#28

Open
retohugi wants to merge 3 commits into
fork/xmfrom
feature/extended-ssrf-and-cert-validation-fixes
Open

Host Header Request Injection Hardening#28
retohugi wants to merge 3 commits into
fork/xmfrom
feature/extended-ssrf-and-cert-validation-fixes

Conversation

@retohugi
Copy link
Copy Markdown
Collaborator

TL;DR;

This PR improves default security of the ErrorManager regarding host header injection attacks. It is based on the XM fork and thus targets the fork, but I'm happy to backport to the full version.

The changes cover .NET Framework upgrade and Nuget restore behaviour as well as clean-ups. They are opinionated, but again, I'm happy to change any of these.

Question

Will you plan to publish releases of the XM fork on Nuget.org? Asking since currently our solution depends on the public release, not the XM fork.

SSRF Risk Analysis — Host Header Injection

Overview

The Sitecore Error Manager is vulnerable to Server-Side Request Forgery (SSRF) via
HTTP Host header injection. When the server handles an error (404, 403, 500), it
constructs a URL using the incoming request's Host header and makes a server-side
HTTP request to that URL to fetch the rendered error page. An attacker who controls
the Host header can redirect this internal request to an arbitrary host.

Affected Components

Primary vulnerability — BaseError.cs:OnLoad()

WebUtil.GetServerUrl() derives scheme and host from the incoming HTTP request's
Host header. The return value is concatenated with a relative path and passed to
HttpWebRequest.Create(), which issues a real HTTP request from the server.

Affected lines (static URL paths):

  • BaseError.cs:117WebUtil.GetServerUrl() + Settings.GetSetting(this.SettingsKey + ".Static")
  • BaseError.cs:150 — same pattern in language fallback branch
  • BaseError.cs:172 — same pattern in layout-not-found fallback

Affected lines (dynamic URL via Sitecore LinkManager):

  • BaseError.cs:129options.AlwaysIncludeServerUrl = true causes LinkManager.GetItemUrl() to embed the host-header-derived server URL
  • BaseError.cs:138, 146LinkManager.GetItemUrl(item, options) returns full URL

Secondary attack surfaces

Component File:Line Risk
Header forwarding BaseError.cs:294-306 HandleHeaderForwarding reads headers from the original request and copies them to the outbound HttpWebRequest. If the request targets an attacker-controlled host, these headers are leaked.
Cookie forwarding BaseError.cs:315-347 When ErrorManager.SendClientCookies is enabled, user cookies are forwarded to the constructed URL. An attacker-controlled host receives them.
Basic auth credentials BaseError.cs:308-313 When ErrorManager.BasicAuthentication.Enabled is true, credentials from configuration are sent to the constructed URL target.
Site context resolution UrlUtil.cs:106 WebUtil.GetHostName() resolves Sitecore site context from the Host header, potentially selecting the wrong site.
SSL validation bypass BaseError.cs:214-218, 357-360 ServicePointManager.ServerCertificateValidationCallback is a process-global static. Concurrent requests create a race condition where one thread's bypass leaks to another thread's request.

Attack Scenarios

1. SSRF — Internal network scanning

An attacker sends Host: 10.0.0.5:8080. The server issues an HTTP request to
http://10.0.0.5:8080/sitecore modules/Web/Error Manager/service/notfound.aspx,
probing internal services not exposed to the internet.

2. Reflected XSS via attacker-controlled response

The server fetches HTML from the attacker's host and writes it verbatim to the
response via this.Response.Write(body) (line 256). The attacker serves malicious
HTML/JavaScript, which is rendered in the victim's browser under the legitimate
domain's origin.

3. Credential / cookie theft

If ErrorManager.SendClientCookies or ErrorManager.BasicAuthentication.Enabled
is true, the outbound request carries sensitive credentials to the
attacker-controlled host.

4. Site context manipulation

Via UrlUtil.ResolveSite(), a spoofed Host header can cause Sitecore to resolve a
different site context, potentially serving content from a different site
definition or database.

Existing Mitigation (pre-fix)

An optional hostname allowlist existed at BaseError.cs:92-102, gated by the
ErrorManager.HostnameAllowList setting. However, it had several weaknesses:

  1. Opt-in — the setting was not present in the shipped configuration, so the
    check was skipped entirely by default (fail-open).
  2. Case-sensitive — used String.Equals() without StringComparison, so
    Example.Com would not match example.com.
  3. No port normalizationRequest.Headers["host"] can include a port
    (e.g., example.com:443) which would fail to match a bare example.com
    entry.
  4. Header-only check — only validated the raw Host header, not the final
    constructed URL. LinkManager.GetItemUrl() could still produce a URL with an
    unexpected host.

Applied Fixes

Fix 1 — Secure-by-default hostname validation

Hostname validation is enabled out of the box via Sitecore site hostname matching
(ErrorManager.ValidateSitecoreSiteHostnames, default: true). This ensures
deployments are secure by default without requiring manual configuration. An
optional explicit allowlist (ErrorManager.HostnameAllowList) can be used to
permit additional hostnames not present in any Sitecore site definition.

Fix 2 — Case-insensitive comparison with port normalization

The allowlist comparison now uses StringComparison.OrdinalIgnoreCase. The
incoming Host header's port suffix is stripped before comparison, so configuring
example.com in the allowlist matches requests with Host: example.com:443.

Fix 3 — Default configuration entry

ErrorManager.HostnameAllowList is now included in the shipped
Unic.ErrorManager.config with documentation explaining the format and
requirement.

Fix 4 — Final URL validation

After constructing the error page URL (and before issuing the HttpWebRequest),
the URL's host is validated against the allowlist. This catches cases where
LinkManager.GetItemUrl() or fallback logic produces a URL pointing to an
unexpected host.

Fix 5 — Sitecore site hostname validation (primary mechanism)

The setting ErrorManager.ValidateSitecoreSiteHostnames (default: true)
dynamically derives allowed hostnames from all Sitecore site definitions at
runtime via SiteContextFactory.Sites. Both SiteInfo.HostName (which can
contain pipe-separated values) and SiteInfo.TargetHostName are checked. This
is the primary SSRF protection mechanism and requires no manual configuration
beyond what Sitecore sites already declare. The optional explicit
HostnameAllowList is checked first and is additive — a hostname is allowed if
it matches either source.

Fix 6 — Thread-safe SSL certificate validation

Replaced the process-global ServicePointManager.ServerCertificateValidationCallback
with a per-request ServerCertificateValidationCallback on the HttpWebRequest
instance, eliminating the race condition between concurrent requests.

Configuration

<!--  VALIDATE SITECORE SITE HOSTNAMES
      If true, hostnames from Sitecore site definitions (HostName and
      TargetHostName) are treated as allowed for error page requests.
      This is the primary SSRF protection mechanism and is enabled by default.
      Default value: true
-->
<setting name="ErrorManager.ValidateSitecoreSiteHostnames" value="true" />

<!--  HOSTNAME ALLOW LIST (OPTIONAL)
      Semicolon-separated list of additional allowed hostnames. Port is not
      required; the check normalizes port suffixes automatically.
      Use this to allow hostnames not configured on any Sitecore site
      definition (e.g. load balancer or CDN hostnames).
      Example: "cdn.example.com;lb.internal.example.com"
      Default value: ""
-->
<setting name="ErrorManager.HostnameAllowList" value="" />

A hostname is allowed if it matches the optional explicit allowlist or a
Sitecore site hostname (when enabled). With the default configuration, all
hostnames declared on Sitecore site definitions are allowed automatically.

Severity

High — exploitable without authentication, enables SSRF to internal networks,
reflected XSS under the legitimate origin, and potential credential leakage.
Requires only a crafted Host header in a request to a non-existent URL.

Reto Hugi added 3 commits May 28, 2026 15:55
Replace the optional host-header allow-list check with a unified
IsHostnameAllowed validation that also accepts hostnames from configured
Sitecore site definitions (HostName/TargetHostName).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant