資訊處理安全確保支援士(情報処理安全確保支援士) 2023年秋季(令和5年)午後問1解說 ── 16則評論卻只顯示2則的儲存型XSS

· · 資訊處理安全確保支援士, 登錄資訊處理安全確保支援士, XSS, 跨站指令碼攻擊, Web應用程式, 資訊安全, 漏洞, IPA, 工作階段管理

「明明應該有16則評論,卻只顯示2則。」

資訊處理安全確保支援士(情報処理安全確保支援士)考試2023年秋季(令和5年)午後問1,正是從這樣一則使用者洽詢開始的1。畫面切換沒有任何異常,也沒有出現錯誤。只是顯示的件數對不上。

原因是儲存型跨站指令碼攻擊(XSS)。不過這道題有趣的地方,並不在於「XSS」這個答案本身,而在於Q公司(題目中登場的電商業者)原本已經備妥的那些「看似合理的對策」,竟然被逐一穿透。評論標題的50字限制被突破,上傳所需的權杖是透過正規流程取得的,被竊取的工作階段 ID 也未經外部伺服器就被帶走了。

本文將逐題追蹤這道大題,整理哪些對策沒有起作用,哪些原本應該起作用的對策又是在哪個環節分道揚鑣的

透過本文可以獲得的,除了考試的解題思路(各設問的解答範例及其依據)之外,還有可用於實務的XSS對策全貌。為了讓準備考試的讀者能按設問逐節閱讀,也讓只需要實務程式碼審查觀點的讀者能直接從第9章、第10章讀起,本文在結構上做了相應安排,兩種讀法都能讀通。

另外,關於該以什麼為基準來全面確認整個網站安全性的宏觀話題,已經在把 IPA《安全的網站製作方式》當作檢查清單來使用一文中討論過。本文則是從那裡列出的11種漏洞中挑出XSS這一項,透過實例做深入剖析。

1. 先講結論

  • 漏洞的種類是儲存型XSS。攻擊者的字串被保存在伺服器上,此後輸出到所有開啟頁面之人的HTML之中。嵌入的腳本是否使用DOM的API,與是否屬於DOM Based XSS是兩回事
  • 輸入字數限制沒能成為XSS對策。攻擊者把投稿拆分成15次,讓夾在投稿之間的HTML被JavaScript註解跳過,藉此串成一整支腳本
  • 上傳用的權杖(CSRF對策)同樣沒能成為XSS對策。攻擊腳本按照與正規畫面相同的步驟取得了權杖。既然運作在同一個來源中,正規使用者能做的事,它就全都能做
  • 被竊取的工作階段 ID 並沒有送到外部。攻擊者把Cookie的內容做成名為「a.png」的圖片檔,放進網站自身的頭像上傳功能,再以正常瀏覽的方式下載回收。出口對策無法察覺這條路徑
  • 題目明確指出Q公司欠缺的對策共有3項。輸出時的跳脫(根本解決)、Cookie的HttpOnly屬性上傳檔案的格式確認(這兩項屬於保險性對策)。只要具備其中任何一項,這條攻擊鏈就會被切斷。不過格式確認在攻擊者改變手法後依然會被通過,因此還需要做到再編碼
  • 題目中沒有出現,但CSP的 script-src 同樣能夠切斷這條攻擊鏈。只要配置成不允許 'unsafe-inline',嵌入的內嵌腳本從一開始就不會被執行

2. 關於題材 ── 出處與本文的處理方式

本文所討論的是以下這道題目。

出處:2023年秋季(令和5年) 資訊處理安全確保支援士考試 午後 問1

IPA表示,除法令另有規定的情況外,使用其公開的歷屆考試試題無須取得許可或支付使用費。但這並不代表IPA放棄了著作權:IPA要求使用者必須以「年度、期、考試類別、時間段、題號等」的形式明確標註出處,若對試題內容做了部分改寫,也必須一併注明2

本文並未直接轉載試題冊中刊載的HTML與腳本,而是在說明機制所需的範圍內,替換為本公司自行撰寫的等效範例程式碼。設問原文與解答範例同樣以摘要形式處理。試題冊、解答範例、評分講評的原文皆可從IPA的頁面免費下載,建議讀者搭配原文一併閱讀1 3 4

設問與本文的對應

為方便一邊翻開試題冊一邊閱讀的讀者,這裡列出設問與本文各節的對應關係。也可以直接從想解的設問處讀起。

設問 所問內容(字數) 本文對應節次
設問1(1) 遭濫用的XSS漏洞種類(3擇1) 第4章
設問1(2) Web應用程式 Q 應採取的對策(30字以內) 第4章「設問1(2):對策」
設問2 讓超過輸入字數限制長度的腳本得以執行的方法(50字以內) 第5章
設問3(1) 攻擊腳本第6~20行的處理內容(60字以內) 第6章
設問3(2) 攻擊者取得上傳資訊的方法(50字以內) 第7章
設問3(3) 取得的資訊能用來做什麼(40字以內) 第7章「設問3(3):工作階段 ID 能用來做什麼」
設問4 攻擊者的網域無法讓攻擊成功的瀏覽器機制(40字以內) 第8章

只需要脫離設問、直接看實務討論的讀者,請從第9章(發揮作用的對策與未發揮作用的對策一覽)與第10章(程式碼審查的觀點)讀起。想以一張圖看懂整條攻擊鏈的話,第7章的循序圖就是從投稿到回收的完整全貌。

試題冊的敘述與本文範例的對應

為了方便對照原文,這裡整理出哪些地方做了什麼樣的替換。

試題冊的敘述 本文的處理方式 刊載位置
頁面 V 的HTML(含分段投稿的評論標題) 未直接轉載,改由本公司撰寫將同樣機制縮減為3則投稿的等效範例 第5章
擷取出的攻擊腳本(約20行) 未直接轉載,改由本公司撰寫執行相同處理的等效JavaScript。程式碼中的註解為本文解說用途 第6章
各設問的題目文字 保留要旨的摘要(字數限制等條件維持原文數值) 第4~8章各節開頭
解答範例 IPA公開的解答範例3 第4~8章各處
評分講評 IPA公開的評分講評中相關部分4 第4章、第5章、第7章
題目的規格(Q公司、頁面 V、會員A/B、功能與字數限制等) 依原文敘述進行摘要 第2章「題目的場景」

題目的場景

登場的是經營服飾電商、員工人數100人的Q公司。該公司以自行開發的「Web應用程式 Q」營運電商網站,使用者以HTTPS存取。這次的設定是新增了會員商品評論功能。

必須先掌握的規格有以下5點。

