﻿# 如何變更 ASP.NET Core MVC 的預設 HtmlEncoder 的編碼範圍

我們公司這幾年由於專案大多都是前後端分離的架構，所以很少用 ASP.NET Core MVC 來開發網站，但最近公司有個專案用到 ASP.NET Core MVC 框架，看到了一段 Code 覺得很陌生，所以就研究了一下，這才發現 ASP.NET Core MVC 在處理 HtmlEncode 的時候，預設的編碼範圍跟 ASP.NET MVC 有些不同，這篇文章就來記錄一下這些技術細節。

![HTML](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/9771729a85d60deed610-8a4339dc-e687-4d53-9f68-c044ab1e79f3.webp)

### 建立範例專案

1.  首先建立一個 ASP.NET Core MVC 專案
    
    ```bash
    dotnet new mvc -n AspNetCoreMvcDefaultHtmlEncoder
    cd AspNetCoreMvcDefaultHtmlEncoder
    
    dotnet new gitignore
    ```
    
2.  在 `HomeController` 的 `Index` Action 中加入一個 `ViewBag` 變數
    
    ```csharp
    ViewBag.Message = "如何用 ASP.NET Core 打造 Web 應用程式";
    ```
    
3.  在 `/Views/Home/Index.cshtml` 加入以下內容
    
    ```html
    @{
        ViewData["Title"] = "Home Page";
    }
    
    <div class="text-center">
        <h1 class="display-4">Welcome</h1>
        <p>@ViewBag.Message</p>
    </div>
    ```
    
    注意：所有透過 `@` 輸出的文字預設都會被編碼，所以這裡的 `@ViewBag.Message` 的輸出內容都會被進行 HtmlEncode 編碼。
    
4.  啟動專案
    
    ```bash
    dotnet run
    ```
    

此時查看執行的結果，從使用者的角度來看結果，基本上沒什麼問題：

![image](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/c78ac155b4eefabbb9f0-0c94ac7f-fb36-4c5e-a1c1-878dc56fcb2b.webp)

但如果從「原始碼」的角度來看，那就會有點「不太一樣」了，因為這段文字的輸出如下：

```html
<div class="text-center">
    <h1 class="display-4">Welcome</h1>
    <p>&#x5982;&#x4F55;&#x7528; ASP.NET Core &#x6253;&#x9020; Web &#x61C9;&#x7528;&#x7A0B;&#x5F0F;</p>
</div>
```

