在 Go 语言中增强 Cookie 的安全性

本文涉及的产品
密钥管理服务KMS,1000个密钥,100个凭据,1个月
简介: 本文讲的是在 Go 语言中增强 Cookie 的安全性,在我开始学习 Go 语言时已经有一些 Web 开发经验了,但是并没有直接操作 Cookie 的经验。我之前做过 Rails 开发,当我不得不需要在 Rails 中读写 Cookie 时,并不需要自己去实现各种安全措施。
本文讲的是在 Go 语言中增强 Cookie 的安全性,

在我开始学习 Go 语言时已经有一些 Web 开发经验了,但是并没有直接操作 Cookie 的经验。我之前做过 Rails 开发,当我不得不需要在 Rails 中读写 Cookie 时,并不需要自己去实现各种安全措施。

瞧瞧,Rails 默认就自己完成了大多数的事情。你不需要设置任何 CSRF 策略,也无需特别去加密你的 Cookie。在新版的 Rails 中,这些事情都是它默认帮你完成的。

而使用 Go 语言开发则完全不同。在 Golang 的默认设置中,这些事都不会帮你完成。因此,当你想要开始使用 Cookie 时,了解各种安全措施、为什么要使用这些措施、以及如何将这些安全措施集成到你的应用中是非常重要的事。希望本文能帮助你做到这一点。

注意:我并不想引起关于 Go 与 Reils 两者哪种更好的论战。两者各有优点,但在本文中我希望能着重讨论 Cookie 的防护,而不是去争论 Rails 和 Go 哪个好。

什么是 Cookie?

在进入 Cookie 防护相关的内容前,我们必须要理解 Cookie 究竟是什么。从本质上说,Cookie 就是存储在终端用户计算机中的键值对。因此,使用 Go 创建一个 Cookie 需要做的事就是创建一个包含键名、键值的 http.Cookie 类型字段,然后调用 http.SetCookie 函数通知终端用户的浏览器设置该 Cookie。

写成代码之后,它看起来类似于这样:

func someHandler(w http.ResponseWriter, r *http.Request) {
  c := http.Cookie{
    Name: "theme",
    Value: "dark",
  }
  http.SetCookie(w, &c)
}

http.SetCookie 函数并不会返回错误,但它可能会静默地移除无效的 Cookie,因此使用它并不是什么美好的经历。但它既然这么设计了,就请你在使用这个函数的时候一定要牢记它的特性。

虽然这好像是在代码中“设定”了一个 Cookie,但其实我们只是在我们返回 Response 时发送了一个 "Set-Cookie" 的 Header,从而定义需要设置的 Cookie。我们不会在服务器上存储 Cookie,而是依靠终端用户的计算机创建与存储 Cookie。

我要强调上面这一点,因为它存在非常严重的安全隐患:我们不能控制这些数据,而终端用户的计算机(以及用户)才能控制这些数据。

当读取与写入终端用户控制的数据时,我们都需要十分谨慎地对数据进行处理。恶意用户可以删除 Cookie、修改存储在 Cookie 中的数据,甚至我们可能会遇到中间人攻击,即当用户向服务器发送数据时,另有人试图窃取 Cookie。

Cookie 的潜在安全问题

根据我的经验,Cookie 相关的安全性问题大致分为以下五大类。下面我们先简单地看一看,本文的剩余部分将详细讨论每个分类的细节问题与解决对策。

1. Cookie 窃取 - 攻击者会通过各种方式来试图窃取 Cookie。我们将讨论如何防范、规避这些方式,但是归根结底我们并不能完全阻止设备上的物理类接触。

2. Cookie 篡改 - Cookie 中存储的数据可以被用户有意或无意地修改。我们将讨论如何验证存储在 Cookie 中的数据确实是我们写入的合法数据

3. 数据泄露 - Cookie 存储在终端用户的计算机上,因此我们需要清楚地意识到什么数据是能存储在 Cookie 中的,什么数据是不能存储在 Cookie 中的,以防其发生数据泄露。

