Sessions: 第三方商店未正确将选项应用于编解码器。

创建于 2015-07-10  ·  17评论  ·  资料来源: gorilla/sessions

我们刚刚升级了这个包,它没有正确设置 cookie。 它正在设置像 _ga=GA1.1.922831813.14264788986 这样的东西,而不是常规的 base64 编码的 cookie 数据。 知道发生了什么或什么时候出现问题以便我们可以恢复到工作版本吗? 谢谢!

bug stale

最有用的评论

请注意,这也会影响大多数存储实现,因为它们都使用 securecookie 将 ID 保存到后端存储。

如果您手动调用SetMaxAge但不直接将其应用于默认情况,则redistore有一半的正确实现。 碰巧默认值(30 天)与securecookie 的相同。

快速浏览一下,似乎_所有_其他存储都受到影响——通过 Options 结构设置 MaxAge 不适用于从 CodecsFromPairs 生成的底层 securecookie 实例。 大多数最终用户不会遇到这个错误,因为我怀疑大多数用户设置的到期日期_小于_securecookie 隐式嵌入到 HMAC 中的内容。

FWIW:我也相信“遥远的未来”cookie 到期并不是很好,除非您有非常特殊的需求。

所有17条评论

“_ga”cookie 与 gorilla/sessions 无关 - 它是谷歌分析
曲奇饼。

如果 gorilla/sessions 没有设置 cookie(没有任何中断
更改)您能否发布相关代码和 Cookie 窗格的输出
从你的浏览器? (Chrome 下的检查器 > 资源 > cookie)

2015 年 7 月 10 日,星期五,上午 7:12,marksalpeter [email protected]
写道:

我们刚刚升级了这个包,它没有正确设置 cookie。 它是
设置像 _ga=GA1.1.922831813.14264788986 这样的东西而不是常规的
base64 编码的 cookie 数据。 知道发生了什么事或什么时候打破了
我们可以恢复到工作版本吗? 谢谢!


直接回复此邮件或在 GitHub 上查看
https://github.com/gorilla/sessions/issues/48。

嗨,马特,

我也在和 Mark 一起做这个项目,我可以给你一些更多的细节。 所以我们只是更新了包,因为最初的问题是 cookie 在 30 天后全部过期,即使我们将 MaxAge 设置为某个非常大的数字。 以下是当前有效的身份验证代码,给我们带来了问题:

常量 (
AUTH_SESSION_NAME = "身份验证会话"
)

变量 (
config = jconfig.LoadConfig( global.CONFIG() + "securecookie.json" )
authKey = []byte(config.GetString("authorization_key"))
encryptionKey = []byte(config.GetString("encryption_key"))
session = session.NewCookieStore(authKey, encryptionKey)
)

函数初始化(){

apiRouter.HandleFunc("/auth/"   , Authenticate      )
apiRouter.HandleFunc("/deauth/"  , Deauthenticate   )
apiRouter.HandleFunc("/reauth/" , ReAuthenticate    )

// register complex data types for saving in sessions
gob.Register(&models.Device{})
gob.Register(&models.Manager{})
gob.Register(&models.SalesRep{})

// modify the options of the session store so that the auth cookie never expires (this is set so it expires in 200 years...)
session.Options.MaxAge = 6307200000

}