功能 規格
登入 以會員ID與密碼進行驗證,以Cookie形式發放工作階段 ID
商品評論 只有已登入會員能投稿。評論標題有50字、評論內文有300字的輸入字數限制,兩者皆為自由填寫
會員個人檔案 提供上傳頭像圖片的頁面,以及登錄信用卡資訊的頁面。兩者都僅限已登入會員使用
頭像圖片上傳 /user/upload 傳送圖片檔與權杖作為參數。只有當權杖與 /user/profile 所發放的權杖一致時才會成功
頭像圖片顯示 已上傳的頭像圖片,會顯示在會員個人檔案設定頁面與評論頁面

最後兩行,稍後會發揮作用。

3. 症狀 ── 為什麼16則變成了2則

會員傳來「素色T恤的評論頁面(頁面 V)應該顯示16則評論,卻只顯示2則」的洽詢。開發部的N先生打開頁面 V,畫面上看到的是這樣的內容:

  • 頁首顯示「16則評論」
  • 會員A的評論1則(標題「Good」,內文「Nice shirt!」)
  • 會員B的評論1則
  • 末尾顯示「以上,共16則評論」

件數顯示16則,實際排列出來的卻只有2則。這時確認HTML,發現會員A的投稿其實存在15則,其中還嵌入了一支很長的腳本。

也就是說,評論本身確實有16則份都輸出到了HTML之中。儘管如此卻只看得到2則,是因為其中大部分變成了 <script> 元素的內容,不再被瀏覽器視為「應當顯示的內容」。

被吞沒的範圍,精確來說並不是「整整15則」。開始標籤 <script> 出現在第1則評論標題的中途(「Good」之後),結束標籤 </script> 則位在第15則評論標題的末尾。因此:

  • 第1則卡片(頭像・顯示名稱・日期・星等,一直到標題的「Good」為止)位於 <script> 之前,所以會顯示
  • 從那之後一直到第15則標題末尾的內容,都被吞進了script元素的內容之中,不會顯示
  • 第15則剩下的部分(內文的「Nice shirt!」)位於 </script> 之後,所以會顯示

結果,第1則的頁首部分與第15則的內文連了起來,畫面上看起來就像「標題是Good、內文是Nice shirt!,會員A的一則評論」。加上會員B的1則,總共2則。攻擊者在第1則開頭放上「Good」、在第15則內文放上「Nice shirt!」這樣自然的字串,恐怕是為了不讓顯示異常看起來太突兀

這種「件數對得上,顯示卻對不上」的症狀解讀方式,在實務上也很有幫助。因為它可以成為懷疑輸出的HTML結構是否遭到破壞、而不是顯示件數邏輯有Bug的一個切入點。

4. 為什麼是「儲存型」XSS ── 設問1

設問1要求從DOM Based XSS、儲存型XSS、反射型XSS這3個選項中,選出這次攻擊所使用的XSS漏洞種類。正確答案是儲存型XSS

而IPA的評分講評對這個設問是這樣寫的。

正答率大致平均,但或許是因為腳本中使用了DOM,可以看到不少考生誤答為「DOM Based XSS」。

題目中出現的攻擊腳本,使用了 XMLHttpRequest,把回應當作DOM接收,並用 getElementById 取得元素。它的確在操作DOM。然而,這與漏洞的種類無關

分水嶺在於「攻擊字串在哪裡變成腳本」

區分這3種類型的關鍵,在於攻擊字串在哪裡變成可執行的程式碼,也就是脆弱的輸出位置(Sink)是位在伺服器端還是瀏覽器端。

種類 Sink(攻擊字串變得可執行的位置) 攻擊字串從哪裡來
反射型XSS 伺服器組合HTML的處理 攻擊者準備的URL參數等,該次請求本身
儲存型XSS 伺服器組合HTML的處理 保存在伺服器端的資料
DOM Based XSS 瀏覽器上的JavaScript(賦值給 innerHTMLevaldocument.write 等) URL的片段、postMessage、伺服器回傳的值等

這道題中,伺服器把攻擊者投稿的評論標題原封不動地嵌入HTML後回傳。Sink位在伺服器端的輸出處理中,該字串會被保存到資料庫,此後分發給所有開啟頁面 V 的人。因此屬於儲存型。

瀏覽器上的JS傳給innerHTML等的時候是包含在伺服器組出的HTML中有保存-此後輸出給所有人沒有保存-只在該次請求中攻擊者的字串被當成腳本執行是在哪裡變得可以執行DOM Based XSS該字串是否保存在伺服器上儲存型XSS反射型XSS

另外,把「伺服器回應中含有攻擊字串就是儲存型/反射型」這樣單純化來理解是危險的。即使伺服器把值以無害的文字或JSON形式回傳,只要瀏覽器端的JavaScript把它賦值給 innerHTML,就會構成DOM Based XSS(這是由保存下來的值所引發、所謂的stored DOM XSS)。這種情況下該修正的並非伺服器的輸出處理,而是用戶端的Sink,因此請不要以回應中是否看得到字串來判斷,而要以它是在哪裡變得可執行來判斷

嵌入的腳本會做什麼(操作DOM、進行通訊、讀取Cookie)與那支腳本是如何進入頁面的,必須分開來思考。之所以容易混淆,不是因為選擇對策時會感到困擾,而是因為會選錯對策。若判斷為DOM Based XSS,就會得出「只要修正用戶端的JavaScript即可」的結論,但實際上該修正的是伺服器端的輸出處理。

設問1(2):對策

設問1(2)要求以30字以內回答Web應用程式 Q 應採取的對策。解答範例是「輸出評論標題之前先施加跳脫處理」。

這裡「輸出之前」這個順序被明確標示出來,是重要的一點。並不是在接收輸入時就跳脫,而是在即將輸出為HTML之前,依照輸出所在的文法脈絡進行跳脫。IPA《安全的網站製作方式》中,也把「對輸出到網頁的所有元素施加跳脫處理」列為XSS根本解決的第一項5

「SQL注入不也是靠輸入時跳脫嗎?」

這裡一定會出現的疑問。結論先講:SQL注入同樣也不是靠「輸入時跳脫」來因應的

IPA提出的SQL注入根本解決是「SQL陳述式的組合全部以預留位置實作」。文中提到,在不得不以字串串接組合SQL陳述式的情況下,可以用跳脫處理作為替代方案,但那也寫成「以字串串接組合SQL陳述式時,須使用可進行跳脫等處理的資料庫引擎API,正確組成SQL陳述式的字面值」,也就是說跳脫的是SQL陳述式構成的那一瞬間6,而不是接收輸入的時間點。

