1
0
mirror of https://github.com/kataras/iris.git synced 2025-12-23 21:07:03 +00:00

enhanced cookie security and management

Former-commit-id: a97b0b33e87749a2e8c32e63269fcc60fa326ff3
This commit is contained in:
Gerasimos (Makis) Maropoulos
2020-05-09 14:04:51 +03:00
parent d5f1649895
commit 50b18c7515
18 changed files with 490 additions and 466 deletions

View File

@@ -36,6 +36,7 @@ import (
jsoniter "github.com/json-iterator/go"
"github.com/microcosm-cc/bluemonday"
"github.com/vmihailenco/msgpack/v5"
"golang.org/x/net/publicsuffix"
"golang.org/x/time/rate"
"gopkg.in/yaml.v3"
)
@@ -984,6 +985,25 @@ type Context interface {
// | Cookies |
// +------------------------------------------------------------+
// AddCookieOptions adds cookie options for `SetCookie`,
// `SetCookieKV, UpsertCookie` and `RemoveCookie` methods
// for the current request. It can be called from a middleware before
// cookies sent or received from the next Handler in the chain.
// See `ClearCookieOptions` too.
//
// Available builtin Cookie options are:
// * CookieSameSite
// * CookiePath
// * CookieCleanPath
// * CookieExpires
// * CookieHTTPOnly
// * CookieEncoding
//
// Example at: https://github.com/kataras/iris/tree/master/_examples/cookies/securecookie
AddCookieOptions(options ...CookieOption)
// ClearCookieOptions clears any previously registered cookie options.
// See `AddCookieOptions` too.
ClearCookieOptions()
// SetCookie adds a cookie.
// Use of the "options" is not required, they can be used to amend the "cookie".
//
@@ -994,14 +1014,6 @@ type Context interface {
// if already set by a previous `SetCookie` call.
// It reports whether the cookie is new (true) or an existing one was updated (false).
UpsertCookie(cookie *http.Cookie, options ...CookieOption) bool
// SetSameSite sets a same-site rule for cookies to set.
// SameSite allows a server to define a cookie attribute making it impossible for
// the browser to send this cookie along with cross-site requests. The main
// goal is to mitigate the risk of cross-origin information leakage, and provide
// some protection against cross-site request forgery attacks.
//
// See https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00 for details.
SetSameSite(sameSite http.SameSite)
// SetCookieKV adds a cookie, requires the name(string) and the value(string).
//
// By default it expires at 2 hours and it's added to the root path,
@@ -4734,48 +4746,171 @@ func (ctx *context) SendFileWithRate(src, destName string, limit float64, burst
// | Cookies |
// +------------------------------------------------------------+
// Set of Cookie actions for `CookieOption`.
const (
OpCookieGet uint8 = iota
OpCookieSet
OpCookieDel
)
// CookieOption is the type of function that is accepted on
// context's methods like `SetCookieKV`, `RemoveCookie` and `SetCookie`
// as their (last) variadic input argument to amend the end cookie's form.
//
// Any custom or builtin `CookieOption` is valid,
// see `CookiePath`, `CookieCleanPath`, `CookieExpires` and `CookieHTTPOnly` for more.
type CookieOption func(*http.Cookie)
// The "op" is the operation code, 0 is GET, 1 is SET and 2 is REMOVE.
type CookieOption func(c *http.Cookie, op uint8)
// CookiePath is a `CookieOption`.
// Use it to change the cookie's Path field.
func CookiePath(path string) CookieOption {
return func(c *http.Cookie) {
c.Path = path
// findCookieAgainst reports whether the "cookie.Name" is in the list of "cookieNames".
// Notes:
// If "cookieNames" slice is empty then it returns true,
// If "cookie.Name" is empty then it returns false.
func findCookieAgainst(cookie *http.Cookie, cookieNames []string) bool {
if cookie.Name == "" {
return false
}
if len(cookieNames) > 0 {
for _, name := range cookieNames {
if cookie.Name == name {
return true
}
}
return false
}
return true
}
// CookieAllowReclaim accepts the Context itself.
// If set it will add the cookie to (on `CookieSet`, `CookieSetKV`, `CookieUpsert`)
// or remove the cookie from (on `CookieRemove`) the Request object too.
func CookieAllowReclaim(ctx Context, cookieNames ...string) CookieOption {
return func(c *http.Cookie, op uint8) {
if op == OpCookieGet {
return
}
if !findCookieAgainst(c, cookieNames) {
return
}
switch op {
case OpCookieSet:
ctx.Request().AddCookie(c)
case OpCookieDel:
// TODO: delete only this c.Name.
ctx.Request().Header.Set("Cookie", "")
}
}
}
// CookieAllowSubdomains set to the Cookie Options
// in order to allow subdomains to have access to the cookies.
// It sets the cookie's Domain field (if was empty) and
// it also sets the cookie's SameSite to lax mode too.
func CookieAllowSubdomains(ctx Context, cookieNames ...string) CookieOption {
host := ctx.Host()
if portIdx := strings.IndexByte(host, ':'); portIdx > 0 {
host = host[0:portIdx]
}
cookieDomain := "." + host
if domain, err := publicsuffix.EffectiveTLDPlusOne(host); err == nil {
cookieDomain = "." + domain
}
return func(c *http.Cookie, _ uint8) {
if c.Domain != "" {
return // already set.
}
if !findCookieAgainst(c, cookieNames) {
return
}
c.Domain = cookieDomain
c.SameSite = http.SameSiteLaxMode // allow subdomain sharing.
}
}
// CookieCleanPath is a `CookieOption`.
// Use it to clear the cookie's Path field, exactly the same as `CookiePath("")`.
func CookieCleanPath(c *http.Cookie) {
c.Path = ""
// CookieSameSite sets a same-site rule for cookies to set.
// SameSite allows a server to define a cookie attribute making it impossible for
// the browser to send this cookie along with cross-site requests. The main
// goal is to mitigate the risk of cross-origin information leakage, and provide
// some protection against cross-site request forgery attacks.
//
// See https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00 for details.
func CookieSameSite(sameSite http.SameSite) CookieOption {
return func(c *http.Cookie, op uint8) {
if op == OpCookieSet {
c.SameSite = sameSite
}
}
}
// CookieExpires is a `CookieOption`.
// Use it to change the cookie's Expires and MaxAge fields by passing the lifetime of the cookie.
func CookieExpires(durFromNow time.Duration) CookieOption {
return func(c *http.Cookie) {
c.Expires = time.Now().Add(durFromNow)
c.MaxAge = int(durFromNow.Seconds())
// CookieSecure sets the cookie's Secure option if the current request's
// connection is using TLS. See `CookieHTTPOnly` too.
func CookieSecure(ctx Context) CookieOption {
return func(c *http.Cookie, op uint8) {
if op == OpCookieSet {
if ctx.Request().TLS != nil {
c.Secure = true
}
}
}
}
// CookieHTTPOnly is a `CookieOption`.
// Use it to set the cookie's HttpOnly field to false or true.
// HttpOnly field defaults to true for `RemoveCookie` and `SetCookieKV`.
// See `CookieSecure` too.
func CookieHTTPOnly(httpOnly bool) CookieOption {
return func(c *http.Cookie) {
c.HttpOnly = httpOnly
return func(c *http.Cookie, op uint8) {
if op == OpCookieSet {
c.HttpOnly = httpOnly
}
}
}
type (
// CookieEncoder should encode the cookie value.
// CookiePath is a `CookieOption`.
// Use it to change the cookie's Path field.
func CookiePath(path string) CookieOption {
return func(c *http.Cookie, op uint8) {
if op > OpCookieGet { // on set and remove.
c.Path = path
}
}
}
// CookieCleanPath is a `CookieOption`.
// Use it to clear the cookie's Path field, exactly the same as `CookiePath("")`.
func CookieCleanPath(c *http.Cookie, op uint8) {
if op > OpCookieGet {
c.Path = ""
}
}
// CookieExpires is a `CookieOption`.
// Use it to change the cookie's Expires and MaxAge fields by passing the lifetime of the cookie.
func CookieExpires(durFromNow time.Duration) CookieOption {
return func(c *http.Cookie, op uint8) {
if op == OpCookieSet {
c.Expires = time.Now().Add(durFromNow)
c.MaxAge = int(durFromNow.Seconds())
}
}
}
// SecureCookie should encodes and decodes
// authenticated and optionally encrypted cookie values.
// See `CookieEncoding` package-level function.
type SecureCookie interface {
// Encode should encode the cookie value.
// Should accept the cookie's name as its first argument
// and as second argument the cookie value ptr.
// Should return an encoded value or an empty one if encode operation failed.
@@ -4785,9 +4920,9 @@ type (
// and remember: if you use AES it only supports key sizes of 16, 24 or 32 bytes.
// You either need to provide exactly that amount or you derive the key from what you type in.
//
// See `CookieDecoder` too.
CookieEncoder func(cookieName string, value interface{}) (string, error)
// CookieDecoder should decode the cookie value.
// See `Decode` too.
Encode(cookieName string, cookieValue interface{}) (string, error)
// Decode should decode the cookie value.
// Should accept the cookie's name as its first argument,
// as second argument the encoded cookie value and as third argument the decoded value ptr.
// Should return a decoded value or an empty one if decode operation failed.
@@ -4797,39 +4932,100 @@ type (
// and remember: if you use AES it only supports key sizes of 16, 24 or 32 bytes.
// You either need to provide exactly that amount or you derive the key from what you type in.
//
// See `CookieEncoder` too.
CookieDecoder func(cookieName string, cookieValue string, v interface{}) error
)
// See `Encode` too.
Decode(cookieName string, cookieValue string, cookieValuePtr interface{}) error
}
// CookieEncode is a `CookieOption`.
// Provides encoding functionality when adding a cookie.
// Accepts a `CookieEncoder` and sets the cookie's value to the encoded value.
// Users of that is the `SetCookie` and `SetCookieKV`.
// CookieEncoding accepts a value which implements `Encode` and `Decode` methods.
// It calls its `Encode` on `Context.SetCookie, UpsertCookie, and SetCookieKV` methods.
// And on `Context.GetCookie` method it calls its `Decode`.
// If "cookieNames" slice is not empty then only cookies
// with that `Name` will be encoded on set and decoded on get, that way you can encrypt
// specific cookie names (like the session id) and let the rest of the cookies "insecure".
//
// Example: https://github.com/kataras/iris/tree/master/_examples/cookies/securecookie
func CookieEncode(encode CookieEncoder) CookieOption {
return func(c *http.Cookie) {
newVal, err := encode(c.Name, c.Value)
if err != nil {
c.Value = ""
} else {
c.Value = newVal
func CookieEncoding(encoding SecureCookie, cookieNames ...string) CookieOption {
return func(c *http.Cookie, op uint8) {
if op == OpCookieDel {
return
}
if !findCookieAgainst(c, cookieNames) {
return
}
switch op {
case OpCookieSet:
// Should encode, it's a write to the client operation.
newVal, err := encoding.Encode(c.Name, c.Value)
if err != nil {
c.Value = ""
} else {
c.Value = newVal
}
return
case OpCookieGet:
// Should decode, it's a read from the client operation.
if err := encoding.Decode(c.Name, c.Value, &c.Value); err != nil {
c.Value = ""
}
}
}
}
// CookieDecode is a `CookieOption`.
// Provides decoding functionality when retrieving a cookie.
// Accepts a `CookieDecoder` and sets the cookie's value to the decoded value before return by the `GetCookie`.
// User of that is the `GetCookie`.
const cookieOptionsContextKey = "iris.cookie.options"
// AddCookieOptions adds cookie options for `SetCookie`,
// `SetCookieKV, UpsertCookie` and `RemoveCookie` methods
// for the current request. It can be called from a middleware before
// cookies sent or received from the next Handler in the chain.
//
// Example: https://github.com/kataras/iris/tree/master/_examples/cookies/securecookie
func CookieDecode(decode CookieDecoder) CookieOption {
return func(c *http.Cookie) {
if err := decode(c.Name, c.Value, &c.Value); err != nil {
c.Value = ""
// Available builtin Cookie options are:
// * CookieAllowReclaim
// * CookieAllowSubdomains
// * CookieSecure
// * CookieHTTPOnly
// * CookieSameSite
// * CookiePath
// * CookieCleanPath
// * CookieExpires
// * CookieEncoding
//
// Example at: https://github.com/kataras/iris/tree/master/_examples/cookies/securecookie
func (ctx *context) AddCookieOptions(options ...CookieOption) {
if len(options) == 0 {
return
}
if v := ctx.Values().Get(cookieOptionsContextKey); v != nil {
if opts, ok := v.([]CookieOption); ok {
options = append(opts, options...)
}
}
ctx.Values().Set(cookieOptionsContextKey, options)
}
func (ctx *context) applyCookieOptions(c *http.Cookie, op uint8, override []CookieOption) {
if v := ctx.Values().Get(cookieOptionsContextKey); v != nil {
if options, ok := v.([]CookieOption); ok {
for _, opt := range options {
opt(c, op)
}
}
}
// The function's ones should be called last, so they can override
// the stored ones (i.e by a prior middleware).
for _, opt := range override {
opt(c, op)
}
}
// ClearCookieOptions clears any previously registered cookie options.
// See `AddCookieOptions` too.
func (ctx *context) ClearCookieOptions() {
ctx.Values().Remove(cookieOptionsContextKey)
}
// SetCookie adds a cookie.
@@ -4837,29 +5033,22 @@ func CookieDecode(decode CookieDecoder) CookieOption {
//
// Example: https://github.com/kataras/iris/tree/master/_examples/cookies/basic
func (ctx *context) SetCookie(cookie *http.Cookie, options ...CookieOption) {
cookie.SameSite = GetSameSite(ctx)
for _, opt := range options {
opt(cookie)
}
ctx.applyCookieOptions(cookie, OpCookieSet, options)
http.SetCookie(ctx.writer, cookie)
}
const setCookieHeaderKey = "Set-Cookie"
// UpsertCookie adds a cookie to the response like `SetCookie` does
// but it will also perform a replacement of the cookie
// if already set by a previous `SetCookie` call.
// It reports whether the cookie is new (true) or an existing one was updated (false).
func (ctx *context) UpsertCookie(cookie *http.Cookie, options ...CookieOption) bool {
cookie.SameSite = GetSameSite(ctx)
for _, opt := range options {
opt(cookie)
}
ctx.applyCookieOptions(cookie, OpCookieSet, options)
header := ctx.ResponseWriter().Header()
if cookies := header["Set-Cookie"]; len(cookies) > 0 {
if cookies := header[setCookieHeaderKey]; len(cookies) > 0 {
s := cookie.Name + "=" // name=?value
for i, c := range cookies {
if strings.HasPrefix(c, s) {
@@ -4867,40 +5056,21 @@ func (ctx *context) UpsertCookie(cookie *http.Cookie, options ...CookieOption) b
// Probably the cookie is set and then updated in the first session creation
// (e.g. UpdateExpiration, see https://github.com/kataras/iris/issues/1485).
cookies[i] = cookie.String()
header["Set-Cookie"] = cookies
header[setCookieHeaderKey] = cookies
return false
}
}
}
header.Add("Set-Cookie", cookie.String())
header.Add(setCookieHeaderKey, cookie.String())
return true
}
const sameSiteContextKey = "iris.cookie_same_site"
// SetSameSite sets a same-site rule for cookies to set.
// SameSite allows a server to define a cookie attribute making it impossible for
// the browser to send this cookie along with cross-site requests. The main
// goal is to mitigate the risk of cross-origin information leakage, and provide
// some protection against cross-site request forgery attacks.
// SetCookieKVExpiration is 365 days by-default
// you can change it or simple, use the SetCookie for more control.
//
// See https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00 for details.
func (ctx *context) SetSameSite(sameSite http.SameSite) {
ctx.Values().Set(sameSiteContextKey, sameSite)
}
// GetSameSite returns the saved-to-context cookie http.SameSite option.
func GetSameSite(ctx Context) http.SameSite {
if v := ctx.Values().Get(sameSiteContextKey); v != nil {
sameSite, ok := v.(http.SameSite)
if ok {
return sameSite
}
}
return http.SameSiteDefaultMode
}
// See CookieExpires` for more.
var SetCookieKVExpiration = time.Duration(8760) * time.Hour
// SetCookieKV adds a cookie, requires the name(string) and the value(string).
//
@@ -4925,8 +5095,13 @@ func (ctx *context) SetCookieKV(name, value string, options ...CookieOption) {
c.Name = name
c.Value = url.QueryEscape(value)
c.HttpOnly = true
// MaxAge=0 means no 'Max-Age' attribute specified.
// MaxAge<0 means delete cookie now, equivalently 'Max-Age: 0'
// MaxAge>0 means Max-Age attribute present and given in seconds
c.Expires = time.Now().Add(SetCookieKVExpiration)
c.MaxAge = int(SetCookieKVExpiration.Seconds())
c.MaxAge = int(time.Until(c.Expires).Seconds())
ctx.SetCookie(c, options...)
}
@@ -4938,24 +5113,24 @@ func (ctx *context) SetCookieKV(name, value string, options ...CookieOption) {
//
// Example: https://github.com/kataras/iris/tree/master/_examples/cookies/basic
func (ctx *context) GetCookie(name string, options ...CookieOption) string {
cookie, err := ctx.request.Cookie(name)
c, err := ctx.request.Cookie(name)
if err != nil {
return ""
}
for _, opt := range options {
opt(cookie)
}
ctx.applyCookieOptions(c, OpCookieGet, options)
value, _ := url.QueryUnescape(cookie.Value)
value, _ := url.QueryUnescape(c.Value)
return value
}
// SetCookieKVExpiration is 365 days by-default
// you can change it or simple, use the SetCookie for more control.
//
// See `SetCookieKVExpiration` and `CookieExpires` for more.
var SetCookieKVExpiration = time.Duration(8760) * time.Hour
var (
// CookieExpireDelete may be set on Cookie.Expire for expiring the given cookie.
CookieExpireDelete = time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)
// CookieExpireUnlimited indicates that does expires after 24 years.
CookieExpireUnlimited = time.Now().AddDate(24, 10, 10)
)
// RemoveCookie deletes a cookie by its name and path = "/".
// Tip: change the cookie's path to the current one by: RemoveCookie("name", iris.CookieCleanPath)
@@ -4967,13 +5142,13 @@ func (ctx *context) RemoveCookie(name string, options ...CookieOption) {
c.Value = ""
c.Path = "/" // if user wants to change it, use of the CookieOption `CookiePath` is required if not `ctx.SetCookie`.
c.HttpOnly = true
// RFC says 1 second, but let's do it 1 to make sure is working
exp := time.Now().Add(-time.Duration(1) * time.Minute)
c.Expires = exp
c.Expires = CookieExpireDelete
c.MaxAge = -1
ctx.SetCookie(c, options...)
// delete request's cookie also, which is temporary available.
ctx.request.Header.Set("Cookie", "")
ctx.applyCookieOptions(c, OpCookieDel, options)
http.SetCookie(ctx.writer, c)
}
// VisitAllCookies takes a visitor function which is called