// 此处理程序在系统中为设备和管理器授予权限
// 如果请求者已经被授权,它将显示他们的信息。 授权不同的
// 帐户,首先客户端必须取消身份验证。
//
// 参数
// 用户:
//
// 注意:从这个 auth 函数返回的数据只保证是准确的
// 当用户第一次进行身份验证时。 否则它将显示缓存在其会话中的数据
//
func Authenticate(响应http.ResponseWriter,请求*http.Request){

log.Println("Authenticate")

// open the session
auth_session, err :=  session.Get(request, AUTH_SESSION_NAME)
if err != nil {
    log.Println("there was an error retreiving the session:", err)
    InternalServerError(response, request)
    return
}

// already authroized as a manager
if manager, ok := auth_session.Values["manager"].(*models.Manager); ok {
    log.Printf("already logged in as manager %d", manager.Id)
    Success(response, request, manager)

// already authrorized as a device
} else if device, ok := auth_session.Values["device"].(*models.Device); ok {
    log.Printf("already logged in as device %d", device.Id)
    Success(response, request, device)

// attempt to gain authroization
} else {            
    fp  := parsers.FormParser(request)  

    // login as a manager
    if username, password := fp.GetString("user", ""), fp.GetString("pass", ""); username != "" && password != ""  {

        db := database.Open()
        defer db.Close()


        if manager := db.LoginAsManager(username, password); manager != nil {
            auth_session.Values["manager"] = manager

            // manager session error
            if err := auth_session.Save(request, response); err != nil {
                log.Printf("manager could not save session %s !\n", err.Error())
                InternalServerError(response, request)

            // manager login in success
            } else {
                log.Printf("logged in as manager %d !\n", manager.Id)
                Success(response, request, manager)     
            }

        // manager login failed
        } else {
            log.Printf("manager credentials not valid!")
            Unauthroized(response, request)
        }

    // login as a salesrep
    } else if username, password := fp.GetString("username", ""), fp.GetString("password", ""); username != "" && password != "" {

        db := database.Open()
        defer db.Close()


        if sales_rep_id := db.LoginAsSalesRep(username, password); sales_rep_id > 0 {

            sales_reps := db.GetSalesRep(&models.SalesRep{ Id: sales_rep_id });

            auth_session.Values["sales_rep"] = sales_reps[0];

            // sales rep session error
            if err := auth_session.Save(request, response); err != nil {
                log.Printf("sales rep could not save session %s !\n", err.Error())
                InternalServerError(response, request)

            // sales rep login in success
            } else {
                log.Printf("logged in as sales rep %d !\n", manager.Id)
                Success(response, request, manager)     
            }

        // sales rep login failed
        } else {
            log.Printf("sales rep credentials not valid!")
            Unauthroized(response, request)
        }

    // login as a device
    } else if pin_code := fp.GetInt32("PinCode", -1); pin_code > 0 {

        db := database.Open()
        defer db.Close()

        if devices := db.GetDevice(&models.Device{ PinCode:pin_code }, nil); len(devices) > 0 {
            auth_session.Values["device"] = &devices[0]

            // device session error
            if err := auth_session.Save(request, response); err != nil {
                log.Printf("device could not save session %s !\n", err.Error())
                InternalServerError(response, request)

            // device login in success
            } else {
                log.Printf("logged in as device %d !\n", devices[0].Id)
                Success(response, request, devices[0])      
            }

        // device login failed
        } else {
            log.Printf("device credentials not valid!")
            Unauthroized(response, request)
        }


    // no valid credentials were provided   
    } else {
        log.Println("no valid deivce or manager credentials")
        BadRequest(response, request)
    }

}

}

这是 30 天后我们在日志中得到的信息:

设备 Cookie: authentication-session=_KaJDVq4lIdQgeHiHcnSMw1IEPDyg3-9XEIBBPxw==; 路径=/; 到期日 = 2215 年 5 月 15 日星期一 20:47:49 UTC; 最大年龄=6307200000

2015/07/10 15:56:04 /routers/api/Auth.go:70: securecookie: 过期时间戳

无论我们将 MaxAge 设置为什么,cookie 似乎都会在 30 天后过期。 您对我们的任何帮助都是巨大的。 谢谢!

好的,我模拟了一个最小的演示程序,我将整个商店的MaxAge设置

package main

import (
    "fmt"
    "log"
    "net/http"

    "github.com/gorilla/sessions"
)

var store = sessions.NewCookieStore([]byte("some-appropriately-auth-key"))

func SomeHandler(w http.ResponseWriter, r *http.Request) {
    session, err := store.Get(r, "example")
    if err != nil {
        http.Error(w, "No good!", 500)
        return
    }

    session.Values["gorilla"] = "sessions!"
    err = session.Save(r, w)
    if err != nil {
        http.Error(w, "No good!", 500)
        return
    }

    fmt.Fprintf(w, "%v", session.Values["gorilla"])
}

func main() {
    store.Options = &sessions.Options{
        MaxAge: 86400 * 60, // 2 months
    }

    http.HandleFunc("/", SomeHandler)
    log.Fatal(http.ListenAndServe(":8002", nil))
}

这给了我我期望的到期时间:

Expiry time test

手动设置我的时钟向前 ~33 天也会使我从securecookie得到expired timestamp错误 - 这是错误的。