也就是說,這與HTML的情況結構完全相同。共通的原則是這樣的。

跳脫應該在資料即將進入哪種文法世界這件事被確定下來的地方進行。

  • 要輸出成HTML,就做HTML跳脫
  • 要成為SQL陳述式的一部分,就用預留位置(不得已時則以SQL字面值的形式跳脫)
  • 要成為shell命令的一部分,就照shell的方式處理

為什麼不能在輸入時就跳脫呢?因為在接收輸入的那個時間點,那筆資料未來會輸出到哪裡尚未確定。同一則評論內文,可能會輸出到HTML頁面,也可能輸出到CSV匯出檔、JSON的API回應、通知郵件,或紀錄檔中。如果在輸入時就先做了HTML跳脫,CSV中就會原封不動地出現 &amp; 這類字串,資料庫中的內容也會與原始輸入不一致,導致搜尋與統計出錯,還容易成為雙重跳脫的溫床。

那麼輸入端就什麼都不用做嗎

不是的。只不過輸入端該做的不是跳脫,而是驗證(Validation)。兩者的角色不同。

  • 在輸入端「擋下」 ── 拒絕接受規格上不可能出現的值。例如郵遞區號欄位拒絕非數字7位、數量欄位拒絕負數。這是為了守護資料正確性、必須獨立存在的處理
  • 在輸出端「跳脫」 ── 以即將輸出所在的文法,把接受下來的資料安全地表現出來。這才是漏洞的根本解決

而且,不能對輸入端的驗證過度期待XSS防護效果,這一點也很重要。IPA在談到XSS時,雖然提及確認輸入值是否符合應用程式規格的做法,但也明確指出這項對策的有效性有限,若應用程式規格允許輸入範圍廣泛的字元種類,就無法構成對策,因此不建議依賴這個方法5

這次的評論標題與評論內文,正是允許輸入範圍廣泛字元種類的自由填寫規格。雖然設有50字、300字的上限,但讓腳本得以運作所需的字數遠比這少得多,所以「存在上限」這件事本身並無法構成防禦。實際上Q公司發生了什麼,我們在接下來的設問2中細看。

5. 50字限制是如何被突破的 ── 設問2

設問2是這道題的精華所在。

關於圖3,請以50字以內回答讓超過輸入字數限制長度的腳本得以執行的方法。

解答範例是「分成多次進行投稿,讓HTML被註解掉、連成一整支腳本」。

攻擊者把腳本拆分成能收在單次投稿長度以內的片段,分15次投稿。關鍵在於如何處理必然夾在各則投稿之間的HTML(</div><div class="..."> 等)。這些內容若原樣出現在JavaScript中,會構成語法錯誤。

這時候派上用場的,就是JavaScript的區塊註解。以下是本公司為說明此機制而撰寫的簡化版本(並非直接引用試題冊中的圖示)。

假設分3次在評論標題欄中做了以下投稿。

第1則: 太棒了<script>a=1;/*
第2則: */b=2;/*
第3則: */c=3;</script>

伺服器會把這些內容分別當成各則評論的標題,依以下方式輸出為HTML。

<div class="review-title">太棒了<script>a=1;/*</div>
<div class="description">…</div>
<div class="review-title">*/b=2;/*</div>
<div class="description">…</div>
<div class="review-title">*/c=3;</script></div>

從瀏覽器的角度來看,從最開始的 <script> 到最後的 </script> 之間構成了一個script元素。其內容如下所示。

a=1;/*</div>
<div class="description">…</div>
<div class="review-title">*/b=2;/*</div>
<div class="description">…</div>
<div class="review-title">*/c=3;

夾在 /**/ 之間的HTML,會被當成JavaScript的註解而跳過,實際會執行的只有 a=1; b=2; c=3; 這部分。而被當成註解跳過的部分,既然屬於script元素的內容,就不會顯示在畫面上。這正是「16則看起來變成2則」這個症狀的真相。

從這裡應該帶回的認知

這裡請不要弄錯因果關係。輸入字數限制之所以無法成為XSS對策,並不是因為投稿次數沒有限制,而是因為只要有50個字,腳本本來就能夠執行。只要多寫一個事件處理屬性,或放一個用來載入外部腳本的標籤,都能收在幾十個字之內。就算把投稿次數限制為1次,字數限制也起不到防禦作用。

那麼這次的分段投稿究竟是為了什麼?那是為了把長達約20行的完整腳本整段帶進來的手段。原因在於攻擊者想做的事情本身很長,所以靠增加次數來湊夠長度,這與「字數限制為何被突破」並不是同一回事。手法本身的說明,和把它評價為對策是否有效,必須分開來看。

同樣的道理,也適用於其他一切「輸入端的限制」。

  • 字數限制、可輸入字元種類的限制、前端驗證 ── 這些都是規格上必要的東西,無法取代XSS的根本解決
  • 攻擊者始終有辦法透過分段、編碼、從其他路徑投入等手段,繞過輸入端的限制
  • 真正該守護的地方,是資料即將輸出為HTML的那一瞬間

另外,IPA的評分講評針對這個設問也寫道:「有部分解答像『用開發者工具刪除輸入限制後再投稿』這樣,可以看出確認得不夠充分。」的確,前端的限制可以用開發者工具解除。但這個設問所要求的,是從HTML留下的痕跡中讀出實際發生了什麼。圖3的HTML中,收在限制字數以內的投稿被拆成15則並排排列,每一則的開頭與末尾都放著註解符號。這不是「解除」限制的痕跡,而是「繞過」限制的痕跡。應該理解為,這道題考驗的是逐一確認攻擊者所留痕跡的態度。

關於輸入值該驗證到什麼程度、如何驗證的設計思路,在應用程式該如何驗證QR Code的讀取值一文中,也從「不信任外部傳入資料」的設計角度整理過。

6. 腳本做了什麼 ── 設問3(1)

N先生擷取出的腳本約有20行。設問3(1)要求以60字以內回答其中第6行到第20行(第一次通訊成功後才會執行的部分)的處理內容。

解答範例是「帶著從XHR回應取得的權杖,把工作階段 ID 當成頭像圖片上傳」。

以下是本公司為說明此動作而撰寫的等效程式碼(並非直接引用試題冊中的圖示)。

// ① 先取得個人檔案設定頁面
const xhr = new XMLHttpRequest();
xhr.open("get", "https://example.jp/user/profile");
xhr.responseType = "document";   // 把回應當成DOM而不是文字接收
xhr.send();

