2. XSS 跨站腳本攻擊
2-1 什麽是 XSS 跨站腳本攻擊?
我們在實戰應用「Rich Editor 編輯器」有介紹過 XSS 跨站腳本攻擊:如果用戶可以自己張貼內容到網站上,並且網站會顯示出來的話,就有可能有 XSS 漏洞。惡意用戶會張貼 JavaScript 代碼上來,那麽這麽當其他用戶瀏覽到這一頁時,瀏覽器就會盲目的執行 JavaScript 代碼。
讓我們攻擊看看,請用 [email protected]
登入,然後針對一則活動留言,貼上以下 JavaScript 代碼:
<script>
$.ajax({
url: $(location).attr('href') + "/comments",
method: "POST",
data: { comment : { content: "啊哈哈哈~你看看你! (σ゚∀゚)σ゚∀゚)σ゚∀゚)σ" } },
dataType: "JSON"
})
</script>
留言完之後,接下來任何人看到這一頁,就會執行這段 Ajax 代碼,也跟著留言一次。請多重新整理頁面幾次看看。
你也可以測試看看其他人瀏覽會怎麽樣?你可以用另一個瀏覽器(例如 Safari),或是用 Chrome 的 Incognito Windows(無痕模式,這會開啟一個乾凈沒有 Cookie 的新視窗),然後改用 [email protected]
登入,然後瀏覽剛剛那一個活動…
admin
也會自動張貼留言了….
2-2 如何防禦 XSS?
要防禦說起來簡單,就是顯示每個用戶貼文的地方都要脫逸(或叫做轉義) HTML,打開 app/views/events/show.html.erb
- <%= raw comment.content %>
+ <%= comment.content %>
Rails 預設就會脫逸(轉義) HTML 了,用 raw comment.content
或 comment.content.html_safe
反而是告訴 Rails 不要脫逸 HTML。拿掉 raw
之後,就會脫逸顯示出原代碼,例如 <
會變成 <
。
不過這樣會完全防止任何 HTML 標籤,例如我們張貼以下這張圖片:
<img src="http://placehold.it/200x200" />
一樣被脫逸了 :(
如果我們還是希望用戶可以輸入 HTML,就像在實戰應用「Rich Editor 編輯器」的需求,請改用 sanitize 白名單過濾,只允許部分的 HTML 標籤:
- <%= comment.content %>
+ <%= sanitize comment.content %>
2-3 另一個不小心的漏洞
請點主選單的修改個人資料,這個地方會不會有漏洞呢?
回到任一個活動頁面:
發現竟然有漏洞,這段 JavaScript 被執行了。
讓我們找找看漏洞出現在哪裡,原來是在留言顯示用戶名稱的地方,沒有做好脫逸 :(
<div class="panel-heading">
<%= user_avatar_link(comment.author) %>
</div>
require 'digest/md5'
module UsersHelper
def user_avatar_link(user)
# https://cn.gravatar.com/
email_md5 = Digest::MD5.hexdigest(user.email)
gravatar_url = "https://www.gravatar.com/avatar/#{email_md5}"
str = "<div class ='user-link'>" + link_to(image_tag(gravatar_url), user_path(user)) + " " + user.display_name + "</div>"
str.html_safe
end
end
這個 user_avatar_link
helper 的目的是輸出用戶的 Gravater 照片的連結,以及用戶的顯示名稱,而問題就出在 str.html_safe
太過放縱了。
但是如果你把 str.html_safe
這行拿掉,你會發現整個 Helper 的輸出都被脫逸了 :( 不是我們想要的結果。
這是因為 Rails 會追蹤每個字串是否是安全的,如果不安全的話,輸出時會自動脫逸。不安全的字串 +
安全的字串,會變成不安全的。由於字串 "<div class ='user-link'>"
預設是不安全的,因此後來累加字串進來,整個 str
都是不安全的,所以 Rails 最後會讓整個 str
都脫逸,造成圖片 img 標籤也無法顯示。
2-4 解法辦法
解法辦法一
小心翼翼地在不需要脫逸的地方加上 .html_safe
require 'digest/md5'
module UsersHelper
def user_avatar_link(user)
# https://cn.gravatar.com/
email_md5 = Digest::MD5.hexdigest(user.email)
gravatar_url = "https://www.gravatar.com/avatar/#{email_md5}"
- str = "<div class ='user-link'>" + link_to(image_tag(gravatar_url), user_path(user)) + " " + user.display_name + "</div>"
- str.html_safe
+ "<div class ='user-link'>".html_safe + link_to(image_tag(gravatar_url), user_path(user)) + " " + user.display_name + "</div>".html_safe
end
end
解法辦法二
可以用 Rails 內建的 content_tag
helper 產生標籤,結果一樣,只是寫起來會比字串相加好看一點:
require 'digest/md5'
module UsersHelper
def user_avatar_link(user)
# https://cn.gravatar.com/
email_md5 = Digest::MD5.hexdigest(user.email)
gravatar_url = "https://www.gravatar.com/avatar/#{email_md5}"
- "<div class ='user-link'>".html_safe + link_to(image_tag(gravatar_url), user_path(user)) + " " + user.display_name + "</div>".html_safe
+ content_tag(:div,
link_to(image_tag(gravatar_url), user_path(user)) + tag("") + user.display_name ,
:class => "user-link" )
end
end
2-5 小結
由於 Rails 預設會脫逸 HTML 的關系,所以 Rails 對 XSS 是有了基本的防禦。如果不是 Rails 這種的框架幫你預設脫逸的話,網頁上只要顯示用戶的輸出,都有可能是個安全漏洞。
不過即使有 Rails 的保護,你也有可能在字串相加的過程中,不小心太過放縱而造成漏洞,就像這一節所示範的。這時候就必須小心處理那些是安全的字串、那些是用戶輸入的不安全字串。