4. 跨站脚本攻击(XSS) - 虽然这条与 Cookie 没有直接关系,但是 XSS 攻击在攻击者能获取 Cookie 时危害更大。我们应该考虑在非必须的时候限制脚本访问 Cookie。

5. 跨站请求伪造(CSRF) - 这种攻击常常是由于使用 Cookie 存储用户登录会话造成的。因此我们将讨论在这种情景下如何防范这种攻击。

如我前面所说,在下文中我们将分别解决这些问题,让你最终能够专业地将你的 Cookie 装进保险柜。

Cookie 窃取

Cookie 窃取攻击就和它字面意思一样 —— 某人窃取了正常用户的 Cookie,然后一般用来将自己伪装成那个正常用户。

Cookie 通常是被以下方式中的某种窃取:

  1. 中间人攻击,或者是类似的其它攻击方式,归纳一下就是攻击者拦截你的 Web 请求,从中窃取 Cookie。
  2. 取得硬件的访问权限。

阻止中间人攻击的终极方式就是当你的网站使用 Cookie 时,使用 SSL。使用 SSL 时,由于中间人无法对数据进行解密,因此外人基本上没可能在请求的中途获取 Cookie。

可能你会觉得“哈哈,中间人攻击不太可能…”,我建议你看看 firesheep,这个简单的工具,它足以说明在使用公共 wifi 时窃取未加密的 Cookie 是一件很轻松的事情。

如果你想确保这种事情不发生在你的用户中,请使用 SSL!试试使用 Caddy Server 进行加密吧。它经过简单的配置就能投入生产环境中。例如,你可以使用下面四行代码轻松让你的 Go 应用使用代理:

calhoun.io {
  gzip
  proxy / localhost:3000
}

然后 Caddy 会为你自动处理所有与 SSL 有关的事务。

防范通过访问硬件来窃取 Cookie 是十分棘手的事情。我们不能强制我们的用户使用高安全性系统,也不能逼他们为电脑设置密码,所以总会有他人坐在电脑前偷走 Cookie 的风险。此外,Cookie 也可能被病毒窃取,比如用户打开了某些钓鱼邮件时就会出现这种情况。

不过这些都容易被发现。例如,如果有人偷了你的手表,当你发现表不在手上时你立马就会注意到它被偷了。然而 Cookie 还可以被复制,这样任何人都不会意识到它已经丢了。

虽然不是万无一失,但你还是可以用一些技术来猜测 Cookie 是否被盗了。例如,你可以追踪用户的登录设备,要求他们重新输入密码。你还可以跟踪用户的 IP 地址,当其在可疑地点登录时通知用户。

所有的这些解决方案都需要后端做更多的工作来追踪数据,如果你的应用需要处理一些敏感信息、金钱,或者它的收益可观的话,请在安全方面投入更多精力。

也就是说,对于大多数只是作为过渡版本的应用来说,使用 SSL 就足够了。

Cookie 篡改(也叫用户伪造数据)

请直面这种情况 —— 可能有一些混蛋突然就想看看你设的 Cookie,然后修改它的值。也可能他是出于好奇才这么做的,但是还是请你为这种可能发生的情况做好准备。

在一些情景中,我们对此并不在意。例如,我们给用户定义一种主题设置时,并不会关心用户是否改变了这个设置。当这个 Cookie 过期时,就会恢复默认的主题设置,并且如果用户设置其为另一个有效的主题时我们可以让他正常使用那个主题,这并不会对系统造成任何损失。

但是在另一些情况下,我们需要格外小心。编辑会话 Cookie 冒充另一个用户产生的危害比改个主题大得多。我们绝不想看到张三假装自己是李四。

我们将介绍两种策略来检测与防止 Cookie 被篡改。

1. 对数据进行数字签名

对数据进行数字签名,即对数据增加一个“签名”,这样能让你校验数据的可靠性。这种方法并不需要对终端用户的数据进行加密或隐藏,只要对 Cookie 增加必要的签名数据,我们就能检测到用户是否修改数据。