清除浏览器中的 cookie,将时钟重置为 now(),然后通过以下方式在底层*securecookie.SecureCookie上强制执行MaxAge

    store.Options = &sessions.Options{
        MaxAge: 86400 * 60, // 2 months
    }
    for _, s := range store.Codecs {
        if cookie, ok := s.(*securecookie.SecureCookie); ok {
            cookie.MaxAge(86400 * 90)
        }
    }

这仍然为我提供了一个在未来 2 个月后到期的浏览器 cookie,因为每个编解码器的maxAge字段不会影响我们写出http.Cookie时浏览器看到的内容。 但是,将我的时钟设置为未来 33 天不会产生过期时间戳错误。

所以我做了一些挖掘:

  • 我们的错误“securecookie: expired timestamp”是从https://github.com/gorilla/securecookie/blob/master/securecookie.go#L244 -L246 生成的
  • 此行在 CookieStore 创建时设置 MaxAge: https :
  • 通过调用CodecsFromPairs应用选项之前,它修复了s.MaxAge内的每个*securecookie.SecureCookie (如编解码器片的一部分)securecookie默认: 86400 * 30
  • 如果我们在sessions.CookieStore.Codecs范围内(按照上面的片段)并在每个 securecookie 实例上设置 MaxAge,我们就可以绕过这个。

TL;DR :根本原因是写入浏览器的http.Cookie获得了我们在选项中设置的时间戳,但底层 securecookie 实例的 HMAC 是 date|value|mac 的串联 - 其中 date 是我们的选项未修复的 _default_ MaxAge。 一旦超过 30 天,HMAC 验证就会失败。

修复将sessions.NewCookieStore覆盖它创建的编解码器并将 Options.MaxAge 应用于每个*securecookie.SecureCookie实例,以允许它纠正 cookie 到期和 HMAC 时间戳验证之间的不匹配。

cc/ @kisielk在我推送修复之前验证我的发现。

PS:与此无关,最佳做法是在您的程序中打开单个数据库池,无论是通过全局还是通过显式传递指针。 根据请求打开和关闭池会影响性能。 *sql.DB对于并发访问是安全的。

请注意,这也会影响大多数存储实现,因为它们都使用 securecookie 将 ID 保存到后端存储。

如果您手动调用SetMaxAge但不直接将其应用于默认情况,则redistore有一半的正确实现。 碰巧默认值(30 天)与securecookie 的相同。

快速浏览一下,似乎_所有_其他存储都受到影响——通过 Options 结构设置 MaxAge 不适用于从 CodecsFromPairs 生成的底层 securecookie 实例。 大多数最终用户不会遇到这个错误,因为我怀疑大多数用户设置的到期日期_小于_securecookie 隐式嵌入到 HMAC 中的内容。

FWIW:我也相信“遥远的未来”cookie 到期并不是很好,除非您有非常特殊的需求。

我还想指出,当前时间戳在当前实现中是完全未加密的。 从理论上讲,我可以对 cookie 进行 base64 解码,将时间戳更改为当天,对 cookie 进行 base64 编码,然后 MaxAge 属性根本无关紧要,即使您确实修复了 options.MaxAge 问题。

是的,“遥远的未来”cookie 只是为了测试,我们现在在我们的生产服务器上将 MaxAge 设置为 0。 我们在周末发现了这个问题并写了一个类似的修复程序,但比你的要简洁一些。

感谢您调查问题!

@wbaron - 没问题。 我们很快就会在上游包中修复它
完成后我会 ping 你,这样你就不必维护自己的叉子(如果你
不想)。