你會發現所有的「中文字」全部都被編譯成 [HTML Entities](https://www.w3schools.com/html/html_entities.asp) 格式了！

### 把所有中文字編碼會有問題嗎？

因為在 ASP.NET MVC 5 的時候並沒有這個問題，而到了 ASP.NET Core MVC 就開始變成這樣，這不經讓我納悶，為什麼要把所有的中文字都編碼呢？難道是因為安全性的考量嗎？還是因為其他的原因呢？

我先說說為什麼有 HtmlEncode 這樣的需求存在，貼上一段來自 [HttpServerUtility.HtmlEncode 方法 (System.Web)](https://learn.microsoft.com/zh-tw/dotnet/api/system.web.httpserverutility.htmlencode?view=netframework-4.8.1&WT.mc_id=DT-MVP-4015686) 的**備註**說明：

> HTML 編碼可確保在瀏覽器中正確顯示文字，而不會由瀏覽器解譯為 HTML。 例如，如果文字字串包含小於符號 `<` 或大於符號 `>`，瀏覽器會將這些字元解譯為 HTML 標籤的開頭。當字元經過 HTML 編碼時，它們會轉換成字串 `&lt;` 和 `&gt;`，這會導致瀏覽器顯示**小於符號**與**大於符號**。

所以，簡單來說，只要不影響 HTML 的解析，那就不需要編碼，但如果影響 HTML 的解析，那就需要編碼，這是一個很簡單的原則。

那我就更納悶了，為什麼 ASP.NET Core MVC 要把**中文字元**全部都編碼呢？難道直接顯示這些中文字會有問題嗎？

我想了一下，大多數預設的情況下，直接在 HTML 中輸入中文字應該都是沒有問題的，因為我們在 HTML 中直接輸入中文字，瀏覽器都可以正確顯示，那是因為現代的瀏覽器都可以正常識別 Unicode 文字，所以我們其實不需要編碼。

但是如果你的網頁輸出的字元編碼並非 Unicode 的話，那就真的有可能會出問題了。例如說你的網頁輸出的字元編碼是 Big5，要在 Big5 編碼的網頁 HTML 下顯示一個 Unicode 才有的文字，因為 Big5 的編碼範圍跟 Unicode 不同，所以就會出現亂碼，這時透過 HtmlEncode 將這些 Unicode 字元轉換成 HTML Entities 形式，這些文字就可以正確被識別了。

### 如何變更預設的 HtmlEncoder 編碼範圍

我有去追查 [HtmlEncoder.cs](https://github.com/dotnet/runtime/blob/main/src/libraries/System.Text.Encodings.Web/src/System/Text/Encodings/Web/HtmlEncoder.cs#L16) 原始碼，發現 ASP.NET Core 預設的 HtmlEncoder 編碼範圍是 `DefaultHtmlEncoder.BasicLatinSingleton`

```csharp
/// <summary>
/// Returns a default built-in instance of <see cref="HtmlEncoder"/>.
/// </summary>
public static HtmlEncoder Default => DefaultHtmlEncoder.BasicLatinSingleton;
```

而 [DefaultHtmlEncoder.BasicLatinSingleton](https://github.com/dotnet/runtime/blob/main/src/libraries/System.Text.Encodings.Web/src/System/Text/Encodings/Web/DefaultHtmlEncoder.cs#L13) 預設是指定 `UnicodeRanges.BasicLatin` 編碼範圍：

```csharp
internal static readonly DefaultHtmlEncoder BasicLatinSingleton = new DefaultHtmlEncoder(new TextEncoderSettings(UnicodeRanges.BasicLatin));
```

簡單來說，預設的 HtmlEncoder 會把所有非基本拉丁語系的文字進行編碼處理！不過，還好[這個編碼範圍是可以變更的](https://learn.microsoft.com/en-us/aspnet/core/security/cross-site-scripting?WT.mc_id=DT-MVP-4015686#customizing-the-encoders)，我們可以在 ASP.NET Core 設定 DI Container 的地方設定一下 `HtmlEncoder` 的物件即可。以下是在 ASP.NET Core 的設定範例：

```csharp
builder.Services.AddSingleton<HtmlEncoder>(
     HtmlEncoder.Create(allowedRanges: new[] { UnicodeRanges.BasicLatin,
                                               UnicodeRanges.CjkUnifiedIdeographs }));
```

這裡的 [UnicodeRanges](https://learn.microsoft.com/zh-tw/dotnet/api/system.text.unicode.unicoderanges?WT.mc_id=DT-MVP-4015686) 類別提供一系列靜態屬性，會傳回對應 Unicode 規格中每個預先定義好的字碼區塊。上述程式的 `UnicodeRanges.CjkUnifiedIdeographs` 就會取得**中日韓**(CJK)文字的 Unicode 區塊 (U+4E00-U+9FCC)，那麼設定上去後，該區塊的文字就不會被 HtmlEncode 編碼。

### 結論

其實我這次在 Code Review 的專案中，看到的程式是這樣寫的：

```csharp
builder.Services.AddSingleton(HtmlEncoder.Create(UnicodeRanges.All));
```

他預設把所有 Unicode 範圍全部都加入了，我個人認為這樣的設定會讓網頁的輸出變得有點不安全，因為 Unicode 的文字範圍非常大，甚至還有一些不可見字元也被定義在 Unicode 之中，這樣的設定會讓網頁輸出的文字全部都不會被 HtmlEncode 編碼，所以「可能」有機會讓網頁容易受到 XSS 攻擊，所以我還是建議要適當的設定 HtmlEncoder 的編碼範圍。

最後提醒大家，在 ASP.NET Core MVC 使用 `@` 輸出內容因為一定會透過 `HtmlEncoder` 進行編碼，所以如果你要輸出的文字必須出現在 JavaScript 字串之中的話，就不太適合直接使用 `@` 輸出，而應該使用 `JavaScriptEncoder` 編碼過再輸出：

```html
@{
    var jsEncoder = System.Text.Encodings.Web.JavaScriptEncoder.Default;
}

<script>
    var str = '@jsEncoder.Encode(ViewBag.Message)';
</script>
```

輸出範例：

```html
<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` 編碼器，範例如下：

```html
@{
    var jsEncoder = System.Text.Encodings.Web.JavaScriptEncoder.Create(System.Text.Unicode.UnicodeRanges.All);
}

<script>
    var str = '@Html.Raw(jsEncoder.Encode(ViewBag.Message))';
</script>
```

輸出範例：

```html
<script>
    var str = '如何用 \u0027ASP.NET Core\u0027 打造 Web 應用程式';
</script>
```

### 相關連結

-   [本文範例程式已放到 GitHub 公開](https://github.com/doggy8088/AspNetCoreMvcDefaultHtmlEncoder/commit/08f14d8ae9f6374ab2d275d3d4df42c33959c260)
-   [Prevent Cross-Site Scripting (XSS) in ASP.NET Core | Microsoft Learn](https://learn.microsoft.com/en-us/aspnet/core/security/cross-site-scripting?WT.mc_id=DT-MVP-4015686#customizing-the-encoders)
-   [UnicodeRanges 類別 (System.Text.Unicode) | Microsoft Learn](https://learn.microsoft.com/zh-tw/dotnet/api/system.text.unicode.unicoderanges?WT.mc_id=DT-MVP-4015686)
-   [ASP.NET Core View 中文變 & # x4E2D; & # x6587;-黑暗執行緒](https://blog.darkthread.net/blog/aspnetcore-cshtml-chinese-char-encoding/)
