Перетяжка, Дом карьеры
Перетяжка, Дом карьеры
Перетяжка, Дом карьеры

Как не сломать прод: настройка CORS и заголовков безопасности в ASP.NET

Как избежать критических уязвимостей и не уронить продакшен? Разбираемся с настройкой CORS и заголовков безопасности в ASP.NET: защищаем API от нежелательных запросов, предотвращаем утечки данных и настраиваем безопасные заголовки без боли и хаоса.

263 открытий708 показов

Когда речь идет о безопасности веб-приложений, настройка CORS (Cross-Origin Resource Sharing) и заголовков безопасности — один из первых шагов к защите данных и предотвращению атак. Однако ошибки в конфигурации могут привести к неприятным последствиям: от блокировки легитимных запросов до уязвимостей. Рассказываем, как грамотно настроить CORS и заголовки безопасности в ASP.NET, чтобы избежать проблем и сохранить баланс между защитой и функциональностью.

Что такое CORS?

Браузеры строго следят за безопасностью и запрещают кросс-доменные запросы, если они не разрешены сервером. Это называется политикой одного источника (Same-Origin Policy). Она защищает пользователей от кражи данных, но иногда мешает законным сценариям.

Например, ваше веб-приложение загружено с siteA.com, но данные хранятся на api.siteB.com. По умолчанию браузер блокирует такие запросы. Как же обойти это ограничение? Использовать CORS (Cross-Origin Resource Sharing).

CORS — механизм, который позволяет серверу явно указывать, какие домены, схемы и порты могут запрашивать у него данные. Работает через специальные HTTP-заголовки.

Если браузер видит нестандартный запрос (например, PUT или DELETE), он сначала отправляет предварительный (preflight) запрос. Это проверка: серверу сообщают о запрашиваемом методе и заголовках, а тот решает — разрешить или запретить. Если сервер даёт добро, основной запрос выполняется.

CORS — не обходной путь, а стандарт, утверждённый W3C. Без него современные веб-приложения не могли бы безопасно взаимодействовать друг с другом.

Какие есть источники: same-origin – different-origin

Два адреса (URL) имеют одинаковый источник (same-origin), если они оба принадлежат одному домену.

Здесь у адресов один источник:

  • https://test.com/index.html
  • https://test.com/about.html

А здесь — разные:

  • https://hello.net
  • https://www.hello.com/foo.html

Так, если приложение обратится с адреса https://hello.net к странице https://www.hello.com/foo.html без настройки политики, то CORS запрос завершится ошибкой. Чтобы запрос обработался, мы должны сказать браузеру, что обращение к источнику https://www.hello.com разрешено.

Пример кросс-доменного запроса: JavaScript фронтенд-код, загруженный с URL https://domain-a.com, использует метод fetch() для запроса JSON-файла с URL https://domain-b.com/data.json.

CORS поддерживает безопасные кросс-доменные запросы — это снижает риски при использовании fetch() и XMLHttpRequest. Однако CORS лишь управляет разрешёнными междоменными запросами, но не гарантирует безопасность.

Включение CORS в ASP.Net Core приложении

Чтобы добавить механизм CORS в ASP.NET приложение, нужно:

  1. Добавить сервисы CORS в контейнер сервисов приложения;
  2. Включить промежуточное ПО CORS в конвейер обработки HTTP-запросов.

В первом шаге необходимо вызвать метод расширения AddCors для интерфейса IServiceCollection. Он добавляет в контейнер две сущности: ICorsService и ICorsPolicyProvider.

			public static IServiceCollection AddCors(this IServiceCollection services)
{
    ArgumentNullException.ThrowIfNull(services);

    services.AddOptions();

    services.TryAdd(ServiceDescriptor.Transient<ICorsService, CorsService>());
    services.TryAdd(ServiceDescriptor.Transient<ICorsPolicyProvider, DefaultCorsPolicyProvider>());

    return services;
}

		