xhr.onload = function () {
  // ② 依照與正規畫面相同的步驟讀取上傳用權杖
  const token = xhr.response.getElementById("token").value;

  // ③ 把Cookie(含工作階段ID)原封不動當成PNG檔案的內容
  const file = new File([document.cookie], "a.png", { type: "image/png" });

  // ④ 送往自身網站的頭像圖片上傳功能
  const form = new FormData();
  form.append("uploadfile", file);
  form.append("token", token);

  const xhr2 = new XMLHttpRequest();
  xhr2.open("post", "https://example.jp/user/upload");
  xhr2.send(form);
};

前半5行僅僅是為了通過Q公司的CSRF對策而存在

這支腳本值得注意的地方,在於前半段整段都花在取得權杖上

Q公司在上傳頭像圖片時,要求「與 /user/profile 所發放的權杖一致」。這是為了防止別的網站放置偽造表單、擅自進行上傳的機制,也就是CSRF對策。

然而攻擊腳本是在受害者的瀏覽器中、與受害者相同的來源運作的。既然如此,只要照著正規畫面的做法去做即可。用GET取得個人檔案設定頁面,讀取其中名為 token 的元素的值。光是這樣就能拿到權杖。

CSRF對策的權杖無法防止XSS。 因為XSS一旦成立,攻擊者的程式碼就能以「已登入的正規使用者」的身分行動。權杖所守護的是「來自別的來源、擅自送來的請求」,而不是「運作在同一來源中的惡意腳本」。

這個區別,在實務中閱讀安全需求時也很有用。「因為有加入CSRF權杖所以沒問題」這樣的說明,對CSRF來說或許正確,但對XSS卻什麼也沒說明。

File 物件所承載的意義

③這一行也值得留意。它以 document.cookie 的字串為內容,把檔名設為 a.png、MIME類型設為 image/png,做出一個檔案物件。

內容其實只是純文字,並不是PNG檔案。儘管如此上傳仍然成功,正如題目所述,原因是Web應用程式 Q 沒有檢查上傳圖片檔案的格式

document.cookie 之所以能被讀取,是因為Cookie沒有附加HttpOnly屬性。RFC 6265對HttpOnly屬性的規定是:「將Cookie的範圍限定於HTTP請求。特別是在透過會把Cookie公開給腳本的瀏覽器API等『非HTTP』API存取Cookie時,指示使用者代理省略該Cookie」7。如果附加了這個屬性,③這一行抓到的字串就不會含有工作階段 ID,即使被帶走也不具價值。

這裡要留意的是,HttpOnly只會隱藏附加了該屬性的Cookie。如果同一個來源還有沒設HttpOnly的Cookie(例如顯示設定或計測用ID),document.cookie 仍然會回傳這些值。並不是「加上HttpOnly,document.cookie 就會變成空的」。既然要守護的是工作階段 ID,就請個別確認工作階段 ID 的Cookie是否確實附加了這個屬性

7. 不流向外部的資訊竊取 ── 設問3(2)(3)

設問3(2)問的是「攻擊者能以什麼方法取得已上傳的資訊」,以50字以內作答。解答範例是「下載會員的頭像圖片,從中取出工作階段 ID 字串」。

回想一下,題目的規格是這樣寫的。

已上傳的頭像圖片,會顯示在會員個人檔案設定頁面與評論頁面。

也就是說,寫有受害者工作階段 ID 的「頭像圖片」,被放在網站上可以瀏覽到的地方。攻擊者不需要做任何特別的事。只要打開顯示著受害者頭像圖片的頁面,取得該圖片的URL即可。由於內容是純文字,打開後就能讀到工作階段 ID。

補充一點,這種回收方式之所以能順利進行,前提是受害者的頭像圖片出現在攻擊者看得到的頁面上(例如自己也投稿過評論的商品評論頁面等)。由於圖示的URL是依會員固定的,只要曾在某處看到過一次,之後就能直接取得該URL。無論如何,攻擊者一方的操作都只是以正規瀏覽者的身分GET頁面或圖片而已,從網站的角度看不會構成異常通訊。

受害者的瀏覽器Web應用程式Q(example.jp)攻擊者受害者的瀏覽器Web應用程式Q(example.jp)攻擊者準備階段把投稿的字串原封不動地保存(保存本身沒有問題)受害者開啟頁面V★漏洞就在這裡★把已保存的標題未經跳脫就嵌入HTML以script元素的形式執行讀取document.cookie做成a.png的內容未確認格式就保存並公開為頭像圖片回收將評論分成15次投稿(以註解符號跳過HTML)1GET 頁面V2含有攻擊腳本的HTML3GET /user/profile4含有權杖的HTML5POST /user/upload(uploadfile=a.png, token=…)6GET 受害者的頭像圖片7寫有工作階段ID的檔案8

出口對策無法阻止這條路徑

這條路徑的棘手之處,畫成圖就一目了然。受害者的瀏覽器從頭到尾都只與正規網站(example.jp)通訊。

結果,屬於監看通訊那一類的對策全部落空。真正有效的只有最後一項。

對策 對這次攻擊 理由
代理伺服器的URL過濾 無效 存取對象僅限業務上正當的電商網站
防火牆的對外通訊監控 無效 不會發生對陌生網域的通訊
Content Security Policy 的 connect-src 限制('self' 等允許同一來源的設定) 無效 兩次通訊都是送往同一來源,落在政策允許的範圍內
Content Security Policy 的 script-src 限制(不允許 'unsafe-inline' 的配置) 有效 嵌入的內嵌腳本從一開始就不會被執行

一提到XSS造成的資訊外洩,常會聯想到「Cookie被送往攻擊者的伺服器」這種畫面,但這道題清楚顯示了帶走資訊的目的地也可能就是受害者網站本身。只要網站內有一個可寫入、又可讀出的地方,那裡就能成為交接處。檔案上傳功能、公開個人檔案欄位、公開留言欄等都是典型的例子。

補充關於CSP的部分,實務上常用的設定中,能確實阻止這次攻擊的並非 connect-src(限制通訊對象),而是 script-src。只要指定 script-src 'self' 且不允許 'unsafe-inline',嵌入在評論中的內嵌 <script> 從一開始就不會被執行。若只把CSP理解成「用來限縮對外通訊的東西」,就會看漏這個差異。

connect-src 那一邊,原理上也並非完全無力。像 connect-src 'none' 這種連同一來源的通訊都禁止的設定,或是把通訊對象限定在特定端點的許可清單,同樣能阻止這兩次XHR。只不過能收緊到這種程度的網站有限,因此就現實的防備而言,主力還是 script-src。正確的理解不是「同一來源所以不受CSP管轄」,而是「一般允許同一來源的常見設定會讓它通過」。

設問3(3):工作階段 ID 能用來做什麼

