在上一章中,我们研究了如何在应用程序工作时存储它生成的信息,以及如何向套件中添加单元测试,以确保应用程序的行为符合我们的预期,并在不符合预期时诊断错误。
在那一章中,我们没有向我们的博客应用程序添加很多功能;现在让我们回到这一点。我们还将把本章中的一些日志记录和测试功能扩展到我们的新特性中。
到目前为止,我们一直在开发一个 web 应用程序的框架,该框架实现博客数据和用户提交的评论的一些基本输入和输出。与任何公共网络服务器一样,我们的服务器也会受到各种攻击。
这些都不是独一无二的,但我们拥有一个工具库,可以用来实施最佳实践并扩展服务器和应用程序以缓解常见问题。
在构建可公开访问的网络应用程序时,常见攻击向量的一个快速简便的参考指南是开放式 Web 应用程序安全项目(OWASP),该项目提供了安全问题出现的最关键领域的定期更新列表。OWASP 可在找到 https://www.owasp.org/ 。其十大项目汇集了 10 个最常见和/或关键的网络安全问题。虽然它不是一个全面的列表,并且有一个习惯在更新之间注明日期,但在编译潜在向量时,它仍然是一个良好的开端。
不幸的是,这些年来一些最普遍的媒介一直存在;尽管事实上,安全专家一直在屋顶上大喊他们的严厉。一些人在网络上的曝光率迅速下降(如注入),但他们仍然倾向于停留更长的时间,一年又一年,甚至随着遗留应用程序的逐步淘汰。
以下是 2013 年末最新十大漏洞中的四个,我们将在本章中介绍其中一些漏洞:
- 注入:任何不受信任的数据有机会被处理而不逃逸,从而允许数据操纵或访问数据或系统的情况,通常其不会公开。最常见的是 SQL 注入。
- 认证中断:这是由于加密算法不好,密码要求弱,会话劫持是可行的。
- XSS:跨站点脚本允许攻击者通过在另一个站点上注入和执行脚本来访问敏感信息。
- Po.T0.跨站点请求伪造:TysT1:不同于 Ty2 T2。XSS,这允许攻击向量源于另一个站点,但它欺骗用户在另一个站点上完成某些动作。
虽然其他攻击向量的范围从与我们的用例相关到不相关,但值得评估我们没有涵盖的攻击向量,看看还有哪些地方可能会被利用。
接下来,我们将研究使用 Go 在应用程序中实现和强制使用 HTTPS 的最佳方法。
在第 5 章、前端与 RESTful API 的集成中,我们研究了创建自签名证书和在我们的应用程序中使用 HTTPS/TLS。但是,让我们快速回顾一下,为什么这不仅关系到我们的应用程序的整体安全,而且关系到整个 Web 的整体安全。
首先,简单 HTTP 通常不会对流量进行加密,特别是对重要的请求头值,如 cookie 和查询参数。我们在这里一般说,因为 RFC2817 确实指定了一个通过 HTTP 协议使用 TLS 的系统,但它几乎没有使用。最重要的是,它不会向用户提供注册站点安全所需的明显反馈。
第二,同样,HTTP 流量随后容易受到中间人攻击。
另一个副作用是:谷歌(或许还有其他搜索引擎)开始青睐 HTTPS 流量,而不是不太安全的流量。
直到最近,HTTPS 才主要被归入电子商务应用程序,但利用 HTTP 缺陷的可用和普遍攻击的增加,如侧劫持和中间人攻击,开始将 Web 的大部分内容推向 HTTPS。
您可能听说过由此产生的移动和[T0]座右铭[T1]HTTPS Everywhere[T2],这些座右铭也渗透到浏览器插件中,迫使站点使用为任何给定站点实现最安全的可用协议。
为了扩展第 6 章、会话和 Cookie中的工作,我们可以做的最简单的事情之一就是要求所有流量通过 HTTP 流量重新路由通过 HTTPS。还有其他方法可以做到这一点,我们将在本章末尾看到,但它可以相当简单地完成。
首先,我们将实现一个goroutine
,分别使用tls.ListenAndServe
和http.ListenAndServe
同时服务我们的 HTTPS 和 HTTP 流量:
var wg sync.WaitGroup
wg.Add(1)
go func() {
http.ListenAndServe(PORT, http.HandlerFunc(redirectNonSecure))
wg.Done()
}()
wg.Add(1)
go func() {
http.ListenAndServeTLS(SECUREPORT, "cert.pem", "key.pem", routes)
wg.Done()
}()
wg.Wait()
这假设我们将SECUREPORT
常数设置为":443"
,就像我们将PORT
设置为":8080"
一样,或者您选择的任何值。没有任何东西可以阻止您使用另一个 HTTPS 端口;这里的好处是,浏览器默认情况下会将https://
请求定向到端口443
,就像它将 HTTP 请求定向到端口80
一样,有时还会回退到端口8080
。请记住,在许多情况下,您需要以 sudo 或管理员身份运行,才能使用低于[T8]的端口启动。
在前面的示例中,您会注意到我们正在使用一个单独的 HTTP 流量处理程序,名为redirectNonSecure
。这实现了一个非常基本的目的,您将在这里看到:
func redirectNonSecure(w http.ResponseWriter, r *http.Request) {
log.Println("Non-secure request initiated, redirecting.")
redirectURL := "https://" + serverName + r.RequestURI
http.Redirect(w, r, redirectURL, http.StatusMovedPermanently)
}
这里,serverName
是显式设置的。
从请求中收集域名或服务器名可能存在一些问题,因此如果可以,最好明确设置。
这里要添加的另一个非常有用的部分是HTTP 严格传输安全(HSTS),当与合规消费者结合时,这种方法希望减轻协议降级攻击(例如强制/重定向到 HTTP)。
这只不过是一个 HTTPS 头,当使用它时,它将自动处理并强制https://
请求,否则这些请求将使用不太安全的协议。
OWASP 建议对此标头进行以下设置:
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
请注意,此标头在 HTTP 上被忽略。
尽管注入仍然是当今网络上最大的攻击载体之一,但大多数语言都有简单而优雅的方法来防止或在很大程度上降低使用准备好的语句和经过消毒的输入保留易受攻击的 SQL 注入的可能性。
但是,即使使用提供这些服务的语言,仍然有机会留下可供利用的领域。
任何软件开发的核心原则之一,无论是在 Web 上、服务器上还是独立的可执行文件上,都是永远不要信任从外部(有时是内部)源获取的输入数据。
这一原则适用于任何语言,尽管有些语言通过预先准备好的查询或抽象(如对象关系映射(ORM)使与数据库的接口更安全和/或更容易。
从本质上讲,Go 没有任何 ORM,而且从技术上讲,甚至没有 O(对象)(Go 不是纯粹面向对象的),因此很难复制面向对象语言在这方面的很多功能。
然而,有许多第三方库试图通过接口和结构强制 ORM,但其中很多都可以很容易地手工编写,因为您可能比任何库都更了解模式和数据结构,即使是抽象意义上的。
然而,对于 SQL,Go 对于几乎所有支持 SQL 的数据库都有一个健壮且一致的接口。
为了说明 SQL 注入漏洞如何在 Go 应用程序中简单地出现,我们将比较原始 SQL 查询和准备好的语句。
从数据库中选择页面时,我们使用以下查询:
err := database.QueryRow("SELECT page_title,page_content,page_date FROM pages WHERE page_guid="+requestGUID, pageGUID).Scan(&thisPage.Title, &thisPage.Content, &thisPage.Date)
这向我们展示了如何通过接受未初始化的用户输入来打开应用程序以防止注入漏洞。在本例中,任何请求类似
/page/foo;delete from pages
理论上可能会很快清空您的pages
桌子。
我们在路由器级别进行了一些初步消毒,在这方面确实有所帮助。由于我们的 mux 路由仅包括字母数字字符,因此我们可以避免将一些需要转义的字符路由到我们的ServePage
或APIPage
处理程序:
routes.HandleFunc("/page/{guid:[0-9a-zA\\-]+}", ServePage)
routes.HandleFunc("/api/page/{id:[\\w\\d\\-]+}", APIPage).
Methods("GET").
Schemes("https")
不过,这并不是解决这个问题的万无一失的方法。前面的查询获取原始输入并将其附加到 SQL 查询中,但我们可以使用 Go 中的参数化、准备好的查询更好地处理此问题。以下是我们最终使用的内容:
err := database.QueryRow("SELECT page_title,page_content,page_date FROM pages WHERE page_guid=?", pageGUID).Scan(&thisPage.Title, &thisPage.Content, &thisPage.Date)
if err != nil {
http.Error(w, http.StatusText(404), http.StatusNotFound)
log.Println("Couldn't get page!")
return
}
这种方法在 Go 的任何查询接口中都可用,该接口使用?
代替变量值进行查询:
res, err := db.Exec("INSERT INTO table SET field=?, field2=?", value1, value2)
rows, err := db.Query("SELECT * FROM table WHERE field2=?",value2)
statement, err := db.Prepare("SELECT * FROM table WHERE field2=?",value2)
row, err := db.QueryRow("SELECT * FROM table WHERE field=?",value1)
虽然所有这些在 SQL 世界中实现的目的略有不同,但它们都以相同的方式实现了准备好的查询。
我们简要介绍了跨站点脚本编写,并将其限制为一个向量,以使您的应用程序对所有用户都更安全,从而避免一些坏苹果的行为。问题的关键是一个用户能够添加危险的内容,这些内容将显示给用户,而无需删除使其变得危险的方面。
最终,您可以选择在数据进入时对其进行清理,或者在向其他用户显示数据时对其进行清理。
换句话说,如果有人生成一块包含script
标记的注释文本,您必须小心防止该文本被其他用户的浏览器呈现。您可以选择保存原始 HTML,然后在输出渲染上剥离所有或仅敏感标记。或者,您可以在输入时对其进行编码。
没有正确的答案;然而,您可能会发现遵循前一种方法的价值,即接受任何内容并清理输出。
这两种方法都有风险,但如果您选择改变方法,这种方法允许您保持消息的原始意图。缺点当然是,您可能会意外地允许一些原始数据在未初始化的情况下通过:
template.HTMLEscapeString(string)
template.JSEscapeString(inputData)
第一个函数将获取数据并删除 HTML 的格式,以生成用户输入的消息的纯文本版本。
第二个函数将执行类似的操作,只是针对特定于 JavaScript 的值。您可以使用类似以下示例的快速脚本非常轻松地测试这些[T0]:
package main
import (
"fmt"
"github.com/gorilla/mux"
"html/template"
"net/http"
)
func HTMLHandler(w http.ResponseWriter, r *http.Request) {
input := r.URL.Query().Get("input")
fmt.Fprintln(w, input)
}
func HTMLHandlerSafe(w http.ResponseWriter, r *http.Request) {
input := r.URL.Query().Get("input")
input = template.HTMLEscapeString(input)
fmt.Fprintln(w, input)
}
func JSHandler(w http.ResponseWriter, r *http.Request) {
input := r.URL.Query().Get("input")
fmt.Fprintln(w, input)
}
func JSHandlerSafe(w http.ResponseWriter, r *http.Request) {
input := r.URL.Query().Get("input")
input = template.JSEscapeString(input)
fmt.Fprintln(w, input)
}
func main() {
router := mux.NewRouter()
router.HandleFunc("/html", HTMLHandler)
router.HandleFunc("/js", JSHandler)
router.HandleFunc("/html_safe", HTMLHandlerSafe)
router.HandleFunc("/js_safe", JSHandlerSafe)
http.ListenAndServe(":8080", router)
}
如果我们从不安全的端点请求,我们将取回数据:
将此与/html_safe
进行比较,后者会自动转义输入,您可以在其中看到经过消毒的内容:
所有这些都不是万无一失的,但是如果您选择在用户提交输入数据时获取输入数据,那么您将需要寻找在结果显示上传递该信息的方法,而无需向 XSS 打开其他用户。
虽然在本书中我们不会深入讨论 CSRF,但总的要点是,恶意参与者可以使用一系列方法来欺骗用户在另一个站点上执行不希望的操作。
因为它在方法上至少与 XSS 有着切分的关系,所以现在值得讨论一下。
最大的体现在形式上;可以将其视为一种允许您发送推文的 Twitter 表单。如果第三方在未经用户同意的情况下代表用户强制请求,请考虑类似的情况:
<h1>Post to our guestbook (and not twitter, we swear!)</h1>
<form action="https://www.twitter.com/tweet" method="POST">
<input type="text" placeholder="Your Name" />
<textarea placeholder="Your Message"></textarea>
<input type="hidden" name="tweet_message" value="Make sure to check out this awesome, malicious site and post on their guestbook" />
<input type="submit" value="Post ONLY to our guestbook" />
</form>
在没有任何保护的情况下,任何发布到此留言簿的人都会在不经意间帮助将垃圾邮件传播到此攻击。
显然,Twitter 是一个成熟的应用程序,很久以前就已经处理了这个问题,但是您已经了解了总体思路。您可能认为限制推荐人可以解决此问题,但这也可能是欺骗。
最短的解决方案是为表单提交生成安全令牌,这会阻止其他站点构造有效的请求。
当然,我们的老朋友 Gorilla 也在这方面提供了一些有用的工具。最相关的是csrf
包,其中包括为请求生成令牌的工具,以及在违反或忽略时将生成403
的预烘焙表单字段。
生成令牌的最简单方法是将其作为接口的一部分,您的处理程序将使用该接口生成模板,就像我们的ApplicationAuthenticate()
处理程序:
Authorize.TemplateTag = csrf.TemplateField(r)
t.ExecuteTemplate(w, "signup_form.tmpl", Authorize)
此时,我们需要在模板中公开[T0]。为了验证,我们需要将其链接到我们的ListenAndServe
呼叫:
http.ListenAndServe(PORT, csrf.Protect([]byte("32-byte-long-auth-key"))(r))
我们之前研究的攻击向量之一是会话劫持,我们在 HTTP 与 HTTPS 的上下文中讨论了会话劫持,以及其他人在网站上查看对身份至关重要的信息类型的方式。
对于许多使用会话作为最终 ID 的非 HTTPS 应用程序来说,在公共网络上查找这些数据非常简单。事实上,一些大型应用程序允许在 URL 中传递会话 ID
在我们的应用程序中,我们使用了 Gorilla 的securecookie
包,它不依赖 HTTPS,因为 cookie 值本身是使用 HMAC 哈希进行编码和验证的。
生成密钥本身非常简单,如我们的应用程序和securecookie
文档所示:
var hashKey = []byte("secret hash key")
var blockKey = []byte("secret-er block key")
var secureKey = securecookie.New(hashKey, blockKey)
有关 Gorillasecurecookie
软件包的更多信息请参见:http://www.gorillatoolkit.org/pkg/securecookie
目前,我们的应用程序服务器具有 HTTPS 优先和安全 cookie,这意味着我们可能会对 cookie 本身中的数据存储和识别更有信心。我们的大多数创建/更新/删除操作都发生在 API 级别,它仍然实现会话检查以确保我们的用户经过身份验证。
对于快速实施本章中提到的一些安全修复(和其他修复)来说,其中一个更有用的包是 Cory Jacobsen 提供的名为secure
的包。
Secure 提供了大量有用的实用程序,例如 SSLRedirects(正如我们在本章中实现的)、allowed Hosts、HSTS 选项和 X-Frame-options 速记,用于防止站点加载到框架中。
这些内容涵盖了我们在本章中讨论的一些主题,在很大程度上是最佳实践。作为中间件的一部分,安全是一种简单的方法,可以一下子快速涵盖其中一些最佳实践。
要获取secure
,只需访问github.com/unrolled/secure即可。
虽然本章并不是对 web 安全问题和解决方案的全面回顾,但我们希望解决 OWASP 和其他方面提出的一些最大和最常见的问题。
在本章中,我们介绍或回顾了防止这些问题蔓延到应用程序中的最佳实践。
在第 10 章、缓存、代理和改进的性能中,我们将研究如何在保持性能和速度的同时,通过增加流量来扩展应用程序。