Существует также перегруженная версия этого метода расширения, позволяющая сконфигурировать политику CORS:

			public static IServiceCollection AddCors(this IServiceCollection services, Action<CorsOptions> setupAction)
		

Во втором шаге вызывается метод расширения UseCors определенного для интерфейса IApplicationBuilder.

			public static IApplicationBuilder UseCors(this IApplicationBuilder app)
{
    ArgumentNullException.ThrowIfNull(app);


    return app.UseMiddleware<CorsMiddleware>();
}

		

Этот метод добавляет промежуточное ПО CorsMiddleware в конвейер обработки HTTP-запросов. Для UseCors есть также перегруженная версия:

			public static IApplicationBuilder UseCors(this IApplicationBuilder app, string policyName)
		

Есть три способа подключения CORS:

  • В ПО промежуточного слоя с помощью именованной политики или политики по умолчанию.
  • Использование маршрутизации конечных точек.
  • С атрибутом [EnableCors].

[EnableCors] с именованной политикой обеспечивает лучший контроль в ограничении конечных точек, поддерживающих CORS.

CORS с именованной политикой и ПО промежуточного слоя

ПО промежуточного слоя CORS обрабатывает запросы между источниками. Следующий код применяет политику CORS ко всем эндпоинтам с указанными источниками:

			// Объявляем название политики CORS
const string MyAllowedOrigins = "_myAllowedOrigins";


var builder = WebApplication.CreateBuilder(args);
// Добавляем CORS в контейнер
builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowedOrigins,
                      policy =>
                      {
                          policy.WithOrigins("http://my-cdn1.com",
                                             "http://my-cdn2.com");
                      });
});

// services.AddResponseCaching();

builder.Services.AddControllers();

var app = builder.Build();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
// Подключаем промежуточное ПО в конвейер
// обработки запросов
app.UseCors(MyAllowedOrigins);

// app.UseResponseCaching();

// app.UseEndpoints();

app.MapControllers();

app.Run();
		

Пример кода выше — часть Program.cs, показывающая инициализацию WebApplication. В ней объявляется константа политики CORS — MyAllowedOrigins. Далее вызывается AddCors, в который через параметры передается имя политики и набор адресов разрешенных источников. Ниже есть вызов метода расширения UseCors, в который передано то же самое имя политики CORS.

Важно:

  • URL в настройках CORS не должны заканчиваться на '/', иначе заголовки не вернутся.
  • При использовании ПО промежуточного слоя кэширования ответов (ResponseCaching) UseCors должен вызываться перед UseResponseCaching.
  • При использовании Endpoints настройте CORS для выполнения между вызовами UseRouting и UseEndpoints.
  • Обычно UseStaticFiles вызывается раньше UseCors, но если JavaScript загружает файлы, UseCors следует вызвать раньше.

CORS с политикой по умолчанию и ПО промежуточного слоя

Следующий выделенный код включает политику CORS по умолчанию:

			var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddDefaultPolicy(
        policy =>
        {
            policy.WithOrigins("http://remote.com",
                               "http://www.cdn.com");
        });
});

builder.Services.AddControllers();

var app = builder.Build();

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();

app.UseCors();

app.UseAuthorization();

app.MapControllers();

app.Run();
		

Разница по сравнению с предыдущим фрагментом кода в том, что здесь явно не указывается имя политики CORS. Предполагается, что она будет единственной, в то время как в предыдущем примере можно объявить несколько политик CORS, каждую со своими настройками.

Включение CORS с маршрутизацией конечных точек

Когда используется вызов UseEndpoints, можно подключить политику CORS к каждой отдельно взятой конечной точке, при этом можно указать имя применяемой политики в методе RequireCors:

			var MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
                      policy =>
                      {
                          policy.WithOrigins("http://remote.com",
                                             "http://www.cdn.com");
                      });
});

builder.Services.AddControllers();
builder.Services.AddRazorPages();

var app = builder.Build();

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();

app.UseCors();

app.UseAuthorization();