設問3(3)問的是取得的資訊能做什麼,以40字以內作答。解答範例是「冒充存取頁面V的會員,使用Web應用程式Q的功能」。

根據評分講評,這個設問的正答率很高。評論中寫道,Cookie被攻擊者取得對電商網站的影響,考生普遍理解得很好。

回頭重讀題目的規格會發現,會員個人檔案功能中有一個登錄信用卡資訊的頁面,並明確標示僅限已登入會員使用。冒充之後能做到什麼,題目從一開始就已經準備好了。

8. 為什麼在攻擊者的網站上不成立 ── 設問4

設問4是這道大題中最本質的一問。

假設攻擊者在自己準備的網域上,架設含有與圖4相同腳本的HTML,Web應用程式Q的已登入會員即使存取該網站,因瀏覽器的機制,攻擊也不會成功。請以40字以內回答這個機制。

解答範例是「腳本無法讓Cookie被送往別的網域URL的機制」。

假設把同一支腳本放在 evil.example 上,讓Q公司的會員踩到。會發生什麼事?

首先,第一次通訊就會落空。evil.example 上的腳本以XMLHttpRequest送往 https://example.jp/user/profile 的請求,屬於跨來源請求。

決定性的一點是不會附上Cookie。圖4的腳本並未指定 withCredentials,因此跨來源請求不會附帶Cookie。從Q公司伺服器的角度看,這是未登入的存取,含有權杖的個人檔案設定頁面不會被回傳。腳本無法取得權杖,攻擊就停在這裡。

還有另一道牆:無法讀取回應。若Q公司沒有透過 Access-Control-Allow-Origin 允許 evil.example,瀏覽器會判定CORS檢查失敗、把這次通訊當成網路錯誤處理,onload 不會觸發,也就無法碰到 xhr.response。一般的網站不會對陌生來源回傳這個標頭,所以實際上這裡也應該會擋下來。

只不過,Q公司實際如何設定CORS,題目中並未寫明。就算假設Q公司允許來自 evil.example 的存取,能讀到的也只是未登入狀態的回應,裡面並不含權杖,攻擊依然無法成立。光是「不附帶Cookie」這一點,攻擊就已經被擋下來了。

兩個條件其中之一就足以讓腳本停下來,但實際上兩者同時在起作用。

再加上,Cookie本身也讀不到。 document.cookie 回傳的是該腳本所運作的來源(evil.example)的Cookie。發給 example.jp 的工作階段 ID 並不在其中。

IPA的解答範例採用前者,也就是「Cookie不會被送往別的網域URL」作為答案。

在此請不要把這兩者當成同一個機制來記憶。它們是各自獨立的機制。

  • document.cookie 不會回傳 example.jp 的Cookie,是因為Cookie依網域各自分離管理。像 evil.exampleexample.jp 這樣屬於不同網站(不同的可註冊網域)之間,這種分離是無法透過設定放寬的。不過同一網域下的子網域之間則另當別論,使用Cookie的 Domain 屬性,可以讓 sub.example.jpexample.jp 之間共享7
  • 跨來源的XHR不附帶Cookie,是因為withCredentials 預設為 false。這一點在條件齊備時是會改變的。若腳本指定 withCredentials = true,且Cookie的 SameSite 屬性允許跨網站傳送,且伺服器回傳明確指出來源的 Access-Control-Allow-Origin 以及 Access-Control-Allow-Credentials: true,Cookie就會被送出,回應也能被讀取

在這裡,請把實際失敗的理由假如把腳本改寫又需要什麼條件分開理解。

從題目與圖4能確定的只有一件事:由於未指定 withCredentials,Cookie不會被附上,因而被視為未登入、無法取得權杖。光是這一點,攻擊就會停下來。

CORS未獲許可,同樣也是讓回應讀不到的一道牆,但Q公司的設定題目並未寫明,所以這一點只能當成「一般網站通常會是這樣」的附加補強來理解。

那麼,如果攻擊者把腳本改成 withCredentials = true 呢?這時才會出現另一個問題。若Cookie的 SameSite 不允許跨網站傳送,即使設了 withCredentials,那個Cookie依然不會被附上。目前的瀏覽器會把未指定 SameSite 的Cookie視為 Lax,因此在預設情況下會偏向不附帶。

另外,Q公司工作階段 ID 的Cookie實際上如何設定 SameSite,題目中並未寫明。因此不能說「因為有 SameSite 才受到保護」。這裡只是想說明,SameSite 對帶著憑證來攻擊的對手是有效的

總結來說,要從別的網域讀取已驗證的回應,必須同時滿足 withCredentials = true、Cookie的 SameSite 允許跨網站傳送、伺服器以支援憑證的CORS允許發送來源這3個條件。網域不同並不代表絕對安全,但也不是只要湊齊一個條件就會被攻破。檢查自家網站時,不該只安心於Cookie依網域分離,而必須實際確認CORS的設定(特別是允許哪些來源送出帶憑證的請求)與Cookie的 SameSite 屬性

所以儲存型XSS的價值才如此之高

這個設問告訴我們的,是攻擊者為何執著於「在受害者網站內執行自己的程式碼」的理由。

即使在自己的網域上準備一個外觀一模一樣的假網站,瀏覽器也會把它視為別的網站。既無法從 document.cookie 讀到對方網站的Cookie,像這個設問中的腳本那樣在不帶憑證的情況下送出XHR,從對方網站看來甚至連登入狀態都不成立。對攻擊者而言,價值就在於程式碼能以受害者網站的來源身分運作這件事本身。儲存型XSS正是實現這一點的手段。

只不過,請不要理解成「別的網域絕對無法送出帶驗證的請求」。如果Cookie的 SameSite 設定允許跨網站傳送(例如 SameSite=None),惡意頁面就確實能夠把帶Cookie的請求送到對方伺服器。表單的POST就是典型例子,CORS所控制的主要是「腳本能否讀取回應」,而不是「請求能否送達伺服器」(不過若附加自訂標頭,或以 application/json 傳送等情況,會先觸發預檢請求,若未獲許可則本次請求本身不會送出)。這正是CSRF這種攻擊得以成立的理由,也正因如此才需要CSRF對策的權杖。

也就是說,別的來源的腳本在預設情況下做不到的是「讀取對方的Cookie」與「讀取回應」,而不是「送出請求」。

而且,這兩種「讀不到」的強度並不相同。document.cookie 的網域分離無法透過伺服器端的設定放寬,但能否讀到回應,則取決於對方伺服器的CORS設定。只要伺服器允許發送來源(若附帶憑證,就要對 Access-Control-Allow-Origin 回傳具體的來源、並同時回傳 Access-Control-Allow-Credentials: true),別的來源的腳本也能讀到回應。這正是應該確認自家網站CORS設定的理由所在。