这种保护 Cookie 的方法原理是哈希编码 —— 我们对数据进行哈希编码,接着将数据与它的哈希编码同时存入 Cookie 中。当用户发送 Cookie 给我们时,再对数据进行哈希计算,验证此时的哈希值与原始哈希值是否匹配。

我们当然不会想看到用户也创建一个新的哈希来欺骗我们,因此你可以使用一些类似 HMAC 的哈希算法来使用秘钥对数据进行哈希编码。这样就能防范用户同时编辑数据与数字签名(即哈希值)。

JSON Web Tokens(JWT) 默认内置了数字签名功能,因此你可能对这种方法比较熟悉。

在 Go 中,可以使用类似 Gorilla 的 securecookie 之类的 package,你可以在创建SecureCookie 时使用它来保护你的 Cookie。

// 推荐使用 32 字节或 64 字节的 hashKey
// 此处为了简洁故设为了 “very-secret”
var hashKey = []byte("very-secret")
var s = securecookie.New(hashKey, nil)

func SetCookieHandler(w http.ResponseWriter, r *http.Request) {
  encoded, err := s.Encode("cookie-name", "cookie-value")
  if err == nil {
    cookie := &http.Cookie{
      Name:  "cookie-name",
      Value: encoded,
      Path:  "/",
    }
    http.SetCookie(w, cookie)
    fmt.Fprintln(w, encoded)
  }
}

然后你可以在另一个处理 Cookie 的函数中同样使用 SecureCookie 对象来读取 Cookie。

func ReadCookieHandler(w http.ResponseWriter, r *http.Request) {
  if cookie, err := r.Cookie("cookie-name"); err == nil {
    var value string
    if err = s.Decode("cookie-name", cookie.Value, &value); err == nil {
      fmt.Fprintln(w, value)
    }
  }
}

以上样例来源于 www.gorillatoolkit.org/pkg/securec….

注意:这儿的数据并不是进行了加密,而只是进行了编码。我们会在“数据泄露”一章讨论如何对数据进行加密。

这种模式还需要注意的是,如果你使用这种方式进行身份验证,请遵循 JWT 的模式,将登录过期日期和用户数据同时进行签名。你不能只凭 Cookie 的过期日期来判断登录是否有效,因为存储在 Cookie 上的日期并未经过签名,且用户可以创建一个永不过期的新 Cookie,将原 Cookie 的内容复制进去就得到了一个永远处于登录状态的 Cookie。

2. 进行数据混淆

还有一种解决方案可以隐藏数据并防止用户造假。例如,不要这样存储 Cookie:

// 别这么做
http.Cookie{
  Name: "user_id",
  Value: "123",
}

我们可以存储一个值来映射存在数据库中的真实数据。通常使用 Session ID 或者 remember token 来作为这个值。例如我们有一个名为 remember_tokens 的表,这样存储数据:

remember_token: LAKJFD098afj0jasdf08jad08AJFs9aj2ASfd1
user_id: 123

在 Cookie 中,我们仅存储这个 remember token。如果用户想伪造 Cookie 也会无从下手。它看上去就是一堆乱码。

之后当用户要登陆我们的应用时,再根据 remember token 在数据库中查询,确定用户具体的登录状态。

为了让此措施正常工作,你需要确保你的混淆值有以下特性:

  • 能映射到用户数据(或其它资源)
  • 随机
  • 熵值高
  • 可被无效化(例如在数据库中删除、修改 token 值)

这种方法也有一个缺点,就是在用户访问每个需要校验权限的页面时都得进行数据库查询。不过这个缺点很少有人注意,而且可以通过缓存等技术来减小数据库查询的开销。这种方法的升级版就是 JWT,应用这种方法你可以随时使会话无效化。

注意:尽管目前 JWT 收到了大多数 JS 框架的追捧,但上文这种方法是我了解的最常用的身份验证策略。

数据泄露