app.UseEndpoints(endpoints =>
{
    endpoints.MapGet("/echo",
        context => context.Response.WriteAsync("echo"))
        .RequireCors(MyAllowSpecificOrigins);

    endpoints.MapControllers()
             .RequireCors(MyAllowSpecificOrigins);

    endpoints.MapGet("/echo2",
        context => context.Response.WriteAsync("echo2"));

    endpoints.MapRazorPages();
});

app.Run();
		

В предыдущем коде:

  • app.UseCors включает ПО промежуточного слоя CORS. Политики CORS привязываются к конечным точкам внутри метода UseEndpoints.
  • Конечные /echo точки и точки контроллера разрешают запросы между источниками с помощью указанной политики – MyAllowSpecificOrigins, переданной в RequireCors.
  • Конечные /echo2 точки и Razor страницы не разрешают запросы между источниками, так как политика по умолчанию не указана.

Включение CORS с помощью атрибутов

Атрибуты [EnableCors] позволяют подключить CORS только к конечным точкам, вместо глобальной настройки через промежуточное ПО. Плюс [EnableCors] без параметров включает политику CORS по умолчанию, а [EnableCors("{cors_policy_name}")] — именованную политику.

Атрибут [EnableCors] можно применить к:

  • Странице Razor Page;
  • Контроллеру целиком;
  • Методам действия контроллера (Action-methods).

Если CORS включён одновременно через атрибут и промежуточное ПО, применяются обе политики.

Microsoft не рекомендует смешивать способы подключения CORS. Рекомендуется в одном приложении либо применять атрибуты [EnableCors], либо использовать ПО промежуточного слоя.

Вот пример, как применить специфическую политику к каждому экшн-методу контроллера:

			[Route("api/[controller]")]
[ApiController]
public class WidgetController : ControllerBase
{
    // GET api/values
    [EnableCors("CorsPolicy1")]
    [HttpGet]
    public ActionResult<IEnumerable<string>> Get()
    {
        return new string[] { "green widget", "red widget" };
    }

    // GET api/values/5
    [EnableCors("CorsPolicy2")]
    [HttpGet("{id}")]
    public ActionResult<string> Get(int id)
    {
        return id switch
        {
            1 => "green widget",
            2 => "red widget",
            _ => NotFound(),
        };
    }
}

		

Здесь показано, как создать две именованные политики CORS:

			var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy("Policy1",
        policy =>
        {
            policy.WithOrigins("http://faraway.com",
                               "http://www.cdn.com");
        });

    options.AddPolicy("Policy2",
        policy =>
        {
            policy.WithOrigins("http://www.cloud.com")
                  .AllowAnyHeader()
                  .AllowAnyMethod();
        });
});

		

Чтобы обеспечить лучшее управление ограничением CORS-запросов, стоит:

  • применять атрибуты [EnbleCors(“{policy_name}”)] с именованной политикой;
  • не объявлять CORS-политику по умолчанию;
  • не использовать маршрутизацию конечных точек.

Как запретить CORS

Запретить CORS для отдельных экшн-методов контроллера можно с помощью атрибута [DisableCors].

Примечание: атрибут [DisableCors] не запрещает CORS, которая была подключена при помощи методов расширения RequireCors в настройках маршрутизации конечных точек.

Пример, как запретить CORS для экшн-метода GetValues2:

			[EnableCors("MyPolicy")]
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
    // GET api/values
    [HttpGet]
    public IActionResult Get() =>
        ControllerContext.MyDisplayRouteInfo();


    // GET api/values/5
    [HttpGet("{id}")]
    public IActionResult Get(int id) =>
        ControllerContext.MyDisplayRouteInfo(id);


    // PUT api/values/5
    [HttpPut("{id}")]
    public IActionResult Put(int id) =>
        ControllerContext.MyDisplayRouteInfo(id);
    // GET: api/values/GetValues2
    [DisableCors]
    [HttpGet("{action}")]
    public IActionResult GetValues2() =>
        ControllerContext.MyDisplayRouteInfo();
}

		

