我們公司這幾年由於專案大多都是前後端分離的架構,所以很少用 ASP.NET Core MVC 來開發網站,但最近公司有個專案用到 ASP.NET Core MVC 框架,看到了一段 Code 覺得很陌生,所以就研究了一下,這才發現 ASP.NET Core MVC 在處理 HtmlEncode 的時候,預設的編碼範圍跟 ASP.NET MVC 有些不同,這篇文章就來記錄一下這些技術細節。
建立範例專案
-
首先建立一個 ASP.NET Core MVC 專案
dotnet new mvc -n AspNetCoreMvcDefaultHtmlEncoder
cd AspNetCoreMvcDefaultHtmlEncoder
dotnet new gitignore
-
在 HomeController
的 Index
Action 中加入一個 ViewBag
變數
ViewBag.Message = "如何用 ASP.NET Core 打造 Web 應用程式";
-
在 /Views/Home/Index.cshtml
加入以下內容
@{
ViewData["Title"] = "Home Page";
}
<div class="text-center">
<h1 class="display-4">Welcome</h1>
<p>@ViewBag.Message</p>
</div>
注意:所有透過 @
輸出的文字預設都會被編碼,所以這裡的 @ViewBag.Message
的輸出內容都會被進行 HtmlEncode 編碼。
-
啟動專案
dotnet run
此時查看執行的結果,從使用者的角度來看結果,基本上沒什麼問題:
但如果從「原始碼」的角度來看,那就會有點「不太一樣」了,因為這段文字的輸出如下:
<div class="text-center">
<h1 class="display-4">Welcome</h1>
<p>如何用 ASP.NET Core 打造 Web 應用程式</p>
</div>
你會發現所有的「中文字」全部都被編譯成 HTML Entities 格式了!
把所有中文字編碼會有問題嗎?
因為在 ASP.NET MVC 5 的時候並沒有這個問題,而到了 ASP.NET Core MVC 就開始變成這樣,這不經讓我納悶,為什麼要把所有的中文字都編碼呢?難道是因為安全性的考量嗎?還是因為其他的原因呢?
我先說說為什麼有 HtmlEncode 這樣的需求存在,貼上一段來自 HttpServerUtility.HtmlEncode 方法 (System.Web) 的備註說明:
HTML 編碼可確保在瀏覽器中正確顯示文字,而不會由瀏覽器解譯為 HTML。 例如,如果文字字串包含小於符號 <
或大於符號 >
,瀏覽器會將這些字元解譯為 HTML 標籤的開頭。當字元經過 HTML 編碼時,它們會轉換成字串 <
和 >
,這會導致瀏覽器顯示小於符號與大於符號。
所以,簡單來說,只要不影響 HTML 的解析,那就不需要編碼,但如果影響 HTML 的解析,那就需要編碼,這是一個很簡單的原則。
那我就更納悶了,為什麼 ASP.NET Core MVC 要把中文字元全部都編碼呢?難道直接顯示這些中文字會有問題嗎?
我想了一下,大多數預設的情況下,直接在 HTML 中輸入中文字應該都是沒有問題的,因為我們在 HTML 中直接輸入中文字,瀏覽器都可以正確顯示,那是因為現代的瀏覽器都可以正常識別 Unicode 文字,所以我們其實不需要編碼。
但是如果你的網頁輸出的字元編碼並非 Unicode 的話,那就真的有可能會出問題了。例如說你的網頁輸出的字元編碼是 Big5,要在 Big5 編碼的網頁 HTML 下顯示一個 Unicode 才有的文字,因為 Big5 的編碼範圍跟 Unicode 不同,所以就會出現亂碼,這時透過 HtmlEncode 將這些 Unicode 字元轉換成 HTML Entities 形式,這些文字就可以正確被識別了。
如何變更預設的 HtmlEncoder 編碼範圍
我有去追查 HtmlEncoder.cs 原始碼,發現 ASP.NET Core 預設的 HtmlEncoder 編碼範圍是 DefaultHtmlEncoder.BasicLatinSingleton
/// <summary>
/// Returns a default built-in instance of <see cref="HtmlEncoder"/>.
/// </summary>
public static HtmlEncoder Default => DefaultHtmlEncoder.BasicLatinSingleton;
而 DefaultHtmlEncoder.BasicLatinSingleton 預設是指定 UnicodeRanges.BasicLatin
編碼範圍:
internal static readonly DefaultHtmlEncoder BasicLatinSingleton = new DefaultHtmlEncoder(new TextEncoderSettings(UnicodeRanges.BasicLatin));
簡單來說,預設的 HtmlEncoder 會把所有非基本拉丁語系的文字進行編碼處理!不過,還好這個編碼範圍是可以變更的,我們可以在 ASP.NET Core 設定 DI Container 的地方設定一下 HtmlEncoder
的物件即可。以下是在 ASP.NET Core 的設定範例:
builder.Services.AddSingleton<HtmlEncoder>(
HtmlEncoder.Create(allowedRanges: new[] { UnicodeRanges.BasicLatin,
UnicodeRanges.CjkUnifiedIdeographs }));
這裡的 UnicodeRanges 類別提供一系列靜態屬性,會傳回對應 Unicode 規格中每個預先定義好的字碼區塊。上述程式的 UnicodeRanges.CjkUnifiedIdeographs
就會取得中日韓(CJK)文字的 Unicode 區塊 (U+4E00-U+9FCC),那麼設定上去後,該區塊的文字就不會被 HtmlEncode 編碼。
結論
其實我這次在 Code Review 的專案中,看到的程式是這樣寫的:
builder.Services.AddSingleton(HtmlEncoder.Create(UnicodeRanges.All));
他預設把所有 Unicode 範圍全部都加入了,我個人認為這樣的設定會讓網頁的輸出變得有點不安全,因為 Unicode 的文字範圍非常大,甚至還有一些不可見字元也被定義在 Unicode 之中,這樣的設定會讓網頁輸出的文字全部都不會被 HtmlEncode 編碼,所以「可能」有機會讓網頁容易受到 XSS 攻擊,所以我還是建議要適當的設定 HtmlEncoder 的編碼範圍。
最後提醒大家,在 ASP.NET Core MVC 使用 @
輸出內容因為一定會透過 HtmlEncoder
進行編碼,所以如果你要輸出的文字必須出現在 JavaScript 字串之中的話,就不太適合直接使用 @
輸出,而應該使用 JavaScriptEncoder
編碼過再輸出:
@{
var jsEncoder = System.Text.Encodings.Web.JavaScriptEncoder.Default;
}
<script>
var str = '@jsEncoder.Encode(ViewBag.Message)';
</script>
輸出範例:
<script>
var str = '\u5982\u4F55\u7528 \u0027ASP.NET Core\u0027 \u6253\u9020 Web \u61C9\u7528\u7A0B\u5F0F';
</script>
透過 JavaScriptEncoder.Default
編碼過的字串只會包含 Latin1 字元,因為所有中文也會進行編碼,所以不會有編碼範圍的問題,可以直接透過 @
輸出,不需要用到 @Html.Raw
輸出字串。
如果你想在 JavaScript 字串中可以看見中文字直接輸出,那就必須自訂一個 JavaScriptEncoder
編碼器,範例如下:
@{
var jsEncoder = System.Text.Encodings.Web.JavaScriptEncoder.Create(System.Text.Unicode.UnicodeRanges.All);
}
<script>
var str = '@Html.Raw(jsEncoder.Encode(ViewBag.Message))';
</script>
輸出範例:
<script>
var str = '如何用 \u0027ASP.NET Core\u0027 打造 Web 應用程式';
</script>
相關連結