def vulnerable() {
def books = Book.find("from Book as b where b.title ='" + params.title + "'")
}
16 安全性
版本 6.2.0
16 安全性
Grails 的安全性並不會比 Java Servlet 高或低。然而,Java servlet(以及 Grails)的安全性極高,且由於 Java 虛擬機器作為程式碼基礎,因此幾乎不會受到常見的緩衝區溢位和格式錯誤的 URL 攻擊。
Web 安全性問題通常發生在開發人員的經驗不足或錯誤,而 Grails 能做的就是避免常見錯誤,並讓撰寫安全應用程式變得更簡單。
Grails 自動執行的動作
Grails 預設具有一些內建的安全機制。
-
透過 GORM 領域物件進行的所有標準資料庫存取都會自動進行 SQL 轉譯,以防範 SQL 注入攻擊
-
預設的 鷹架 範本會在顯示時對所有資料欄位進行 HTML 轉譯
-
Grails 連結建立標籤 (link、form、createLink、createLinkTo 等) 都會使用適當的轉譯機制來防範程式碼注入
-
Grails 提供 編解碼器,讓您在以 HTML、JavaScript 和 URL 呈現資料時輕易地跳脫資料,以防止注入攻擊。
16.1 防禦攻擊
SQL 注入
Hibernate 是 GORM 領域類別背後的技術,它在提交至資料庫時會自動跳脫資料,因此這不是問題。然而,仍然有可能撰寫使用未檢查請求參數的錯誤動態 HQL 程式碼。例如,執行下列動作容易受到 HQL 注入攻擊
或使用 GString 進行類似的呼叫
def vulnerable() {
def books = Book.find("from Book as b where b.title ='${params.title}'")
}
不要這麼做。改用命名或位置參數來傳遞參數
def safe() {
def books = Book.find("from Book as b where b.title = ?",
[params.title])
}
或
def safe() {
def books = Book.find("from Book as b where b.title = :title",
[title: params.title])
}
網路釣魚
這實際上是公關問題,在於避免劫持您的品牌,以及與客戶之間的既定溝通政策。客戶需要知道如何辨識有效的電子郵件。
XSS - 跨網站指令碼注入
您的應用程式驗證盡可能多的內容非常重要,以確保傳入的請求來自您的應用程式,而不是來自其他網站。確保所有呈現到檢視的資料值都正確跳脫也很重要。例如,在呈現到 HTML 或 XHTML 時,您必須確保人員無法惡意地將 JavaScript 或其他 HTML 注入到其他人檢視的資料或標籤中。
Grails 2.3 以上版本包含特殊支援,用於自動編碼放入 GSP 頁面的資料。請參閱 跨網站指令碼 (XSS) 防護 文件,以取得更多資訊。
您也必須避免使用請求參數或資料欄位來決定要將使用者重新導向到哪個 URL。例如,如果您使用 successURL
參數來決定在成功登入後將使用者重新導向到哪裡,攻擊者可以使用您自己的網站模仿您的登入程序,然後在登入後將使用者重新導向回他們自己的網站,這可能會讓 JavaScript 程式碼利用該網站上已登入的帳戶。
跨網站請求偽造
CSRF 涉及從網站信任的使用者傳輸未經授權的指令。典型的範例是另一個網站嵌入一個連結,如果使用者仍經過驗證,就會在您的網站上執行動作。
降低這類攻擊風險的最佳方式是在您的表單上使用 useToken
屬性。請參閱 處理重複表單提交,以取得更多有關如何使用它的資訊。額外的措施是不使用記住我 cookie。
HTML/URL 注入
這是提供錯誤資料的地方,當稍後用於在頁面中建立連結時,按一下它不會造成預期的行為,而且可能會重新導向到另一個網站或變更請求參數。
HTML/URL 注入很容易使用 Grails 提供的 編解碼器 來處理,而且 Grails 提供的標籤函式庫在適當的地方都使用 encodeAsURL。如果您建立自己的標籤來產生 URL,您也需要留意這麼做。
阻斷服務
負載平衡器和其他設備在此處更有可能派上用場,但也有與過多查詢相關的問題,例如攻擊者建立連結以設定結果集的最大值,因此查詢可能會超過伺服器的記憶體限制或使系統變慢。此處的解決方案是在將請求參數傳遞給動態尋找器或其他 GORM 查詢方法之前,務必先清除請求參數
int limit = 100
def safeMax = Math.min(params.max?.toInteger() ?: limit, limit) // limit to 100 results
return Book.list(max:safeMax)
可猜測的 ID
許多應用程式使用 URL 的最後一部分作為從 GORM 或其他地方擷取的某個物件的「id」。特別是在 GORM 的情況下,這些很容易猜測,因為它們通常是連續整數。
因此,您必須聲明請求使用者有權查看具有請求 id 的物件,才能將回應傳回給使用者。
不這樣做就是「安全取決於隱藏」,這必定會被破解,就像使用「letmein」等預設密碼一樣。
您必須假設每個未受保護的 URL 都可以透過某種方式公開存取。
16.2 防範跨網站指令碼 (XSS)
跨網站指令碼 (XSS) 攻擊是網路應用程式的常見攻擊媒介。它們通常涉及在表單中提交 HTML 或 Javascript 程式碼,使得當該程式碼顯示時,瀏覽器會執行一些惡意行為。它可能像彈出警示方塊一樣簡單,也可能更糟,例如可以存取其他使用者的工作階段 Cookie。
解決方案是在頁面中顯示時,清除所有不可信的使用者輸入。例如,
<script>alert('Got ya!');</script>
將變成
<script>alert('Got ya!');</script>
在呈現時,消除惡意輸入的影響。
預設情況下,Grails 會採取安全措施,並清除 GSP 中 ${}
表達式中的所有內容。所有標準 GSP 標籤在預設情況下也是安全的,會清除任何相關的屬性值。
那麼,當您要停止 Grails 清除某些內容時會發生什麼事?將 HTML 放入資料庫並按原樣呈現,只要該內容是可信的,就有有效的用例。在這種情況下,您可以告訴 Grails 內容是安全的,應該以原始方式呈現,也就是說,不清除任何內容
<section>${raw(page.content)}</section>
您在此處看到的 raw()
方法可從控制器、標籤庫和 GSP 頁面取得。
預防 XSS 很困難,需要開發人員投入大量注意力
儘管 Grails 在預設情況下會採取安全措施,但這並不能保證您的應用程式不會受到 XSS 類型的攻擊。此類攻擊成功的機率會比其他情況低,但開發人員應始終意識到潛在的攻擊媒介,並嘗試在測試期間找出應用程式的漏洞。切換到不安全的預設值也很容易,從而增加引進漏洞的風險。 |
OWASP - XSS 防護規則 和 OWASP - 類型的跨網站指令碼 中有關於 XSS 的更多詳細資訊。XSS 的類型有:儲存的 XSS、反射的 XSS 和 基於 DOM 的 XSS。基於 DOM 的 XSS 防護 因為 Javascript 客戶端側範本和單頁應用程式越來越普及,所以變得越來越重要。
Grails 編解碼器主要用於防範儲存的和反射的 XSS 類型的攻擊。Grails 2.4 包含 HTMLJS 編解碼器,有助於防範一些基於 DOM 的 XSS 攻擊。
很難做出一個對所有人都有效的解決方案,因此 Grails 在微調跳脫運作方式方面提供了很大的彈性,讓您在關閉預設跳脫或變更用於頁面、標籤、頁面片段等的編解碼器時,可以讓大部分的應用程式保持安全。
組態
建議您檢閱新建立的 Grails 應用程式的組態,以了解 Grails 中 XSS 防護運作的方式。
當您使用 HttpOnly 標記標記 cookie 時,它會告訴瀏覽器這個特定的 cookie 只能由伺服器存取。嚴格禁止從客戶端指令碼存取 cookie。這可以在 application.yml
組態檔中組態,如下所示
server:
session:
cookie:
domain: example.org
http-only: true
path: /
secure: true
GSP 具備自動 HTML 編碼 GSP 表達式的功能,而從 Grails 2.3 開始,這是預設組態。新建立的 Grails 應用程式的預設組態(在 application.yml
中找到)如下所示
grails:
views:
gsp:
encoding: UTF-8
htmlcodec: xml # use xml escaping instead of HTML4 escaping
codecs:
expression: html # escapes values inside ${}
scriptlets: html # escapes output from scriptlets in GSPs
taglib: none # escapes output from taglibs
staticparts: none # escapes output from static template parts
GSP 具備幾個在將頁面寫入回應時使用的編解碼器。編解碼器在 codecs
區塊中組態,並說明如下
-
expression
- 表達式編解碼器用於編碼在 ${..} 表達式中找到的任何程式碼。新建立的應用程式的預設值是html
編碼。 -
scriptlet
- 用於 GSP scriptlet (<% %>、<%= %> 區塊) 的輸出。新建立的應用程式的預設值是html
編碼 -
taglib
- 用於編碼 GSP 標籤庫的輸出。新應用程式的預設值是none
,因為通常由標籤作者負責定義給定標籤的編碼,而透過指定none
,Grails 仍然與較舊的標籤庫向後相容。 -
staticparts
- 用於編碼 GSP 頁面輸出的原始標記。預設值是none
。
雙重編碼防護
Grails 2.3 之前的版本包含將預設編碼設定為 html
的功能,不過啟用此設定有時會因為編碼套用兩次(一次由 html
編碼,然後如果外掛手動呼叫 encodeAsHTML
,則再次編碼)而導致使用現有外掛時出現問題。
Grails 2.3 包含雙重編碼防護,因此當評估表達式時,如果資料已編碼,則不會編碼(範例 ${foo.encodeAsHTML()}
)。
原始輸出
如果您 100% 確定要顯示在頁面上的值並非從使用者輸入接收,而且您不希望編碼該值,則可以使用 raw
方法
${raw(book.title)}
標籤庫、控制器和 GSP 頁面中都有提供 'raw' 方法。
依外掛編碼
Grails 也具備依外掛控制所使用編碼的功能。例如,如果您已安裝名為 foo
的外掛,則將下列組態放入 application.groovy
中,將只停用 foo
外掛的編碼
foo.grails.views.gsp.codecs.expression = "none"
依頁面編碼
您也可以使用頁面指令,依頁面控制用來呈現 GSP 頁面的各種編碼
<%@page expressionCodec="none" %>
依標籤庫編碼
每個建立的標籤庫都有機會使用 "defaultEncodeAs" 屬性指定用來編碼標籤庫輸出的預設編碼
static defaultEncodeAs = 'html'
也可以使用 "encodeAsForTags" 依標籤指定編碼
static encodeAsForTags = [tagName: 'raw']
情境敏感編碼切換
某些標籤需要某些編碼,而 Grails 具備使用 "withCodec" 方法僅啟用標籤執行特定部分的編碼的功能。例如,考慮 "<g:javascript>"" 標籤,它允許您將 JavaScript 程式碼嵌入頁面中。此標籤需要 JavaScript 編碼,而不是 HTML 編碼,才能執行標籤主體(但不需要輸出標記)。
out.println '<script type="text/javascript">'
withCodec("JavaScript") {
out << body()
}
out.println()
out.println '</script>'
強制標籤編碼
如果標籤指定與您的需求不同的預設編碼,您可以透過傳遞選用的 'encodeAs' 屬性,強制任何標籤的編碼
<g:message code="foo.bar" encodeAs="JavaScript" />
所有輸出的預設編碼
新應用程式的預設組態對於大多數使用案例來說都很理想,而且與現有外掛和標籤庫向下相容。不過,您也可以透過將 Grails 組態為在回應結束時始終編碼所有輸出,讓應用程式更安全。這可透過在 application.groovy
中使用 filteringCodecForContentType
組態來完成
grails.views.gsp.filteringCodecForContentType.'text/html' = 'html'
請注意,如果已啟用,staticparts
編解碼器通常需要設定為 raw
,以避免對靜態標記進行編碼
codecs {
expression = 'html' // escapes values inside ${}
scriptlet = 'html' // escapes output from scriptlets in GSPs
taglib = 'none' // escapes output from taglibs
staticparts = 'raw' // escapes output from static template parts
}
16.3 編碼和解碼物件
Grails 支援動態編碼/解碼方法的概念。Grails 會隨附一組標準編解碼器。Grails 也支援一種簡單的機制,讓開發人員可以提供自己的編解碼器,並在執行階段識別這些編解碼器。
編解碼器類別
Grails 編解碼器類別可能包含編碼封閉區塊、解碼封閉區塊,或同時包含這兩者。當 Grails 應用程式啟動時,Grails 架構會從 grails-app/utils/
目錄動態載入編解碼器。
架構會在 grails-app/utils/
中尋找以慣例 Codec
結尾的類別名稱。例如,Grails 隨附的標準編解碼器之一為 HTMLCodec
。
如果編解碼器包含 encode
封閉區塊,Grails 會建立一個動態 encode
方法,並將該方法新增至 Object
類別,其名稱會代表定義編碼封閉區塊的編解碼器。例如,HTMLCodec
類別定義了一個 encode
封閉區塊,因此 Grails 會將其附加名稱 encodeAsHTML
。
HTMLCodec
和 URLCodec
類別也定義了一個 decode
封閉區塊,因此 Grails 會將其附加名稱 decodeHTML
和 decodeURL
。可以在 Grails 應用程式的任何位置呼叫動態編解碼器方法。例如,假設一個報表包含一個名為「description」的屬性,該屬性可能包含必須轉譯才能在 HTML 文件中顯示的特殊字元。處理 GSP 中此問題的一種方式是使用動態編碼方法編碼 description 屬性,如下所示
${report.description.encodeAsHTML()}
使用 value.decodeHTML()
語法執行解碼。
靜態編譯程式碼的編碼器和解碼器介面
使用編解碼器的首選方式是使用 codecLookup bean 取得 Encoder
和 Decoder
執行個體。
package org.grails.encoder;
public interface CodecLookup {
public Encoder lookupEncoder(String codecName);
public Decoder lookupDecoder(String codecName);
}
使用 CodecLookup
和 Encoder
介面的範例
import org.grails.encoder.CodecLookup
class CustomTagLib {
CodecLookup codecLookup
def myTag = { Map attrs, body ->
out << codecLookup.lookupEncoder('HTML').encode(attrs.something)
}
}
標準編解碼器
HTMLCodec
此編解碼器執行 HTML 轉譯和反轉譯,讓值可以在 HTML 頁面中安全地呈現,而不會建立任何 HTML 標籤或損壞頁面配置。例如,給定一個值「你不知道 2 > 1 嗎?」,你無法在 HTML 頁面中安全地顯示這個值,因為 > 看起來像是在關閉標籤,特別是在你將此資料呈現於屬性中時,例如輸入欄位的 value 屬性。
使用範例
<input name="comment.message" value="${comment.message.encodeAsHTML()}"/>
請注意,HTML 編碼不會重新編碼撇號/單引號,因此你必須在屬性值中使用雙引號,以避免帶有撇號的文字影響你的頁面。 |
HTMLCodec 預設為 HTML4 風格轉譯(Grails 2.3.0 之前版本的舊版 HTMLCodec 實作),會轉譯非 ASCII 字元。
你可以透過在 application.groovy
中設定此組態屬性來使用純 XML 轉譯,而不是 HTML4 轉譯。
grails.views.gsp.htmlcodec = 'xml'
XMLCodec
此編解碼器執行 XML 轉譯和反轉譯。它會轉譯 &、<、>、"、'、\\\\、@、`、不換行空白 (\\\\u00a0)、行分隔符號 (\\\\u2028) 和段落分隔符號 (\\\\u2029)。
HTMLJSCodec
此編解碼器執行 HTML 和 JS 編碼。它用於防止某些 DOM-XSS 漏洞。請參閱 OWASP - 基於 DOM 的 XSS 防範秘笈,以取得防止基於 DOM 的 XSS 攻擊的準則。
URLCodec
在連結或表單動作中建立 URL,或任何用於建立 URL 的資料時,都需要進行 URL 編碼。它可以防止非法字元進入 URL 並改變其意義,例如「Apple & Blackberry」在 GET 要求中無法作為參數正常運作,因為連字元會中斷參數剖析。
使用範例
<a href="/mycontroller/find?searchKey=${lastSearch.encodeAsURL()}">
Repeat last search
</a>
Base64Codec
執行 Base64 編碼/解碼功能。使用範例
Your registration code is: ${user.registrationCode.encodeAsBase64()}
JavaScriptCodec
轉譯字串,讓它們可以用作有效的 JavaScript 字串。例如
Element.update('${elementId}',
'${render(template: "/common/message").encodeAsJavaScript()}')
HexCodec
將位元組陣列或整數清單編碼為小寫十六進位字串,並可以將十六進位字串解碼為位元組陣列。例如
Selected colour: #${[255,127,255].encodeAsHex()}
MD5Codec
使用 MD5 演算法來摘要位元組陣列或整數清單,或字串的位元組(使用預設系統編碼),作為小寫十六進位字串。使用範例
Your API Key: ${user.uniqueID.encodeAsMD5()}
MD5BytesCodec
使用 MD5 演算法來摘要位元組陣列或整數清單,或字串的位元組(使用預設系統編碼),作為位元組陣列。使用範例
byte[] passwordHash = params.password.encodeAsMD5Bytes()
SHA1Codec
使用 SHA1 演算法來摘要位元組陣列或整數清單,或字串的位元組 (使用預設系統編碼),作為小寫十六進位字串。使用範例
Your API Key: ${user.uniqueID.encodeAsSHA1()}
SHA1BytesCodec
使用 SHA1 演算法來摘要位元組陣列或整數清單,或字串的位元組 (使用預設系統編碼),作為位元組陣列。使用範例
byte[] passwordHash = params.password.encodeAsSHA1Bytes()
SHA256Codec
使用 SHA256 演算法來摘要位元組陣列或整數清單,或字串的位元組 (使用預設系統編碼),作為小寫十六進位字串。使用範例
Your API Key: ${user.uniqueID.encodeAsSHA256()}
SHA256BytesCodec
使用 SHA256 演算法來摘要位元組陣列或整數清單,或字串的位元組 (使用預設系統編碼),作為位元組陣列。使用範例
byte[] passwordHash = params.password.encodeAsSHA256Bytes()
自訂 Codec
應用程式可以定義自己的 Codec,而 Grails 會將它們與標準 Codec 一起載入。自訂 Codec 類別必須定義在 grails-app/utils/
目錄中,且類別名稱必須以 Codec
結尾。Codec 可能包含 static
encode
閉包、static
decode
閉包,或同時包含這兩個閉包。閉包必須接受單一引數,而該引數會是動態方法所呼叫的物件。例如
class PigLatinCodec {
static encode = { str ->
// convert the string to pig latin and return the result
}
}
在放置好上述 Codec 後,應用程式可以執行類似下列動作
${lastName.encodeAsPigLatin()}
16.4 驗證
Grails 沒有預設的驗證機制,因為驗證可以透過許多不同的方式來實作。不過,使用 攔截器 來實作簡單的驗證機制很簡單。這對於簡單的使用案例來說就足夠了,但強烈建議使用已建立的安全性架構,例如使用 Spring Security 或 Shiro 外掛程式。
攔截器讓您可以在所有控制器或 URI 空間中套用驗證。例如,您可以透過執行下列動作,在一個稱為 grails-app/controllers/SecurityInterceptor.groovy
的類別中建立一組新的篩選器
grails create-interceptor security
並在那裡實作您的攔截邏輯
class SecurityInterceptor {
SecurityInterceptor() {
matchAll()
.except(controller:'user', action:'login')
}
boolean before() {
if (!session.user && actionName != "login") {
redirect(controller: "user", action: "login")
return false
}
return true
}
}
在此,攔截器在執行所有動作之前攔截執行,但 login
除外,且如果工作階段中沒有使用者,則重新導向至 login
動作。
login
動作本身也很簡單
def login() {
if (request.get) {
return // render the login view
}
def u = User.findByLogin(params.login)
if (u) {
if (u.password == params.password) {
session.user = u
redirect(action: "home")
}
else {
render(view: "login", model: [message: "Password incorrect"])
}
}
else {
render(view: "login", model: [message: "User not found"])
}
}
16.5 安全性外掛程式
如果您需要進階功能,例如授權、角色等,而這些功能超出簡單驗證的範圍,則您應該考慮使用 spring security core 外掛程式。
16.5.1 Spring Security
Spring Security 外掛程式建構在 Spring Security 專案上,該專案提供一個彈性、可擴充的架構,用於建構各種驗證和授權架構。這些外掛程式是模組化的,因此您可以只安裝應用程式需要的功能。Spring Security 外掛程式是 Grails 的官方安全性外掛程式,且持續受到維護和支援。
有一個 Spring Security Core 外掛程式,它支援基於表單的驗證、加密/加鹽的密碼、HTTP 基本驗證等,而次要的相依外掛程式提供替代功能,例如 ACL 支援、使用 Jasig CAS 的單一登入、LDAP 驗證、Kerberos 驗證,以及提供 使用者介面擴充功能 和安全性工作流程的外掛程式。
請參閱 Spring Security Core 外掛程式頁面以取得基本資訊,以及 使用者指南 以取得詳細資訊。