XSS之所以特別,就在於它能跳過所有這些條件,一開始就以受害者網站的來源身分行動。

換句話說,XSS並不是「畫面上出現怪字元的小毛病」,而是讓攻擊者站上與自家網站正規使用者相同立場的漏洞。這種認知上的差異,會左右優先順序的判斷。

9. 發揮作用的對策,與未發揮作用的對策

到這裡為止,整理成一張表格。這是本文最希望讀者帶走的部分。

Q公司有/沒有的機制 對這次攻擊 理由
HTTPS通訊 未發揮作用 屬於通訊路徑的保護,與嵌入頁面的腳本無關
評論標題50字・內文300字的輸入字數限制 未發揮作用 被拆成15次投稿,投稿之間的HTML被JavaScript的註解跳過
上傳用權杖(CSRF對策) 未發揮作用 同一來源的腳本能以正規步驟取得權杖
需要登入才能使用的上傳功能 未發揮作用 實際執行的是已登入的受害者自己的瀏覽器
輸出時跳脫(沒有做) 有效(根本解決) 投稿的字串不再被當成HTML解讀,腳本從一開始就不會執行
Cookie的HttpOnly屬性(沒有加) 有效(保險性對策) document.cookie 讀不到工作階段 ID
上傳檔案的格式確認・再編碼(沒有做) 有效(保險性對策) 原始文字無法以PNG形式保存,藏在中繼資料裡的字串也會因再編碼而消失。不過若是編碼成圖片像素本身,仍會殘留

上面4項,對這次攻擊完全沒有起到任何作用。把下面3項套進IPA《安全的網站製作方式》的分類,第一項是根本解決,其餘兩項是保險性對策5

補充一點,上面4項並不代表「IPA不承認是對策」。IPA的分類只有根本解決與保險性對策兩種,輸入值的內容檢查其實也被列為保險性對策之一。只不過那裡附有「這項對策的有效性有限」的但書,而這次正好碰上了那個限制。

重要的是,下面3項只要具備任何一項,這道題中所執行的攻擊鏈就會被切斷

  • 若做了跳脫,腳本從一開始就不會執行
  • 若加了HttpOnly,腳本會執行,但讀不到Cookie
  • 若確認了格式,Cookie讀得到,但原始文字無法以PNG形式上傳

多層防禦真正發揮意義的,正是這樣的場合。不過保險性對策無法取代根本解決。即使加了HttpOnly,只要XSS仍然存在,攻擊者依然能在受害者的瀏覽器上執行任意操作(購買商品、變更登錄資訊等)。不需要竊取工作階段 ID 的攻擊,可以設計出無數種。

為什麼不只寫「格式確認」,還要寫到「再編碼」

表格最後一行寫成「格式確認・再編碼」兩段式,是有理由的。

題目中的Q公司完全沒有確認圖片檔案的格式,因此光是加入格式檢查,這次攻擊就會被擋下。不過,這僅限於攻擊者送出原始文字的情況。攻擊者也可以組出一個合法的PNG檔案,把字串藏在其中繼資料區域等處。這種情況下,「格式是否正確」的確認就會被通過。

因此,若要收緊這條路徑,就必須做到在伺服器端對上傳的圖片重新編碼後再配送(不直接保存原始的位元組序列)這一步。

不過,再編碼也無法完全堵死這條路。若攻擊者把工作階段 ID 直接畫成圖片的像素本身、做成一張合法的PNG,一般的再編碼會保留這種視覺資訊,攻擊者依然能下載後讀取出來。再編碼能擋下的,是送出原始位元組序列的變種與藏在中繼資料區域的變種,而不是編碼成圖片內容的資訊。

也就是說,只要網站內存在「可寫入、又可讀出」的地方,就無法把這條外流路徑完全封死。這正好體現了保險性對策的性質。保險性對策光是擋下「目前觀察到的攻擊」還不夠,還必須考量到攻擊者稍微改變手法之後是否依然成立才能設計完善。而無論疊加多少層,都無法取代作為根本解決的輸出時跳脫。

另外,把上傳的檔案從主體以外的另一個網域配送這種設計雖然常被建議,但這並不是針對這條外流路徑的對策。攻擊者從別的網域一樣能照常下載圖片,而且同源政策也不是用來把公開的位元組序列變成秘密的機制。獨立網域配送真正有效的,是防範上傳的檔案本身以主動內容的形式,在受害者網站的來源中被執行這類威脅(例如被上傳HTML或SVG的情況)。請把這視為針對另一種威脅的對策,分開理解。

不依賴傳送端申報的資訊(副檔名或Content-Type),而是依內容本身來判斷檔案,這種思考方式本身不只在Web,也是普遍需要的基本動作。不過「依內容判斷」的具體做法,會因格式而異。像PNG這樣在開頭位元組序列中有固定排列的格式,可以據此確認。而CSV則沒有標準化的簽章,開頭的BOM只能標示文字編碼,並不能證明內容就是CSV。正如CSV檔案的處理方式一文所處理的,CSV的情況需要確認的是能否以預期的方言解析通過,以及是否落在預期的結構與件數上限之內。

10. 檢查自己程式碼時的著眼點

把這道大題套用到程式碼審查的檢查清單,會得到以下項目。只要是擁有會員功能或投稿功能的Web應用程式,就能直接套用。

  1. 輸出時的跳脫,是否在所有輸出位置都有生效。 全面清查明確關閉了範本引擎自動跳脫的地方(raw| safedangerouslySetInnerHTML 等),並逐一確認能否說明關閉的理由
  2. 是否把輸入端的限制當成XSS對策來計算。 字數限制與可輸入字元種類的限制屬於規格上的需求,並非XSS的根本解決
  3. 是否在輸入時就進行跳脫。 輸入端的角色是拒絕規格外數值的驗證,而不是跳脫。輸入的當下尚未決定輸出目的地(HTML、CSV、JSON、郵件、紀錄檔),因此輸入時跳脫會招致雙重跳脫與資料破壞。SQL陳述式的組合也一樣,根本解決是預留位置
  4. Cookie是否附加了 HttpOnlySecureSameSite 工作階段 ID 的Cookie請特別確認
  5. 上傳檔案的格式,是否依內容本身而非副檔名或Content-Type來確認。 傳送端申報的資訊不能作為驗證依據。不過光靠格式檢查,仍無法擋下把資料藏在合法圖片中繼資料區域的手法
  6. 是否對上傳的圖片重新編碼後再配送。 是否原封不動地回傳原始位元組序列。不過再編碼後,編碼成像素的資訊仍會殘留,因此要理解「可寫入又可讀出的地方」無法完全封死。另外,獨立網域配送是針對上傳檔案在自家網站來源中被執行這種威脅的對策,而不是針對這條外流路徑的對策
  7. CSP是否配置成能阻止內嵌腳本執行的形式。 判斷基準不是「有沒有寫 'unsafe-inline'」,而是「內嵌腳本是否實際被允許」。若 script-src 中指定了nonce或hash,CSP Level 2 以後的瀏覽器會忽略 'unsafe-inline',因此在並列寫出 'unsafe-inline' 與nonce、以求向後相容的政策中,沒有nonce的注入腳本依然會被阻擋
  8. 能否說明權杖「守護的是什麼」。 CSRF對策的權杖無法防止XSS,兩者各自需要不同的對策