Обратите внимание, что политика CORS разрешена для всего контроллера, так как контроллер декорирован атрибутом [EnableCors(“MyPolicy”)], а экшн-метод GetValues2 — [DisableCors], значит, CORS-политика не будет применена к этому методу.

Параметры политики CORS

Настройка разрешенных источников

Есть два метода расширения для указания разрешенных источников:

  1. WithOrigins — позволяет указать список разрешенных источников;
  2. AllowAnyOrigin — разрешает CORS-запросы к любым источникам.

Они влияют на заголовок Access-Control-Allow-Origin предварительных запросов.

Настройка разрешенных HTTP методов

Для указания разрешенных HTTP-методов есть похожая на предыдущие пара методов:

  1. WithMethods — позволяет указать список разрешенных HTTP-методов;
  2. AllowAnyMethod — разрешает использование любых HTTP-методов в CORS-запросах.

Они тоже влияют на заголовок Access-Control-Allow-Methods preflight-запросов.

Настройка разрешенных HTTP-заголовков

Для указания разрешённых HTTP-заголовков используются:

  • WithHeaders — задаёт список разрешённых заголовков,
  • AllowAnyHeader — разрешает любые заголовки.

AllowAnyHeader влияет на preflight-запросы и заголовок Access-Control-Request-Headers. Если заголовок не разрешён в WithHeaders, запрос будет отклонён. Если сервер вернёт 200 OK, но без CORS-заголовков, браузер заблокирует cross-origin запрос.

Настройка доступных заголовков ответов

По умолчанию браузер не предоставляет все заголовки ответов приложениям. По умолчанию предоставляются только:

  • Cache-Control
  • Content-Language
  • Content-Type
  • Expires
  • Last-Modified
  • Pragma

В спецификации CORS эти заголовки называются simple response headers. Чтобы сделать другие заголовки доступными для приложения, вызовите метод расширения WithExposedHeaders, передав массив имен заголовков в качестве параметра:

			var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy("MyExposeResponseHeadersPolicy",
        policy =>
        {
            policy.WithOrigins("https://*.example.com")
                  .WithExposedHeaders("x-custom-header");
        });
});

builder.Services.AddControllers();

var app = builder.Build();
		

Передача учетных данных в запросах между источниками

В CORS-запросах учетные данные нужно специально обрабатывать. Речь идет о cookie и схемах аутентификации HTTP. Чтобы отправить учетные данные в CORS-запросе, клиент должен выставить свойство XMLHttpRequest.withCredentials в значение true.

Пример использования XMLHttpRequest напрямую в коде JavaScript:

			var xhr = new XMLHttpRequest();
xhr.open('get', 'https://www.my-cloud.com/api/test');
xhr.withCredentials = true;
		

Пример использования Fetch API:

			fetch('https://www.my-cloud.com/api/test', {
    credentials: 'include'
});

		

Сервер должен разрешить передачу учетных данных. Чтобы это сделать, вызовите метод расширения AllowCredentials:

			var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy("MyMyAllowCredentialsPolicy",
        policy =>
        {
            policy.WithOrigins("http://cdn.com")
                  .AllowCredentials();
        });
});

builder.Services.AddControllers();

var app = builder.Build();
		

В результате появится заголовок Access-Control-Allow-Credentials в ответе сервера, значит, он разрешил передачу данных. Если в ответе не будет этого заголовка, то браузер не предоставит ответ, и CORS-запрос завершится ошибкой.

Включение учетных данных в CORS-запросы — риск нарушения безопасности. Web-сайт из другого домена может отправить учетные данные аутентифицированного пользователя приложению от имени этого пользователя, но без его ведома.

Спецификация CORS также говорит, что при использовании заголовков Access-Control-Allow-Credentials нельзя применять вызов AllowAnyOrigin при создании политики CORS.

Предварительные (preflight) запросы