@marksalpeter - 你能指出在哪里吗? securecookie MACs
name|date|value 在 base64 编码之前用于字符串表示。 如果
你在客户端解码你不能修改那里的时间戳(它是
MAC'ed - 实际上没有什么可以修改的),如果你尝试,
cookie 应该在解码(请求)时失败验证,因为 MAC 会失败
正确解码哈希/身份验证密钥。 参考:
https://github.com/gorilla/securecookie/blob/master/securecookie.go#L185 -L191

在星期二,2015年7月14日在10时40分wbaron [email protected]写道:

是的,“遥远的未来”cookie 只是为了测试,我们将 MaxAge 设置为
现在在我们的生产服务器上为 0。 我们在周末发现了这个问题
写了一个类似于你的修复程序,简洁一点。

感谢您调查问题!


直接回复此邮件或在 GitHub 上查看
https://github.com/gorilla/sessions/issues/48#issuecomment -121257995。

@elithrar我的错误。 当我解码 cookie 时,我得到 [timestamp]|[MAC'ed info] 我只是假设最大信息之前的时间戳是你使用的。

@elithrar你的提议对我来说听起来不错。 遗憾的是它必须单独应用于每个商店:/

@kisielk - 绝对是一种耻辱。 我想避免接触第三方
商店,但没有一种干净的方式来有效地耦合第三方商店
在securecookie 中使用s.MaxAge 方法的Options.MaxAge 字段。

我正在考虑在 securecookie 中提供一个func CodecMaxAge(codecs []Codec, age int) []Codec函数(缺点:不能是一个方法
接口 + 添加到公共 API)在内部执行类型
断言 + 在每个编解码器上调用 s.MaxAge(age)。 这会保持冗余(循环
在切片上,将断言输入到 cookie 类型)第三方商店中的代码
因为他们只需要在NewXXXXStore函数中调用securecookie.CodecMaxAge(mystore.Codecs, mystore.opts.MaxAge)来设置
基础 s.maxAge 字段。

接受更好的想法,但这似乎是最简单的。

2015 年 7 月 17 日星期五上午 8:03 Kamil Kisiel [email protected]
写道:

@elithrar https://github.com/elithrar你的提议听起来不错
我。 遗憾的是它必须单独应用于每个商店:/


直接回复此邮件或在 GitHub 上查看
https://github.com/gorilla/sessions/issues/48#issuecomment -122133977。

是的,我觉得还好。 一旦您在此处进行更改,我将向其他商店发送一些 PR 以使用它。

用于跟踪目的:

  • [x] [gorilla/sessions](https://github.com/gorilla/sessions)
  • [ ] [github.com/starJammer/gorilla-sessions-arangodb](https://github.com/starJammer/gorilla-sessions-arangodb) - ArangoDB
  • [ ] [github.com/yosssi/boltstore](https://github.com/yosssi/boltstore) - Bolt
  • [ ] [github.com/srinathgs/couchbasestore](https://github.com/srinathgs/couchbasestore) - Couchbase
  • [ ] [github.com/denizeren/dynamostore](https://github.com/denizeren/dynamostore) - AWS 上的 Dynamodb
  • [ ] [github.com/bradleypeabody/gorilla-sessions-memcache](https://github.com/bradleypeabody/gorilla-sessions-memcache) - Memcache
  • [ ] [github.com/hnakamur/gaesessions](https://github.com/hnakamur/gaesessions) - GAE 上的 Memcache
  • [x] [github.com/kidstuff/mongostore](https://github.com/kidstuff/mongostore) - MongoDB
  • [ ] [github.com/srinathgs/mysqlstore](https://github.com/srinathgs/mysqlstore) - MySQL
  • [x] [github.com/antonlindstrom/pgstore](https://github.com/antonlindstrom/pgstore) - PostgreSQL
  • [ ] [github.com/boj/redistore](https://github.com/boj/redistore) - Redis
  • [x] [github.com/boj/rethinkstore](https://github.com/boj/rethinkstore) - RethinkDB
  • [ ] [github.com/boj/riakstore](https://github.com/boj/riakstore) - Riak
  • [ ] [github.com/michaeljs1990/sqlitestore](https://github.com/michaeljs1990/sqlitestore) - SQLite

一旦 PR 被合并,我会追踪这些。

此问题已自动标记为陈旧,因为它没有看到最近的更新。 它会在几天后自动关闭。

在从另一个包中运行后查看这个问题,它看起来不应该被关闭...... @elithrar清单仍然需要另外 10 个检查,除非该信息已过时。

它应该是准确的:但并非所有商店都积极维护或提交了 PR。

此问题已自动标记为陈旧,因为它没有看到最近的更新。 它会在几天后自动关闭。

此问题已自动标记为陈旧,因为它没有看到最近的更新。 它会在几天后自动关闭。

此页面是否有帮助?
0 / 5 - 0 等级

相关问题

elithrar picture elithrar  ·  25评论

danvonk picture danvonk  ·  9评论

CasperHK picture CasperHK  ·  11评论

gtaylor picture gtaylor  ·  7评论

luca-moser picture luca-moser  ·  3评论