尤其第1項,是實際調查中經常發現的典型模式。原以為框架自動跳脫就安全了,卻因為「想直接放進HTML」這種個別需求,只在一個地方關閉了自動跳脫,這種情況並不罕見。

結語 ── 這道題真正考驗的能力

到目前為止看過的對策一覽,已經整理在開頭的「先講結論」中。最後,關於這道題本身,再補上一點。

作為一道考題,它做得好的地方,並不在於考「知不知道XSS」,而在於考能不能分辨手上這些對策,哪一個實際發揮了作用

Q公司並非什麼都沒做。以HTTPS通訊、對輸入設有字數限制、上傳要求權杖、功能僅限已登入會員使用。這樣排列起來,看起來已經算是有做過一些對策的架構。即便如此,還是全部被穿透了。反過來看,欠缺的那3項每一項都很不起眼,並不是會寫進發行說明的功能。

安全性的討論之所以困難,是因為對策的數量與防禦的強度並不成正比。能不能一項一項說出「做了什麼」不重要,重要的是能不能說出「那項對策擋住的是哪種攻擊的哪個階段」。這不只是為了應付資格考試的能力,對接受安全性對策說明的一方、以及負責實作的一方來說,都是最需要的判斷力。

相關諮詢領域

合同會社小村軟體承接擁有會員功能或投稿功能之網站的製作,以及既有Web應用程式是否存在同樣漏洞的設計審查。

參考連結

  1. IPA 獨立行政法人資訊處理推進機構, 試題冊・配分比例・解答範例・評分講評(2023年度、令和5年度) 所收「2023年度 秋期 資訊處理安全確保支援士考試 午後 試題」。內容涵蓋Q公司Web應用程式Q的功能(會員登錄、以Cookie發放工作階段ID的登入機制、商品評論功能中評論標題50字・評論內文300字的輸入字數限制、會員個人檔案功能中的頭像圖片上傳與信用卡資訊登錄、頭像圖片上傳以圖片檔與權杖作為參數、已上傳的頭像圖片會顯示在會員個人檔案設定頁面與評論頁面)、頁面V中16則評論卻只顯示2則的現象、頁面V的HTML中含有會員A的15則投稿與一支長腳本、擷取出的腳本內容,以及Web應用程式Q存在讓會員輸入的腳本被執行的漏洞、Cookie未附加HttpOnly屬性、未確認上傳圖片檔格式等內容。設問1至設問4的題目文字同樣出自這份試題冊。  2

  2. IPA 獨立行政法人資訊處理推進機構, 關於考試的常見問答. 關於該機構公開的歷屆考試試題,除法令另有規定的情況外,使用時無須取得許可或支付使用費,但並不代表放棄著作權,須以「年度、期、考試類別、時間段、題號等」的形式明確標註出處(並舉例顯示「出處:平成31年度 春期 基本資訊技術者考試 上午 問1」),以及若對試題內容做了部分改寫,也須一併注明等內容。 

  3. IPA 獨立行政法人資訊處理推進機構, 2023年度 秋期 資訊處理安全確保支援士考試 解答範例. 問1的出題宗旨(以濫用Web應用程式漏洞所導致的資安事件應對為題材,考驗解讀由HTML與ECMAScript遭濫用之漏洞與問題點、並擬定對策的能力),以及各設問的解答範例(設問1(1)為「イ 儲存型XSS」、設問1(2)為「輸出評論標題之前先施加跳脫處理。」、設問2為「分成多次進行投稿,讓HTML被註解掉、連成一整支腳本。」、設問3(1)為「帶著從XHR回應取得的權杖,把工作階段ID當成頭像圖片上傳。」、設問3(2)為「下載會員的頭像圖片,從中取出工作階段ID字串。」、設問3(3)為「冒充存取頁面V的會員,使用Web應用程式Q的功能。」、設問4為「腳本無法讓Cookie被送往別的網域URL的機制」)。  2

  4. IPA 獨立行政法人資訊處理推進機構, 2023年度 秋期 資訊處理安全確保支援士考試 評分講評. 問1整體正答率大致平均,設問1(1)方面「或許是因為腳本中使用了DOM,可以看到不少考生誤答為『DOM Based XSS』」,以及希望考生連漏洞的特徵與對策方法都能正確理解的提醒;設問2方面「有部分解答像『用開發者工具刪除輸入限制後再投稿』這樣,可以看出確認得不夠充分」,以及希望考生培養仔細確認攻擊者所留痕跡、正確掌握攻擊方法之能力的提醒;還有設問3(3)正答率高,顯示考生對電商網站中Cookie被攻擊者取得所造成的影響理解良好等內容。  2

  5. IPA 獨立行政法人資訊處理推進機構, 安全的網站製作方式. 針對11種網站漏洞,把威脅與對策分成「根本解決」(消除漏洞成因本身的實作方式)與「保險性對策」(在漏洞仍然殘留的情況下、用來降低攻擊成功率或損害程度的對策)分別呈現;跨站指令碼攻擊的根本解決中,列出對輸出到網頁的所有元素施加跳脫處理;以及附屬的安全實作檢核表與別冊《安全的SQL呼叫方式》《網站健康檢查規格》等內容。  2 3

  6. IPA 獨立行政法人資訊處理推進機構, 安全的網站製作方式 - 1.1 SQL注入. 把「SQL陳述式的組合全部以預留位置實作」列為SQL注入的根本解決,並指出靜態預留位置(Prepared Statement)在原理上不會產生此漏洞;作為以字串串接組合SQL陳述式時的實作方式,列出「以字串串接組合SQL陳述式時,須使用可進行跳脫等處理的資料庫引擎API,正確組成SQL陳述式的字面值」,亦即跳脫是針對構成SQL陳述式的字面值進行的(而非在接收輸入的時間點);同時列出「不讓傳給Web應用程式的參數中直接指定SQL陳述式」作為根本解決,以及「不把錯誤訊息原封不動顯示在瀏覽器上」「賦予資料庫帳號適當的權限」作為保險性對策等內容。 

  7. IETF, RFC 6265: HTTP State Management Mechanism, 第4.1.2.6節「The HttpOnly Attribute」。關於HttpOnly屬性把Cookie的範圍限定於HTTP請求,特別是在透過會把Cookie公開給腳本的瀏覽器API等「非HTTP」API存取Cookie時,指示使用者代理省略該Cookie。同時提及Secure屬性(第4.1.2.5節)限制Cookie只能在安全通道上傳送。  2

