最近的ASP.NET MVC專案用到了RichText編輯器,允許使用者編輯包含不同字型、大小、粗細、顏色的格式化文字,其中有些需注意細節,整理筆記備忘。

網頁版RichText編譯器的選擇不少,本文以KendoEditor為例,結果則以PostBack方式回傳。即使換用其他編輯器或改以AJAX回傳,ASP.NET MVC整合重點大同小異。

範例的MVC網站共有Index及Result兩個View,Index為編輯器頁面,Result則用來顯示結果。Controller除了Index及Result兩個Action,再增加一個Sumbit Action,負責接受前端送回內容,模擬將結果寫入DB(為求簡化,以保存在記憶體替代)供Result View讀取顯示,接著導向Result View顯示編輯結果。

HomeController.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
 
namespace Mvc.Controllers
{
    public class HomeController : Controller
    {
        static string _content = string.Empty;
        void SaveToDb(string content)
        {
            //模擬寫入DB
            _content = content;
        }
        string ReadFromDb()
        {
            //模擬由DB讀取
            return _content;
        }
 
 
        public ActionResult Index()
        {
            return View();
        }
 
        [HttpPost]
        [ValidateInput(false)]
        public ActionResult Submit(string content)
        {
            SaveToDb(content);
            return RedirectToAction("Result");
        }
 
        public ActionResult Result()
        {
            ViewBag.Content = ReadFromDb();
            return View();
        }
    }
}

Index.cshtml已盡量簡化,網頁只有一個KendoEditor及一顆送出鈕,送出前透過JavaScript取出編輯結果(HTML)存入<input type="hidden" name="content" />,傳送給Submit Action接收:

 
@{
    Layout = null;
}
 
<!DOCTYPE html>
 
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Kendo Editor Test</title>
    <link rel="stylesheet" 
href="//kendo.cdn.telerik.com/2016.2.714/styles/kendo.common.min.css" />
    <link rel="stylesheet" 
href="//kendo.cdn.telerik.com/2016.2.714/styles/kendo.default.min.css" />
    <script src="//kendo.cdn.telerik.com/2016.2.714/js/jquery.min.js"></script>
    <script src="//kendo.cdn.telerik.com/2016.2.714/js/kendo.all.min.js"></script>
</head>
<body>
    <div>
        @using (Html.BeginForm("Submit", "Home"))
        {
            <textarea id="editor" style="width: 480px; height: 200px;">
                黑暗執行緒
            </textarea>
            <input type="hidden" id="content" name="content" />
            <button id="submit" type="submit">Submit</button>
        }
    </div>
 
    <script>
        $("#editor").kendoEditor({
            tools: [
                "formatting",
                "bold",
                "italic",
                "underline",
                "strikethrough",
                "foreColor",
                "backColor"
            ]
        });
        var editor = $("#editor").data("kendoEditor");
        $("#submit").click(function () {
            $("#content").val(editor.value());
        });
    </script>
</body>
</html>

Result.cshtml也很單純,在Server端將HTML內容存入ViewBag.Content,View裡以@ViewBag.Content顯示的結果經過HtmlEncode處理(<變成&lt;)可呈現HTML原始碼,@Html.Raw(ViewBag.Content)則將HTML內容變成網頁一部分,可呈現HTML裡<h1>、<span style="color:#444">等樣式效果。注意:Html.Raw()允許使用者輸入內容成為網頁HTML語法的一部分,跟SQL Injection漏洞原理相仿,一旦引用就存在被注入惡意程式碼的風險,若一定要使用需嚴防XSS攻擊!這部分後面會再說明。

 
@{
    Layout = null;
}
 
<!DOCTYPE html>
 
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>結果顯示</title>
    <style>fieldset { width: 400px; height: 120px; }</style>
</head>
<body>
     <fieldset>
        <legend>輸入內容</legend>
        <div>@ViewBag.Content</div>
    </fieldset>
    <fieldset>
        <legend>HTML顯示結果</legend>
        <div>@Html.Raw(ViewBag.Content)</div>
    </fieldset>
</body>
</html>

