(快速参考)

16 安全性

版本 6.2.0

16 安全性

Grails 并不比 Java Servlet 安全或不安全。但是,由于 Java 虚拟机作为代码底层支持,Java servlet(以及 Grails)极其安全,而且很大程度上可以避免常见的缓冲区覆盖和格式错误的 URL 攻击。

Web 安全问题通常是由于开发者的天真或错误而发生的,而 Grails 只能做一点点来避免常见的错误,并使编写安全的应用程序变得更容易。

Grails 自动执行的操作

默认情况下,Grails 有一些内置的安全机制。

  • 通过 GORM 域对象实现的所有标准数据库访问都会自动进行 SQL 转义,以防止 SQL 注入攻击

  • 默认的 脚手架 模板会在显示时转义所有数据字段的 HTML 代码

  • Grails 链接创建标记 (linkformcreateLinkcreateLinkTo 等) 全部使用适当的转义机制来防止代码注入

  • Grails 提供 编解码器,使你能够在将数据呈现为 HTML、JavaScript 和 URL 时对其进行微不足道的转义,从而防止此处的注入攻击。

16.1 抵御攻击

SQL 注入

Hibernate,GORM 域类的底层技术,在向数据库提交时自动转义数据,因此这不是问题。但仍然可能编写不当的动态 HQL 代码,其中使用未经检查的请求参数。例如,执行以下操作容易受到 HQL 注入攻击

def vulnerable() {
    def books = Book.find("from Book as b where b.title ='" + params.title + "'")
}

或使用 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 注入

当恶意数据用于在页面中创建链接时,就是这种情况下,单击它不会引发预期的行为,并且它可能会重定向到其他网站或更改请求参数。

Grails 提供的 codecs 和 Grails 提供的标签库都可以轻松处理 HTML/URL 注入,并且所有 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) 攻击是针对 Web 应用程序的常见攻击媒介。它们通常涉及在表单中提交 HTML 或 JavaScript 代码,使得在显示该代码时,浏览器会执行一些有害操作。这可能简单到只是弹出一个警报框,也可能严重得多,比如有人可能访问其他用户的会话 cookie。

解决方案是在页面中显示不受信任的用户输入时对其进行转义。例如,

<script>alert('Got ya!');</script>

将变为

&lt;script&gt;alert('Got ya!');&lt;/script&gt;

在呈现时,这将消除恶意输入的影响。

默认情况下,Grails 会安全处理,并转义 GSP 中 ${} 表达式中的全部内容。所有标准 GSP 标记默认情况下也是安全的,会转义所有相关的属性值。

那么,当您希望阻止 Grails 转义某些内容时会发生什么?将 HTML 放入数据库并按原样呈现是有合理用例的,只要该内容是受信任的即可。在这样的情况下,您可以告知 Grails 该内容是安全可按原样呈现的,即不经过任何转义

<section>${raw(page.content)}</section>

在此处看到的 raw() 方法适用于控制器、标记库和 GSP 页面。

XSS 防护很难,并且需要大量开发者注意

尽管 Grails 默认安全处理,但这并不能保证您的应用程序不会受到 XSS 样式攻击。这样的攻击成功的可能性会低于其他情况,但开发者始终应该意识到潜在的攻击媒介,并尝试在测试过程中发现应用程序中的漏洞。切换到不安全的默认设置也很简单,从而增加了引入漏洞的风险。

关于 XSS 的详细信息,请参见 OWASP - XSS 防范规则OWASP - 跨站脚本攻击类型。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 具有自动对 GSP 表达式进行 HTML 编码的功能,并且从 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

双重编码预防

2.3 之前的 Grails 版本包含设置默认编解码器为 html 的能力,然而,启用此设置有时被证明在使用现有插件时由于编码被应用了两次(一次是 html 编解码器,然后如果插件手动调用 encodeAsHTML 则再次进行)而导致出现问题。

Grails 2.3 包含双重编码预防,这样当求值一个表达式时,如果数据已编码,它将不会编码(示例为 ${foo.encodeAsHTML()})。

原始输出

如果您 100% 确定您希望在页面上展示的值尚未从用户输入接收,且您不希望编码此值,那么您可使用 raw 方法。

${raw(book.title)}

“raw” 方法在标记库、控制器和 GSP 页面中均可用。

每个插件编码

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 还支持开发者为 runtime 中识别自己编解码器的简单机制。

编解码器类

Grails编解码器类是可能包含encode闭包、decode闭包或两者。当Grails应用启动时,Grails框架从grails-app/utils/目录动态加载编解码器。

框架在grails-app/utils/中根据以传统Codec结尾的类名查找。例如,与Grails一起发布的标准编解码器之一是HTMLCodec

如果编解码器包含一个encode闭包,Grails将会创建一个动态encode方法,并以表示定义encode闭包的编解码器的名称添加该方法到Object类。例如,HTMLCodec类定义了一个encode闭包,因此,Grails以encodeAsHTML名称附加它。

HTMLCodecURLCodec类也定义一个decode闭包,因此,Grails分别以decodeHTMLdecodeURL名称附加它们。动态编解码器方法可以从Grails应用的任何地方调用。例如,考虑一个案例,其中包含一个名为'description'的属性的报告可能包含必须转义才能显示在HTML文档中的特殊字符。处理GSP中的方法之一是使用动态encode方法对description属性进行编码,如下所示

${report.description.encodeAsHTML()}

使用value.decodeHTML()语法执行解码。

用于静态编译代码的编码器和解码器接口

使用编解码器的一种首选方法是使用codecLookup bean来掌握 EncoderDecoder 实例。

package org.grails.encoder;

public interface CodecLookup {
    public Encoder lookupEncoder(String codecName);
    public Decoder lookupDecoder(String codecName);
}

使用CodecLookupEncoder接口的示例

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标签或损坏页面布局。例如,对于一个值为“Don‘t you know that 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()

自定义编解码器

应用程序可以定义自己的编解码器的 Grails,然后 Grails 会将它们与标准编解码器一起加载。必须在 grails-app/utils/ 目录中定义一个自定义编解码器类,并且类名称必须以 Codec 结尾。此编解码器可包含 static encode 闭包、static decode 闭包或两者兼有。该闭包必须接受单个参数,该参数将是调用动态方法的对象。例如

class PigLatinCodec {
  static encode = { str ->
    // convert the string to pig latin and return the result
  }
}

如果使用了上述编解码器,则应用程序可以执行类似这样的操作

${lastName.encodeAsPigLatin()}

16.4 认证

Grails 没有默认的身份验证机制,这是因为身份验证有许多不同的方法。但是,使用 拦截器 实现一个简单的身份验证机制非常简单。对于简单的用例,这已足够,但极建议使用已建立的安全框架,例如使用 Spring SecurityShiro 插件。

拦截器允许你在所有控制器或 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 插件,它支持基于表单的身份验证、加密/salted 密码、HTTP 基本身份验证等,而次要依赖插件提供备用功能,例如 ACL 支持与 Jasig CAS 进行单点登录LDAP 身份验证Kerberos 身份验证 以及提供 用户界面扩展 和安全工作流的插件。

请参阅 Spring Security Core 插件页面以获取基本信息,以及 用户指南 以获取详细信息。