共用相同標籤的最新文章。能以相近的主題延伸理解。

與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。

本文連結到以下服務頁面,歡迎從最接近的入口查看。

網站製作

因為對於擁有會員功能或投稿功能的網站來說,本文所討論的輸出時跳脫、Cookie屬性設定等要點,會直接關係到網站本身的製作品質。

常見問題

整理諮詢這個主題時常見的問題。

這道題的XSS為什麼是「儲存型」?腳本明明操作了DOM,為什麼不是DOM Based XSS?
漏洞的種類取決於攻擊者的字串「在哪裡」變成可執行的程式碼,也就是說取決於脆弱的輸出位置(Sink)是在伺服器端還是瀏覽器端。這道題中,伺服器把攻擊者投稿的評論標題原封不動地嵌入HTML後回傳。Sink位在伺服器的輸出處理中,該字串會被保存下來,此後分發給所有開啟頁面的人,因此屬於儲存型XSS。DOM Based XSS指的是瀏覽器上的JavaScript透過對innerHTML賦值等方式,使某個值變得可執行的類型。嵌入的腳本是否使用了XMLHttpRequest、getElementById等DOM API,與漏洞的種類無關。另外,即使伺服器把已保存的值以無害的文字或JSON形式回傳,只要瀏覽器端的JavaScript把它傳給innerHTML,就會構成DOM Based XSS(這屬於因已保存的值所引發的所謂stored DOM XSS)。請不要以回應中是否看得到該字串來判斷,而要以它是在哪裡變得可執行來判斷。IPA的評分講評也指出,或許是因為腳本中使用了DOM,有不少考生誤答為DOM Based XSS。
評論標題明明有50字的輸入字數限制,為什麼還能執行很長的腳本?
攻擊者把腳本拆成能收在單次投稿長度以內的片段,分成多次投稿。第1則在標題中途開啟script標籤,並在標題末尾開啟JavaScript的區塊註解(斜線加星號)。第2則在開頭先關閉該註解,接著寫下下一步指令,再於末尾重新開啟註解——如此反覆。這樣一來,夾在各則投稿之間的HTML(div標籤等)就會全部落入JavaScript的註解中而被忽略,只有被拆分的片段會串連起來,作為一整支腳本執行。也就是說,輸入字數限制只限制了單則的長度,並沒有限制投稿的次數。
被竊取的工作階段 ID 被送到哪裡去了?
並沒有被送到外部伺服器。腳本把Cookie的內容原封不動地當成檔案內容,檔名設為「a.png」、MIME類型設為「image/png」,送到受害者所使用的電商網站自身的會員頭像圖片上傳功能。由於上傳的頭像圖片會顯示在評論頁面等處,攻擊者只要正常下載該圖片,就能從其內容字串中取出工作階段 ID。受害者的瀏覽器自始至終只與正規網站通訊,不會產生任何對外的可疑通訊,因此這是一條代理伺服器的URL過濾或防火牆的對外通訊監控都無法察覺的路徑。
上傳應該需要權杖才能進行,為什麼攻擊還是成立了?
因為那個權杖是用來擋下來自其他網站的擅自請求的機制(CSRF對策),並不是針對運作在同一網站上的腳本所設計的對策。攻擊腳本先以XMLHttpRequest存取個人檔案設定頁面,像正規畫面一樣取得權杖,接著帶著這個權杖執行上傳。由於XSS的緣故,攻擊者的程式碼是在與受害者相同的來源(origin)中運作,因此正規使用者能做的事,攻擊者的程式碼全都能做。CSRF對策的權杖無法防止XSS。
聽說SQL注入中輸入時的跳脫很重要。難道只有XSS才是在輸出時處理嗎?
不,SQL注入同樣也不是靠「輸入時跳脫」來因應的。IPA提出的根本解決是「SQL陳述式的組合全部以預留位置實作」。雖然也提到在以字串串接組合SQL陳述式時,可以用跳脫處理作為替代方案,但那也是為了「正確組成SQL陳述式的字面值」,也就是在組合SQL陳述式的那一瞬間進行的處理,而不是在接收輸入的當下。共通的原則是:「跳脫應該在資料即將進入哪種文法世界這件事被確定下來的地方進行。」如果要輸出成HTML,就做HTML跳脫;如果要成為SQL陳述式的一部分,就用預留位置;如果要成為shell命令的一部分,就照shell的方式處理。之所以不能在輸入時跳脫,是因為那個時間點輸出目的地尚未確定。同一份資料可能會輸出到HTML頁面,也可能輸出到CSV、JSON、通知郵件或紀錄檔中,如果在輸入時就做了HTML跳脫,CSV中就會原封不動地出現實體參照這類字串,資料庫中的內容也會和原始輸入不一致,導致搜尋與統計出錯。當然,這不代表輸入端什麼都不用做。輸入端該負責的不是跳脫,而是驗證(Validation),也就是拒絕規格上不可能出現的值。
從這道題應該帶回實務工作的對策是什麼?
根本解決,是對包括評論標題在內的所有使用者輸入,都在即將輸出為HTML之前施加跳脫處理。在此基礎上,可以列舉的保險性對策則有:替工作階段 ID 的Cookie加上HttpOnly屬性,使其無法被腳本讀取;在伺服器端把上傳的圖片重新編碼,不保留原始的位元組序列;把Content Security Policy配置成能夠阻止內嵌腳本執行的形式。這道題中的Q公司,只要實作了其中任何一項,這條攻擊鏈就會在某處被切斷。不過保險性對策是有可能被攻擊者改變手法後繞過的,因此無法取代作為根本解決的跳脫處理。

作者檔案

本文作者的個人檔案頁面。

Go Komura

小村軟體有限公司 代表

以 Windows 軟體開發、技術諮詢與故障調查為中心,在難以重現的故障調查與既有資產仍在運作的專案上具有優勢。

回到部落格一覽