在真正出现数据泄露前,通常需要另一种攻击向量 —— 例如 Cookie 窃取。然而还是很难去正确地判断并提防数据泄露的发生。因为仅仅是 Cookie 发生了泄露并不意味着攻击者也得到了用户的账户密码。

无论何时,都应当减少存储在 Cookie 中的敏感数据。绝不要将用户密码之类的东西存在 Cookie 中,即使密码已经经过了编码也不要这么做。这篇文章 给出了几个开发者无意间将敏感数据存储在 Cookie 或 JWT 中的实例,由于(JWT 的 payload)是 base64 编码,没有经过任何加密,因此任何人都可以对其进行解码。

出现数据泄露可是犯了大错。如果你担心你不小心存储了一些敏感数据,我建议你使用如 Gorilla 的 securecookie 之类的 package。

前面我们讨论了如何对你的 Cookie 进行数字签名,其实 securecookie 也可以用于加密与解密你的 Cookie 数据,让你的数据不能被轻易地解码并读取。

使用这个 package 进行加密,你只需要在创建 SecureCookie 实例时传入一个“块秘钥”(blockKey)即可。

var hashKey = []byte("very-secret")
// 增加这一部分进行加密
var blockKey = []byte("a-lot-secret")
var s = securecookie.New(hashKey, blockKey)

其它所有东西都和前面章节的数字签名中的样例一致。

再次提醒,你不应该在 Cookie 中存储任何敏感数据,尤其不能存储密码之类的东西。加密仅仅是一项为数据增加一部分安全性,使其成为”半敏感数据“数据的技术而已。

跨站脚本攻击(XSS)

跨站脚本(Cross-site scripting)也经常被记为 XSS,及有人试图将一些不是你写的 JavaScript 代码注入你的网站中。但由于其攻击的机理,你无法知道正在浏览器中运行的 JavaScript 代码到底是不是你的服务器提供的代码。

无论何时,你都应该尽量去阻止 XSS 攻击。在本文中我们不会深入探讨这种攻击的具体细节,但是以防万一我建议你在非必要的情况下禁止 JavaScript 访问 Cookie 的权限。在你需要这个权限的时候你可以随时开启它,所以不要让它成为你的网站安全性脆弱的理由。

在 Go 中完成这点很简单,只需要在创建 Cookie 时设置 HttpOnly 字段为 true 即可。

cookie := http.Cookie{
  // true 表示脚本无权限,只允许 http request 使用 Cookie。
  // 这与 Http 与 Https 无关。
  HttpOnly: true,
}

CSRF(跨站请求伪造)

CSRF 发生的情况为某个用户访问别人的站点,但那个站点有一个能提交到你的 web 应用的表单。由于终端用户提交表单时的操作不经由脚本,因此浏览器会将此请求设为用户进行的操作,将 Cookie 附上表单数据同时发送。

乍一看似乎这没什么问题,但是如果外部网站发送一些用户不希望发送的数据时会发生什么呢?例如,badsite.com 中有个表单,会提交请求将你的 100 美元转到他们的账户中,而 chase.com 希望你在它这儿登录你的银行账户。这可能会导致在终端用户不知情的情况下钱被转走。

Cookie 不会直接导致这样的问题,不过如果你使用 Cookie 作为身份验证的依据,那你需要使用 Gorilla 的 csrf 之类的 package 来避免 CSRF 攻击。

这个 package 将会提供一个 CSRF token,插入你网站的每个表单中,当表单中不含 token 时,csrf package 中间件将会阻止表单的提交,使得别的网站不能欺骗用户在他们那儿向你的网站提交表单。

更多关于 CSRF 攻击的资料请参阅:

在非必要时限制 Cookie 的访问权限

我们要讨论的最后一件事与特定的攻击无关,更像是一种指导原则。我建议在使用 Cookie 时尽量限制其权限,仅在你需要时开发相关权限。