就這樣,一個提供使用者編輯格式化文字內容的網頁介面就完成了。

接下來,來談談幾個需要注意的地方。

第一,Submit Action宣告為[HttpPost],不允許以GET方式執行。原因:永遠不要使用GET方式接收指令進行資料更新!

第二,在ActionResult Submit(string content)上有個[ValidateInput(false)],目的在關閉Request內容檢核。基於安全考量,ASP.NET MVC預設會攔截包含XML標籤的Request內容,避免有心人士透過Action注入XSS攻擊程式。但在RichText編輯情境,content包含HTML是正常的,若不設定[ValidateInput(false)]停用檢核機制,送出資料時會出現錯誤:

具有潛在危險Request.Form的值已從用戶端(content="<h2><span style="col…")偵測到。

關閉ValidateInput代表我們預期並接受content參數包含HTML語法,但於此同時也開始要承擔「content內容可能包藏XSS攻擊」風險。等等,KendoEditor並不容許輸入<script>、<iframe>,使用者應該沒法搞怪吧?錯!只要資料來自前端由使用者提供,處處隱藏殺機,例如以下XSS注入示範:

不需用特殊道具,瀏覽器開啟F12跑一行指令,即可篡改傳送內容加入惡意程式碼,若Result View是公眾瀏覽的頁面,就可能被當成發動攻擊的跳板。

第三點,要防止使用者輸入HTML夾帶惡意程式,最有效的方法是使用Sanitizer工具進行過濾,只保留白名單列舉的HTML標籤,排除可能夾帶惡意內容的管道。至於過濾工具,過去大家蠻常用的AntiXSS Library Sanitizer,處於3.x版不夠安全,4.x版把不該殺的也殺光光的尷尬處境(4.x版被一顆星評價洗版),已不再是好選擇。重新評估,我選擇較活躍的開源專案-HtmlSanitizer

【2016-08-22更新】感謝Bruce補充,AntiXSS Library在4.3.1版再改回白名單保留邏輯(被罵了兩年,呼~),可再納入考量。

可使用NuGet安裝:

裝妥後在Submit()加上content = new HtmlSanitizer().Sanitize(content),即可過濾content可能有害的內容,前述示範惡意插入的JavaScript會整段被移除。

[HttpPost]
[ValidateInput(false)]
public ActionResult Submit(string content)
{
    content = new HtmlSanitizer().Sanitize(content);
    SaveToDb(content);
    return RedirectToAction("Result");
}

重新整理重點:

  • Razor語法插入後端內容時預設會經過HtmlEncode,基本上能有效防止XSS攻擊。但RichText在呈現時必須原始呈現,需使用@Html.Raw()嵌入頁面。使用Html.Raw()代表使用者輸入內容有可能成為網頁HTML一部分,務必從嚴檢核,防範被插入惡意程式。
  • 接收資料進行更動作業的Action宜加上[HttpPost]降低被攻擊機率。
  • 接收HTML資料的Action需加上[ValidateInput(false)],避免資料傳送被封鎖。
  • 關閉ValidateInput後,防範攻擊就變成我們的責任,HTML內容進入系統前應使用Sanitizer濾掉可能有害部分。
    注意:所有可能以Html.Raw()內嵌或直接成為網頁HTML一部分的輸入參數都應該處理。

[2016-08-19更新]

關閉ValidateInput後Action的所有參數都允許傳入HTML,如要進一步限定只開放某個參數接受HTML,使用AllowHTML Attribute會更安全,可一次避免其他參數被植入XSS攻擊的風險,感謝demo補充。


Comments

# by demo

建議不要使用[ValidateInput(false)] 因為這樣會關閉整個Action的檢查,使用Viewmodel接受資料,並且針對唯一需要開放 HTML的屬性使用 AllowHTML attribute 可以進一步的把開放的空間再縮小

# by Jeffrey

to demo, 感謝補充,已加入本文。

# by KKbruce

AntiXSS 4.3.0 有改回來哦 參考:http://blog.kkbruce.net/2014/06/microsoft-web-protection-library-430.html

# by Jeffrey

to KKBruce,感謝提供,已補充於本文。

Post a comment