Правильные заголовки безопасности являются обязательными для вашего веб-сайта, управляемого Optimizely. Существует множество инструментов, которые помогут в этом, но, когда это возможно, я предпочитаю использовать собственное решение, особенно для такой низкоуровневой функции, как заголовки безопасности. Нет необходимости поддерживать пакет NuGet в актуальном состоянии. Не нужно беспокоиться о постоянных обновлениях версий. Все это содержится в базе кода, которой я управляю. Я сделал это с помощью специального промежуточного программного обеспечения, которое можно настроить в Startup.cs. Заголовки, которые можно добавить, включают:
- Параметры X-Content-Type
- Параметры X-Frame
- Сервер
- Строгая транспортная безопасность
- X-XSS-Защита
- Политика рефереров
- Политика разрешений
- Политика безопасности контента
- X-Параметры загрузки
Первое, что мне нужно сделать, это создать несколько классов констант, которые будут содержать все имена и значения заголовков, которые мне понадобятся. Каждый класс содержит строку имени заголовка и строки любых параметров. Политика безопасности контента и политика разрешений, будучи немного более сложными, также нуждаются в перечислении, чтобы охватить некоторые параметры.
Параметры X-Content-Type
///
/// X-Content-Type-Options-related constants.
///
public static class ContentTypeOptionsConstants
{
///
/// Header value for X-Content-Type-Options
///
public static readonly string Header = "X-Content-Type-Options";
///
/// Disables content sniffing
///
public static readonly string NoSniff = "nosniff";
}
В данном случае у меня есть только опции nosniff, потому что именно их я буду использовать.
Параметры X-Frame
///
/// X-Frame-Options-related constants.
///
public static class FrameOptionsConstants
{
///
/// The header value for X-Frame-Options
///
public static readonly string Header = "X-Frame-Options";
///
/// The page cannot be displayed in a frame, regardless of the site attempting to do so.
///
public static readonly string Deny = "DENY";
///
/// The page can only be displayed in a frame on the same origin as the page itself.
///
public static readonly string SameOrigin = "SAMEORIGIN";
///
/// The page can only be displayed in a frame on the specified origin. {0} specifies the format string
///
public static readonly string AllowFromUri = "ALLOW-FROM {0}";
}
Сервер
///
/// Server headery-related constants.
///
public static class ServerConstants
{
///
/// The header value for X-Powered-By
///
public static readonly string Header = "Server";
}
Строгая транспортная безопасность
///
/// Strict-Transport-Security-related constants.
///
public static class StrictTransportSecurityConstants
{
///
/// Header value for Strict-Transport-Security
///
public static readonly string Header = "Strict-Transport-Security";
///
/// Tells the user-agent to cache the domain in the STS list for the provided number of seconds {0}
///
public static readonly string MaxAge = "max-age={0}";
///
/// Tells the user-agent to cache the domain in the STS list for the provided number of seconds {0} and include any subdomains.
///
public static readonly string MaxAgeIncludeSubdomains = "max-age={0}; includeSubDomains";
///
/// Tells the user-agent to remove, or not cache the host in the STS cache.
///
public static readonly string NoCache = "max-age=0";
}
X-XSS-Защита
///
/// X-XSS-Protection-related constants.
///
public static class XssProtectionConstants
{
///
/// Header value for X-XSS-Protection
///
public static readonly string Header = "X-XSS-Protection";
///
/// Enables the XSS Protections
///
public static readonly string Enabled = "1";
///
/// Disables the XSS Protections offered by the user-agent.
///
public static readonly string Disabled = "0";
///
/// Enables XSS protections and instructs the user-agent to block the response in the event that script has been inserted from user input, instead of sanitizing.
///
public static readonly string Block = "1; mode=block";
///
/// A partially supported directive that tells the user-agent to report potential XSS attacks to a single URL. Data will be POST'd to the report URL in JSON format.
/// {0} specifies the report url, including protocol
///
public static readonly string Report = "1; report={0}";
}
Политика рефереров
///
/// Referrer Policy related constants
///
public static class ReferrerPolicyConstants
{
///
/// Header value for Referrer-Policy
///
public static readonly string Header = "Referrer-Policy";
public static readonly string NoReferrer = "no-referrer";
public static readonly string NoReferrerWhenDowngrade = "no-referrer-when-downgrade";
public static readonly string SameOrigin = "same-origin";
public static readonly string Origin = "origin";
public static readonly string StrictOrigin = "strict-origin";
public static readonly string OriginWhenCrossOrigin = "origin-when-cross-origin";
public static readonly string StrictOriginWhenCrossOrigin = "strict-origin-when-cross-origin";
public static readonly string UnsafeUrl = "unsafe-url";
}
Политика разрешений
///
/// Permissions Policy related constants
///
public static class PermissionsPolicyConstants
{
///
/// Header value for Permissions-Policy
///
public static readonly string Header = "Permissions-Policy";
}
При настройке политики разрешений вы можете установить разрешения для различных функций. Различные функции, которые я поместил в перечисление:
///
/// Enum of some permission policies
///
public enum PermissionPolicy
{
[EnumSelectionDescription(Text = "Accelerometer", Value = "accelerometer")]
Accelerometer = 1,
[EnumSelectionDescription(Text = "Camera", Value = "camera")]
Camera = 2,
[EnumSelectionDescription(Text = "Geolocation", Value = "")]
Geolocation = 3,
[EnumSelectionDescription(Text = "Gyroscope", Value = "gyroscope")]
Gyroscope = 4,
[EnumSelectionDescription(Text = "Magnetometer", Value = "magnetometer")]
Magnetometer = 5,
[EnumSelectionDescription(Text = "Microphone", Value = "microphone")]
Microphone = 6,
[EnumSelectionDescription(Text = "Payment", Value = "payment")]
Payment = 7,
[EnumSelectionDescription(Text = "Usb", Value = "usb")]
Usb = 8
}
Политика безопасности контента
///
/// Permissions Policy related constants
///
public static class ContentSecurityPolicyConstants
{
///
/// Header value for Permissions-Policy
///
public static readonly string Header = "Content-Security-Policy";
}
Политику безопасности контента необходимо настроить для различных типов контента. Я также управляю ими с помощью перечисления:
public enum ContentSecurityPolicy
{
[EnumSelectionDescription(Text = "DefaultSource", Value = "default-src 'self'")]
DefaultSource = 1,
[EnumSelectionDescription(Text = "ConnectSource", Value = " connect-src * 'self' data: https:")]
ConnectSource = 2,
[EnumSelectionDescription(Text = "FontSource", Value = " font-src 'self' data: https:")]
FontSource = 3,
[EnumSelectionDescription(Text = "FrameSource", Value = " frame-src 'self' data: https:")]
FrameSource = 4,
[EnumSelectionDescription(Text = "ImageSource", Value = " img-src * 'self' data: https: blob:")]
ImageSource = 5,
[EnumSelectionDescription(Text = "ScriptSource", Value = " script-src 'self' 'nonce-{nonceValue}' 'strict-dynamic' ")]
ScriptSource = 6,
[EnumSelectionDescription(Text = "StyleSource", Value = " style-src 'self' 'unsafe-inline' *")]
StyleSource = 7,
[EnumSelectionDescription(Text = "FormAction", Value = " form-action 'self' data: https:")]
FormAction = 8,
[EnumSelectionDescription(Text = "MediaSource", Value = " media-src 'self' data: https: blob:")]
MediaSource = 9
}
X-Параметры загрузки
///
/// Permissions Policy related constants
///
public static class XDownloadOptionsConstants
{
///
/// Header value for Permissions-Policy
///
public static readonly string Header = "X-Download-Options";
public static readonly string NoOpen = "noopen";
}
Настоящая работа, которую я делаю, — это класс-строитель SecurityHeadersBuilder. Этот конструктор имеет множество методов для установки различных значений заголовков. Затем я создаю метод, который создает политику безопасности. Концептуально у вас может быть множество политик. Мне нужен только один, поэтому у меня есть метод по умолчанию, который создает все мои заголовки. Он сохраняет их в классе политики.
Сначала класс политики. Это довольно просто, у него просто есть метод добавления и удаления для установки заголовков.
///
/// The security headers policy
///
public class SecurityHeadersPolicy
{
///
/// Headers to add
///
public IDictionary SetHeaders { get; }
= new Dictionary();
///
/// Headers to remove
///
public ISet RemoveHeaders { get; }
= new HashSet();
}
Он сохраняет эти значения в словаре.
Затем конструктор добавляет заголовки в эту политику:
///
/// Middle ware to create the desired security headers
///
public class SecurityHeadersBuilder
{
private readonly SecurityHeadersPolicy _policy = new();
///
/// The number of seconds in one year
///
public const int OneYearInSeconds = 60 * 60 * 24 * 365;
private CompositeFormat FrameOptionsAllowFromUri { get; set; } = CompositeFormat.Parse(FrameOptionsConstants.AllowFromUri);
private CompositeFormat XssProtectionConstantsReport { get; set; } = CompositeFormat.Parse(XssProtectionConstants.Report);
private CompositeFormat StrictTransportSecurityConstantsMaxAge { get; set; } = CompositeFormat.Parse(StrictTransportSecurityConstants.MaxAge);
private CompositeFormat StrictTransportSecurityConstantsMaxAgeIncludeSubdomains { get; set; } = CompositeFormat.Parse(StrictTransportSecurityConstants.MaxAgeIncludeSubdomains);
///
/// Add default headers in accordance with most secure approach
///
public SecurityHeadersBuilder AddDefaultSecurePolicy()
{
this.AddXssProtectionBlock();
this.AddStrictTransportSecurityMaxAge();
this.AddPermissionsPolicy();
this.AddXssProtectionBlock();
this.AddContentTypeOptionsNoSniff();
this.AddReferrerPolicyStrictOriginWhenCrossOrigin();
this.AddXDownloadOptionsNoOpen();
return this;
}
///
/// Add X-Frame-Options DENY to all requests.
/// The page cannot be displayed in a frame, regardless of the site attempting to do so
///
public SecurityHeadersBuilder AddFrameOptionsDeny()
{
this._policy.SetHeaders[FrameOptionsConstants.Header] = FrameOptionsConstants.Deny;
return this;
}
///
/// Add X-Frame-Options SAMEORIGIN to all requests.
/// The page can only be displayed in a frame on the same origin as the page itself.
///
public SecurityHeadersBuilder AddFrameOptionsSameOrigin()
{
this._policy.SetHeaders[FrameOptionsConstants.Header] = FrameOptionsConstants.SameOrigin;
return this;
}
///
/// Add X-Frame-Options ALLOW-FROM {uri} to all requests, where the uri is provided
/// The page can only be displayed in a frame on the specified origin.
///
/// The uri of the origin in which the page may be displayed in a frame
public SecurityHeadersBuilder AddFrameOptionsSameOrigin(string uri)
{
this._policy.SetHeaders[FrameOptionsConstants.Header] = string.Format(CultureInfo.InvariantCulture, this.FrameOptionsAllowFromUri, uri);
return this;
}
///
/// Add X-XSS-Protection 1 to all requests.
/// Enables the XSS Protections
///
public SecurityHeadersBuilder AddXssProtectionEnabled()
{
this._policy.SetHeaders[XssProtectionConstants.Header] = XssProtectionConstants.Enabled;
return this;
}
///
/// Add X-XSS-Protection 0 to all requests.
/// Disables the XSS Protections offered by the user-agent.
///
public SecurityHeadersBuilder AddXssProtectionDisabled()
{
this._policy.SetHeaders[XssProtectionConstants.Header] = XssProtectionConstants.Disabled;
return this;
}
///
/// Add X-XSS-Protection 1; mode=block to all requests.
/// Enables XSS protections and instructs the user-agent to block the response in the event that script has been inserted from user input, instead of sanitizing.
///
public SecurityHeadersBuilder AddXssProtectionBlock()
{
this._policy.SetHeaders[XssProtectionConstants.Header] = XssProtectionConstants.Block;
return this;
}
///
/// Add X-XSS-Protection 1; report=http://site.com/report to all requests.
/// A partially supported directive that tells the user-agent to report potential XSS attacks to a single URL. Data will be POST'd to the report URL in JSON format.
///
public SecurityHeadersBuilder AddXssProtectionReport(string reportUrl)
{
this._policy.SetHeaders[XssProtectionConstants.Header] =
string.Format(CultureInfo.InvariantCulture, this.XssProtectionConstantsReport, reportUrl);
return this;
}
///
/// Add Strict-Transport-Security max-age= to all requests.
/// Tells the user-agent to cache the domain in the STS list for the number of seconds provided.
///
public SecurityHeadersBuilder AddStrictTransportSecurityMaxAge(int maxAge = OneYearInSeconds)
{
this._policy.SetHeaders[StrictTransportSecurityConstants.Header] =
string.Format(CultureInfo.InvariantCulture, this.StrictTransportSecurityConstantsMaxAge, maxAge);
return this;
}
///
/// Add Strict-Transport-Security max-age= ; includeSubDomains to all requests.
/// Tells the user-agent to cache the domain in the STS list for the number of seconds provided and include any subdomains.
///
public SecurityHeadersBuilder AddStrictTransportSecurityMaxAgeIncludeSubDomains(int maxAge = OneYearInSeconds)
{
this._policy.SetHeaders[StrictTransportSecurityConstants.Header] =
string.Format(CultureInfo.InvariantCulture, this.StrictTransportSecurityConstantsMaxAgeIncludeSubdomains, maxAge);
return this;
}
///
/// Add Strict-Transport-Security max-age=0 to all requests.
/// Tells the user-agent to remove, or not cache the host in the STS cache
///
public SecurityHeadersBuilder AddStrictTransportSecurityNoCache()
{
this._policy.SetHeaders[StrictTransportSecurityConstants.Header] =
StrictTransportSecurityConstants.NoCache;
return this;
}
///
/// Add X-Content-Type-Options nosniff to all requests.
/// Can be set to protect against MIME type confusion attacks.
///
public SecurityHeadersBuilder AddContentTypeOptionsNoSniff()
{
this._policy.SetHeaders[ContentTypeOptionsConstants.Header] = ContentTypeOptionsConstants.NoSniff;
return this;
}
///
/// Removes the Server header from all responses
///
public SecurityHeadersBuilder RemoveServerHeader()
{
this._policy.RemoveHeaders.Add(ServerConstants.Header);
return this;
}
///
/// Add the XDownload option headers
///
///
public SecurityHeadersBuilder AddXDownloadOptionsNoOpen()
{
this._policy.SetHeaders[XDownloadOptionsConstants.Header] = XDownloadOptionsConstants.NoOpen;
return this;
}
///
/// Adds a custom header to all requests
///
/// The header name
/// The value for the header
///
public SecurityHeadersBuilder AddCustomHeader(string header, string value)
{
if (string.IsNullOrEmpty(header))
{
throw new ArgumentNullException(nameof(header));
}
this._policy.SetHeaders[header] = value;
return this;
}
///
/// Remove a header from all requests
///
/// The to remove
///
public SecurityHeadersBuilder RemoveHeader(string header)
{
if (string.IsNullOrEmpty(header))
{
throw new ArgumentNullException(nameof(header));
}
this._policy.RemoveHeaders.Add(header);
return this;
}
///
/// Add referrer policy header of NoReferrer
///
///
public SecurityHeadersBuilder AddReferrerPolicyNoReferrer()
{
this._policy.SetHeaders[ReferrerPolicyConstants.Header] = ReferrerPolicyConstants.NoReferrer;
return this;
}
///
/// Add referrer policy header of NoReferrer
///
///
public SecurityHeadersBuilder AddReferrerPolicyStrictOriginWhenCrossOrigin()
{
this._policy.SetHeaders[ReferrerPolicyConstants.Header] = ReferrerPolicyConstants.StrictOriginWhenCrossOrigin;
return this;
}
///
/// Add the permissions policy
///
///
public SecurityHeadersBuilder AddPermissionsPolicy()
{
// Get all permissions policies
var permissionPolicies = Enum.GetValues();
// Loop through the policies
var policy = (from permissionPolicy in permissionPolicies
select permissionPolicy.Value() into value
where !string.IsNullOrEmpty(value)
select $"{value}=()").ToList();
this._policy.SetHeaders[PermissionsPolicyConstants.Header] = string.Join(',', policy.ToArray());
return this;
}
///
/// Builds a new using the entries added.
///
/// The constructed .
public SecurityHeadersPolicy Build() => this._policy;
}
Разбираем, что здесь происходит:
Сначала я создаю политику:
private readonly SecurityHeadersPolicy _policy = new();
Затем я создаю константу для хранения 1 года в секундах (для заголовка Stricct Transport Security я устанавливаю максимальный возраст, который должен составлять год).
///
/// The number of seconds in one year
///
public const int OneYearInSeconds = 60 * 60 * 24 * 365;
Далее у меня есть несколько значений заголовков, которые требуют замены строк. Для каждого из них я создаю объект CompositeFormat, анализируя значение заголовка. Создание CompositeFormat кэширует анализ этой строки, добавляя небольшой прирост производительности при последующих вызовах. Замена произойдет позже:
private CompositeFormat FrameOptionsAllowFromUri { get; set; } = CompositeFormat.Parse(FrameOptionsConstants.AllowFromUri);
private CompositeFormat XssProtectionConstantsReport { get; set; } = CompositeFormat.Parse(XssProtectionConstants.Report);
private CompositeFormat StrictTransportSecurityConstantsMaxAge { get; set; } = CompositeFormat.Parse(StrictTransportSecurityConstants.MaxAge);
private CompositeFormat StrictTransportSecurityConstantsMaxAgeIncludeSubdomains { get; set; } = CompositeFormat.Parse(StrictTransportSecurityConstants.MaxAgeIncludeSubdomains);
В этом случае, если вы посмотрите выше, вы увидите, что FrameOptionsConstants.AllowFromUri равно «ALLOW-FROM {0}». Когда я устанавливаю этот заголовок, я заменю «{0}» URL-адресом.
Затем я создаю метод для установки каждого значения заголовка. Например, для FrameOptions у меня есть три метода установки разных значений:
///
/// Add X-Frame-Options DENY to all requests.
/// The page cannot be displayed in a frame, regardless of the site attempting to do so
///
public SecurityHeadersBuilder AddFrameOptionsDeny()
{
this._policy.SetHeaders[FrameOptionsConstants.Header] = FrameOptionsConstants.Deny;
return this;
}
///
/// Add X-Frame-Options SAMEORIGIN to all requests.
/// The page can only be displayed in a frame on the same origin as the page itself.
///
public SecurityHeadersBuilder AddFrameOptionsSameOrigin()
{
this._policy.SetHeaders[FrameOptionsConstants.Header] = FrameOptionsConstants.SameOrigin;
return this;
}
///
/// Add X-Frame-Options ALLOW-FROM {uri} to all requests, where the uri is provided
/// The page can only be displayed in a frame on the specified origin.
///
/// The uri of the origin in which the page may be displayed in a frame
public SecurityHeadersBuilder AddFrameOptionsSameOrigin(string uri)
{
this._policy.SetHeaders[FrameOptionsConstants.Header] = string.Format(CultureInfo.InvariantCulture, this.FrameOptionsAllowFromUri, uri);
return this;
}
Каждый метод устанавливает для заголовка желаемое значение. Используя созданный мной объект политики, он добавляет заголовок, используя константу имени заголовка из наших классов констант, а затем константу нужного значения.
Удаление заголовка сервера является обычным явлением, поэтому у меня есть для этого специальный метод:
///
/// Removes the Server header from all responses
///
public SecurityHeadersBuilder RemoveServerHeader()
{
this._policy.RemoveHeaders.Add(ServerConstants.Header);
return this;
}
Что касается политики разрешений, я хочу разрешить все возможные разрешения для функций, поэтому я просто перебираю перечисление и добавляю все значения.
///
/// Add the permissions policy
///
///
public SecurityHeadersBuilder AddPermissionsPolicy()
{
// Get all permissions policies
var permissionPolicies = Enum.GetValues();
// Loop through the policies
var policy = (from permissionPolicy in permissionPolicies
select permissionPolicy.Value() into value
where !string.IsNullOrEmpty(value)
select $"{value}=()").ToList();
this._policy.SetHeaders[PermissionsPolicyConstants.Header] = string.Join(',', policy.ToArray());
return this;
}
Для ваших нужд вам может потребоваться создать другую версию этого метода, которая устанавливает только необходимые вам разрешения.
У меня есть специальный метод, который создает нашу политику по умолчанию:
///
/// Add default headers in accordance with most secure approach
///
public SecurityHeadersBuilder AddDefaultSecurePolicy()
{
this.AddXssProtectionBlock();
this.AddStrictTransportSecurityMaxAge();
this.AddPermissionsPolicy();
this.AddContentTypeOptionsNoSniff();
this.AddReferrerPolicyStrictOriginWhenCrossOrigin();
this.AddXDownloadOptionsNoOpen();
return this;
}
Здесь я вызываю только те методы, которые мне нужны для реализации моей политики. Обратите внимание, что здесь отсутствует Политика безопасности контента. Я вернусь к этому.
Наконец, у меня есть метод сборки, который возвращает политику:
///
/// Builds a new using the entries added.
///
/// The constructed .
public SecurityHeadersPolicy Build() => this._policy;
Промежуточное ПО
Следующая часть, которая мне нужна, — это промежуточное программное обеспечение для создания и использования этих заголовков. Промежуточное программное обеспечение требует метода вызова, который выполняет всю работу. То есть он берет политики, созданные в сборщике, и добавляет их как фактические заголовки. Кроме того, здесь я добавляю Политику безопасности контента.
using System.Globalization;
using EPiServer.Framework.ClientResources;
using Hero.OptimizelyCMS.Foundation.Extensions;
using System.Web;
using Hero.OptimizelyCMS.Foundation.Utilities;
namespace Hero.OptimizelyCMS.Foundation.SecurityPolicy.SecurityMiddleware;
///
/// Middleware for setting security headers
///
public class SecurityHeadersMiddleware
{
private readonly RequestDelegate _next;
private readonly SecurityHeadersPolicy _policy;
private readonly ICspNonceService _cspNonceService;
public SecurityHeadersMiddleware(RequestDelegate next, SecurityHeadersPolicy policy, ICspNonceService cspNonceService)
{
this._next = next;
this._policy = policy;
this._cspNonceService = cspNonceService;
}
///
/// Add and remove desired headers
///
/// The current HttpContext
///
public async Task Invoke(HttpContext context)
{
// Get existing headers
var headersDictionary = context.Response.Headers;
// If we should add Csp
if (ShouldAddCsp(context))
{
// Add CSP
headersDictionary[ContentSecurityPolicyConstants.Header] = this.GetContentSecurityPolicy();
// Frame options
headersDictionary[FrameOptionsConstants.Header] = FrameOptionsConstants.SameOrigin;
}
// Loop through headers to add
foreach (var headerValuePair in this._policy.SetHeaders)
{
// Add each header
headersDictionary[headerValuePair.Key] = headerValuePair.Value;
}
// Loop through headers to remove
foreach (var header in this._policy.RemoveHeaders)
{
// Remove the header
headersDictionary.Remove(header);
}
await this._next(context);
}
///
/// Determine if we should add Csp
///
///
///
private static bool ShouldAddCsp(HttpContext context)
{
// Get the current url
var currentUrl = context.Request.Url();
var path = currentUrl.PathAndQuery;
var segments = path.Split("http://world.optimizely.com/", StringSplitOptions.RemoveEmptyEntries);
// Get the first segment
var segmentZero = segments.ElementAtOrDefault(0);
// If there is no segment zero, we are on the home page
if (string.IsNullOrEmpty(segmentZero))
{
return true;
}
// If the segment is one of these, we should not add CSP
return segmentZero.ToLower(CultureInfo.InvariantCulture) switch
{
"error" => false,
"episerver" => false,
"util" => false,
"cleaner" => false,
"redirectmanager" => false,
_ => true,
};
}
///
/// Create the Csp
///
///
public string GetContentSecurityPolicy()
{
var policy = new List();
// Get Csp Enum values
var contentSecurityPolicies = Enum.GetValues();
// Loop through each policy
foreach (var contentSecurityPolicy in contentSecurityPolicies)
{
// Get the policy value
var value = contentSecurityPolicy.Value();
// Add the nonce to the policy
if (string.IsNullOrEmpty(value))
{
continue;
}
var nonce = this._cspNonceService.GetNonce();
nonce = HttpUtility.JavaScriptStringEncode(nonce);
value = value.Replace("{nonceValue}", nonce);
policy.Add(value);
}
// Return the policy
return string.Join(';', policy.ToArray());
}
}
И давайте разберем, что здесь происходит:
Я внедряю RequestDelegate. Вот как промежуточное программное обеспечение перейдет к следующему фрагменту промежуточного программного обеспечения, как только оно будет выполнено здесь. Я добавляю нашу политику заголовка и экземпляр ICspNonceService для добавления политики безопасности контента nonce (подробнее об этом чуть позже).
При вызове Invoke я получаю текущий словарь заголовков, определяю, следует ли добавить политику безопасности контента, а затем просматриваю все соответствующие заголовки в нашей политике, добавляю их в ответ и удаляю все заголовки, которые следует удалить. .
Я обнаружил, что добавление Политики безопасности контента внутри CMS нарушило многие функции CMS. Поэтому я смотрю путь и не добавляю политику безопасности, если нахожусь в CMS. Сюда входят обычные пути cms (episerver, util), а также некоторые пользовательские URL-адреса для пользовательских инструментов администрирования. Возможно, вам придется указать другие пути в зависимости от установленных вами пользовательских инструментов или сторонних надстроек.
///
/// Determine if we should add Csp
///
///
///
private static bool ShouldAddCsp(HttpContext context)
{
// Get the current url
var currentUrl = context.Request.Url();
var path = currentUrl.PathAndQuery;
var segments = path.Split("http://world.optimizely.com/", StringSplitOptions.RemoveEmptyEntries);
// Get the first segment
var segmentZero = segments.ElementAtOrDefault(0);
// If there is no segment zero, we are on the home page
if (string.IsNullOrEmpty(segmentZero))
{
return true;
}
// If the segment is one of these, we should not add CSP
return segmentZero.ToLower(CultureInfo.InvariantCulture) switch
{
"error" => false,
"episerver" => false,
"util" => false,
"cleaner" => false,
"redirectmanager" => false,
_ => true,
};
}
Если мне действительно нужно создать политику, я просматриваю настроенный мной Enum и добавляю эти значения.
///
/// Create the Csp
///
///
public string GetContentSecurityPolicy()
{
var policy = new List();
// Get Csp Enum values
var contentSecurityPolicies = Enum.GetValues();
// Loop through each policy
foreach (var contentSecurityPolicy in contentSecurityPolicies)
{
// Get the policy value
var value = contentSecurityPolicy.Value();
// Add the nonce to the policy
if (string.IsNullOrEmpty(value))
{
continue;
}
var nonce = this._cspNonceService.GetNonce();
nonce = HttpUtility.JavaScriptStringEncode(nonce);
value = value.Replace("{nonceValue}", nonce);
policy.Add(value);
}
// Return the policy
return string.Join(';', policy.ToArray());
}
Здесь следует отметить одну маленькую вещь. Я хочу использовать nonce со сценариями. Nonce — это хэш, уникальный для каждого запроса. Если тег сценария не имеет nonce, браузер сочтет его подозрительным и не запустит сценарий. Тег сценария с одноразовым номером может выглядеть так:
Я создал собственную реализацию этой службы и использую ее для генерации nonce и строки, заменяющей ее в атрибуте ScriptSource политики безопасности контента.
///
/// Concrete implementation of nonce service for generating the nonce
///
public class CspNonceService : ICspNonceService
{
private readonly IHttpContextAccessor _httpContextAccessor;
public CspNonceService(IHttpContextAccessor httpContextAccessor) => this._httpContextAccessor = httpContextAccessor;
// Cache key
public const string Key = "csp-nonce";
///
/// Get the current nonce
///
///
public string GetNonce()
{
// Get the cache
var items = this._httpContextAccessor.HttpContext?.Items;
// If we have no items and the key is not in the cache
if (items != null && !items.ContainsKey(Key))
{
// Generate the nonce and add it to cache
items.Add(Key, GenerateNonce());
}
// Get the nonce from cache
var nonce = items != null ? items[Key] : GenerateNonce();
// Return the nonce
return nonce?.ToString();
}
///
/// Generate a new nonce
///
///
private static string GenerateNonce()
{
// Generate a new nonce
var numArray = new byte[32];
using (var randomNumberGenerator = RandomNumberGenerator.Create())
{
randomNumberGenerator.GetBytes(numArray);
}
// Convert it to base 64 string
return Convert.ToBase64String(numArray);
}
}
Запускать
Последний шаг — зарегистрировать все необходимые мне ресурсы и добавить промежуточное ПО в конвейер.
В Startup.cs я регистрирую службу nonce в методе ConfigurationServices:
// Add nonce service
services.AddScoped(sp => new CspNonceService(sp.GetRequiredService()));
Optimizely может добавить это в свои собственные блоки скриптов после регистрации (https://docs.developers.optimizely.com/content-management-system/docs/content-security-policy).
ПРИМЕЧАНИЕ. Использование nonce — это предложение «все или ничего». Если вы это реализуете, всем тегам скриптов, добавленным на страницы, потребуется nonce. Для этого вам потребуется установить его вручную с помощью реализации ICspNonceService.
Затем я создаю метод расширения, чтобы упростить настройку этой политики.
///
/// Method for implementing security header middleware
///
public static class MiddlewareExtensions
{
public static IApplicationBuilder UseSecurityHeadersMiddleware(this IApplicationBuilder app, SecurityHeadersBuilder builder)
{
var policy = builder.Build();
return app.UseMiddleware(policy);
}
}
Наконец, я добавляю промежуточное программное обеспечение в конвейер, также в Startup.cs в методе настройки. Я добавляю его на ранней стадии конвейера, чтобы все запросы получали его:
// Add security headers
app.UseSecurityHeadersMiddleware(new SecurityHeadersBuilder()
.AddDefaultSecurePolicy()
.RemoveHeader("X-Powered-By")
);
Это можно легко настроить в соответствии с вашими конкретными потребностями.
ПРИМЕЧАНИЕ. Я работаю локально, и вы можете видеть, что заголовок сервера все еще существует. Заголовок сервера для Kestrel необходимо удалить другим способом, когда вы настраиваете Kestrel в своем коде.
Теперь, когда я захожу на свой сайт, я вижу все свои заголовки:
И я вижу, что если я вхожу в CMS, политика безопасности контента не добавляется:
Наконец, если я просканирую сайт, используя эту конфигурацию, я увижу, что мои заголовки настроены правильно:
Потенциальное будущее усовершенствование: с помощью политики безопасности контента я создал довольно разрешительную политику. Фактически вы можете включить конкретные URL-адреса для различных частей политики. Вероятно, лучше всего сохранить это в настройках приложения, а затем извлечь и добавить в политику.
21 февраля 2024 г.