最近的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
{
publicclass HomeController : Controller
{
staticstring _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;
}
<!DOCTYPEhtml>
<html>
<head>
<metaname="viewport"content="width=device-width"/>
<title>Kendo Editor Test</title>
<linkrel="stylesheet"
href="//kendo.cdn.telerik.com/2016.2.714/styles/kendo.common.min.css"/>
<linkrel="stylesheet"
href="//kendo.cdn.telerik.com/2016.2.714/styles/kendo.default.min.css"/>
<scriptsrc="//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處理(<變成<)可呈現HTML原始碼,@Html.Raw(ViewBag.Content)則將HTML內容變成網頁一部分,可呈現HTML裡<h1>、<span style="color:#444">等樣式效果。注意:Html.Raw()允許使用者輸入內容成為網頁HTML語法的一部分,跟SQL Injection漏洞原理相仿,存在被注入惡意程式碼的風險,使用時需嚴加防範攻擊!這部分後面再說明。
@{
Layout = null;
}
<!DOCTYPEhtml>
<html>
<head>
<metaname="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。
可使用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)],避免資料傳送被封鎖。
- HTML內容進入系統前應使用Sanitizer濾掉可能有害部分。