Для некоторых CORS-запросов браузер отправляет preflight-запрос с HTTP-методом OPTIONS перед отправкой самого CORS. Браузер может не отправлять предварительный запрос в этих случаях:

  • HTTP-метод основного CORS-запроса один из GET, HEAD или POST;
  • Приложение не добавляет в запрос заголовки, кроме Accept, Accept-Language, Content-Language, Content-Type, Last-Event-ID;
  • Запрос содержит заголовок Content-Type, то у него должно быть одно из значений: application/x-www-form-urlencoded, multipart/form-data, text/plain

Эти правила применяются к авторским заголовкам, которые задаются вызовом setRequestHeader объекта XMLHttpRequest. С User-Agent, Host, или Content-Length (заголовками браузера) так не работает.

У Preflight-запроса могут быть такие заголовки:

  • Access-Control-Request-Method — HTTP-метод ( в основном CORS-запросе);
  • Access-Control-Request-Headers — набор заголовков, которые будут установлены приложением в основном запросе.

Если предварительный запрос отклоняется сервером, то вылезет код 200 ОК, и браузер не будет пытаться отправлять CORS. При отладке в браузере (F12 tools) в консоли приложения появятся ошибки, если preflight-запрос отклонится сервером.

Чтобы разрешить передачу только определенных заголовков, вызывайте WithHeaders:

			using Microsoft.Net.Http.Headers;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy("MyAllowHeadersPolicy",
        policy =>
        {
            policy.WithOrigins("http://store.com")
                  .WithHeaders(HeaderNames.ContentType, "x-custom-header");
        });
});

builder.Services.AddControllers();

var app = builder.Build();
		

Чтобы разрешить передачу всех авторских заголовков, используйте вызов AllowAnyHeader.

Автоматическая обработка предварительных запросов

Если политика CORS применяется с помощью одного из приведенных ниже способов:

  • глобально, с помощью вызова app.UseCors в модуле Program.cs;
  • с помощью атрибута [EnableCors],

то ASP.NET будет автоматически обрабатывать запросы OPTIONS.

Обработка предварительных запросов с помощью атрибута [HttpOptions]

В ASP.NET предусмотрена возможность явного объявления методов для обработки предварительных запросов. Вот пример, как создать методы для обработки OPTIONS-запросов с [HttpOptions]:

			[Route("api/[controller]")]
[ApiController]
public class TodoItems2Controller : ControllerBase
{
    // OPTIONS: api/TodoItems2/5
    [HttpOptions("{id}")]
    public IActionResult PreflightRoute(int id)
    {
        return NoContent();
    }

    // OPTIONS: api/TodoItems2 
    [HttpOptions]
    public IActionResult PreflightRoute()
    {
        return NoContent();
    }
    . . .
}

		

Установка срока действия ответов для предварительных запросов

Браузер может закэшировать preflight-запросы, чтобы снизить нагрузку на сервер и сократить время обработки основных CORS-запросов. Чтобы ограничить срок хранения ответов в кэше, используйте заголовок Access-Control-Max-Age в ответе prefligh-запроса. Для этого при инициализации CORS-политики вызовите метод SetPreflightMaxAge:

			var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy("MySetPreflightExpirationPolicy",
        policy =>
        {
            policy.WithOrigins("http://outer.com")
                  .SetPreflightMaxAge(TimeSpan.FromSeconds(300));
        });
});

builder.Services.AddControllers();

var app = builder.Build();
		

CORS позволяет обращаться к ресурсам на других доменах, но требует аккуратной настройки, чтобы не сломать безопасность.

В примерах выше CORS-политики определялись в коде приложения, но в проде такой подход не всегда удобен. При переносе на другой хостинг или смене окружения настройки лучше вынести в конфигурацию сервера или прокси. Это упростит поддержку и адаптацию без правок в коде.

Полезные ссылки

Cross-Origin Resource Sharing (CORS) - HTTP | MDN

Enable Cross-Origin Requests (CORS) in ASP.NET Core | Microsoft Learn

Больше про .NET — в нашем тг канале!

Следите за новыми постами
Следите за новыми постами по любимым темам
263 открытий708 показов