前面讨论 XSS 时我也简单的提到过这点,但一般的观点是你需要尽可能限制对 Cookie 的访问。例如,如果你的 Web 应用没有使用子域名,那你就不应该赋予 Cookie 所有子域的权限。不过这是 Cookie 的默认值,因此其实你什么都不用做就能将 Cookie 的权限限制在某个特定域中。

但是,如果你需要与子域共享 Cookie,你可以这么做:

c := Cookie{
  // 根据主机模式的默认设置,Cookie 进行的是精确域名匹配。
  // 因此请仅在需要的时候开启子域名权限!
  // 下面的代码可以让 Cookie 在 yoursite.com 的任何子域下工作:
  Domain: "yoursite.com",
}

欲了解更多有关域的信息,请参阅 tools.ietf.org/html/rfc626…。你也可以在这儿阅读源码,参阅其默认设置:golang.org/src/net/htt….

你可以参阅 这个 stackoverflow 的问题 了解更多信息,弄明白为什么在为子域使用 Cookie 时不需要提供子域前缀.此外 Go 源码链接中也可以看到如果你提供前缀名的话会被自动去除。

除了将 Cookie 的权限限制在特定域上之外,你还可以将 Cookie 限制于某个特定的目录路径中。

c := Cookie{
  // Defaults 设置为可访问应用的任何路径,但你也可以
  // 进行如下设置将其限制在特定子目录下:
  Path: "/app/",
}

还有你也可以对其设置路径前缀,例如 /blah/,你可以参阅下面这篇文章了解更多这个字段的使用方法:tools.ietf.org/html/rfc626….

为什么我不使用 JWT?

就知道肯定会有人提出这个问题,下面让我简单解释一下。

可能有很多人和你说过,Cookie 的安全性与 JWT 一样。但实际上,Cookie 与 JWT 解决的并不是相同的问题。比如 JWT 可以存储在 Cookie 中,这和将其放在 Header 中的实际效果是一样的。

另外,Cookie 可用于无需验证的数据,在这种情况下了解如何增加 Cookie 的安全性也是必要的。





原文发布时间为:2017年9月2日

本文来自云栖社区合作伙伴掘金,了解相关信息可以关注掘金网站。
目录
相关文章
|
22天前
|
运维 监控 算法
监控局域网其他电脑:Go 语言迪杰斯特拉算法的高效应用
在信息化时代,监控局域网成为网络管理与安全防护的关键需求。本文探讨了迪杰斯特拉(Dijkstra)算法在监控局域网中的应用,通过计算最短路径优化数据传输和故障检测。文中提供了使用Go语言实现的代码例程,展示了如何高效地进行网络监控,确保局域网的稳定运行和数据安全。迪杰斯特拉算法能减少传输延迟和带宽消耗,及时发现并处理网络故障,适用于复杂网络环境下的管理和维护。
|
23天前
|
编译器 Go
揭秘 Go 语言中空结构体的强大用法
Go 语言中的空结构体 `struct{}` 不包含任何字段,不占用内存空间。它在实际编程中有多种典型用法:1) 结合 map 实现集合(set)类型;2) 与 channel 搭配用于信号通知;3) 申请超大容量的 Slice 和 Array 以节省内存;4) 作为接口实现时明确表示不关注值。此外,需要注意的是,空结构体作为字段时可能会因内存对齐原因占用额外空间。建议将空结构体放在外层结构体的第一个字段以优化内存使用。
|
1月前
|
存储 Go
Go 语言入门指南:切片
Golang中的切片(Slice)是基于数组的动态序列,支持变长操作。它由指针、长度和容量三部分组成,底层引用一个连续的数组片段。切片提供灵活的增减元素功能,语法形式为`[]T`,其中T为元素类型。相比固定长度的数组,切片更常用,允许动态调整大小,并且多个切片可以共享同一底层数组。通过内置的`make`函数可创建指定长度和容量的切片。需要注意的是,切片不能直接比较,只能与`nil`比较,且空切片的长度为0。
Go 语言入门指南:切片
|
1月前
|
算法 安全 Go
公司局域网管理系统里的 Go 语言 Bloom Filter 算法,太值得深挖了
本文探讨了如何利用 Go 语言中的 Bloom Filter 算法提升公司局域网管理系统的性能。Bloom Filter 是一种高效的空间节省型数据结构,适用于快速判断元素是否存在于集合中。文中通过具体代码示例展示了如何在 Go 中实现 Bloom Filter,并应用于局域网的 IP 访问控制,显著提高系统响应速度和安全性。随着网络规模扩大和技术进步,持续优化算法和结合其他安全技术将是企业维持网络竞争力的关键。
50 2
公司局域网管理系统里的 Go 语言 Bloom Filter 算法,太值得深挖了
|
27天前
|
开发框架 前端开发 Go
eino — 基于go语言的大模型应用开发框架(二)
本文介绍了如何使用Eino框架实现一个基本的LLM(大语言模型)应用。Eino中的`ChatModel`接口提供了与不同大模型服务(如OpenAI、Ollama等)交互的统一方式,支持生成完整响应、流式响应和绑定工具等功能。`Generate`方法用于生成完整的模型响应,`Stream`方法以流式方式返回结果,`BindTools`方法为模型绑定工具。此外,还介绍了通过`Option`模式配置模型参数及模板功能,支持基于前端和用户自定义的角色及Prompt。目前主要聚焦于`ChatModel`的`Generate`方法,后续将继续深入学习。
269 7
|
23天前
|
存储 缓存 监控
企业监控软件中 Go 语言哈希表算法的应用研究与分析
在数字化时代,企业监控软件对企业的稳定运营至关重要。哈希表(散列表)作为高效的数据结构,广泛应用于企业监控中,如设备状态管理、数据分类和缓存机制。Go 语言中的 map 实现了哈希表,能快速处理海量监控数据,确保实时准确反映设备状态,提升系统性能,助力企业实现智能化管理。
31 3
|
1月前
|
监控 Linux PHP
【02】客户端服务端C语言-go语言-web端PHP语言整合内容发布-优雅草网络设备监控系统-2月12日优雅草简化Centos stream8安装zabbix7教程-本搭建教程非docker搭建教程-优雅草solution
【02】客户端服务端C语言-go语言-web端PHP语言整合内容发布-优雅草网络设备监控系统-2月12日优雅草简化Centos stream8安装zabbix7教程-本搭建教程非docker搭建教程-优雅草solution
87 20
|
1月前
|
存储 开发框架 Devops
eino — 基于go语言的大模型应用开发框架(一)
Eino 是一个受开源社区优秀LLM应用开发框架(如LangChain和LlamaIndex)启发的Go语言框架,强调简洁性、可扩展性和可靠性。它提供了易于复用的组件、强大的编排框架、简洁明了的API、最佳实践集合及实用的DevOps工具,支持快速构建和部署LLM应用。Eino不仅兼容多种模型库(如OpenAI、Ollama、Ark),还提供详细的官方文档和活跃的社区支持,便于开发者上手使用。
182 8
|
23天前
|
存储 缓存 安全
Go 语言中的 Sync.Map 详解:并发安全的 Map 实现
`sync.Map` 是 Go 语言中用于并发安全操作的 Map 实现,适用于读多写少的场景。它通过两个底层 Map(`read` 和 `dirty`)实现读写分离,提供高效的读性能。主要方法包括 `Store`、`Load`、`Delete` 等。在大量写入时性能可能下降,需谨慎选择使用场景。
|
1月前
|
存储 算法 Go
Go语言实战:错误处理和panic_recover之自定义错误类型
本文深入探讨了Go语言中的错误处理和panic/recover机制,涵盖错误处理的基本概念、自定义错误类型的定义、panic和recover的工作原理及应用场景。通过具体代码示例介绍了如何定义自定义错误类型、检查和处理错误值,并使用panic和recover处理运行时错误。文章还讨论了错误处理在实际开发中的应用,如网络编程、文件操作和并发编程,并推荐了一些学习资源。最后展望了未来Go语言在错误处理方面的优化方向。