Skip to content

Commit 8e2ab96

Browse files
🧹 chore: Improve Cookie() validation (#3546)
* validate cookies with net/http * refactor: sanitize cookie via net/http * Address review comments * refactor: set cookie attrs via setters * Update ctx.go Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Update ctx.go * Update ctx.go * Update ctx.go * Add more unit-tests * Fix test * Add default case for samesite --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
1 parent f9547df commit 8e2ab96

2 files changed

Lines changed: 165 additions & 19 deletions

File tree

ctx.go

Lines changed: 63 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -397,36 +397,80 @@ func (c *DefaultCtx) RequestCtx() *fasthttp.RequestCtx {
397397

398398
// Cookie sets a cookie by passing a cookie struct.
399399
func (c *DefaultCtx) Cookie(cookie *Cookie) {
400-
fcookie := fasthttp.AcquireCookie()
401-
fcookie.SetKey(cookie.Name)
402-
fcookie.SetValue(cookie.Value)
403-
fcookie.SetPath(cookie.Path)
404-
fcookie.SetDomain(cookie.Domain)
405-
// only set max age and expiry when SessionOnly is false
406-
// i.e. cookie supposed to last beyond browser session
407-
// refer: https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#define_the_lifetime_of_a_cookie
408-
if !cookie.SessionOnly {
409-
fcookie.SetMaxAge(cookie.MaxAge)
410-
fcookie.SetExpire(cookie.Expires)
400+
if cookie.Path == "" {
401+
cookie.Path = "/"
402+
}
403+
404+
if cookie.SessionOnly {
405+
cookie.MaxAge = 0
406+
cookie.Expires = time.Time{}
411407
}
412-
fcookie.SetSecure(cookie.Secure)
413-
fcookie.SetHTTPOnly(cookie.HTTPOnly)
408+
409+
var sameSite http.SameSite
414410

415411
switch utils.ToLower(cookie.SameSite) {
416412
case CookieSameSiteStrictMode:
417-
fcookie.SetSameSite(fasthttp.CookieSameSiteStrictMode)
413+
sameSite = http.SameSiteStrictMode
418414
case CookieSameSiteNoneMode:
419-
fcookie.SetSameSite(fasthttp.CookieSameSiteNoneMode)
415+
sameSite = http.SameSiteNoneMode
420416
case CookieSameSiteDisabled:
421-
fcookie.SetSameSite(fasthttp.CookieSameSiteDisabled)
417+
sameSite = 0
418+
case CookieSameSiteLaxMode:
419+
sameSite = http.SameSiteLaxMode
422420
default:
421+
sameSite = http.SameSiteLaxMode
422+
}
423+
424+
// create/validate cookie using net/http
425+
hc := &http.Cookie{
426+
Name: cookie.Name,
427+
Value: cookie.Value,
428+
Path: cookie.Path,
429+
Domain: cookie.Domain,
430+
Expires: cookie.Expires,
431+
MaxAge: cookie.MaxAge,
432+
Secure: cookie.Secure,
433+
HttpOnly: cookie.HTTPOnly,
434+
SameSite: sameSite,
435+
Partitioned: cookie.Partitioned,
436+
}
437+
438+
if err := hc.Valid(); err != nil {
439+
// invalid cookies are ignored, same approach as net/http
440+
return
441+
}
442+
443+
// create fasthttp cookie
444+
fcookie := fasthttp.AcquireCookie()
445+
fcookie.SetKey(hc.Name)
446+
fcookie.SetValue(hc.Value)
447+
fcookie.SetPath(hc.Path)
448+
fcookie.SetDomain(hc.Domain)
449+
450+
if !cookie.SessionOnly {
451+
fcookie.SetMaxAge(hc.MaxAge)
452+
fcookie.SetExpire(hc.Expires)
453+
}
454+
455+
fcookie.SetSecure(hc.Secure)
456+
fcookie.SetHTTPOnly(hc.HttpOnly)
457+
458+
switch sameSite {
459+
case http.SameSiteLaxMode:
423460
fcookie.SetSameSite(fasthttp.CookieSameSiteLaxMode)
461+
case http.SameSiteStrictMode:
462+
fcookie.SetSameSite(fasthttp.CookieSameSiteStrictMode)
463+
case http.SameSiteNoneMode:
464+
fcookie.SetSameSite(fasthttp.CookieSameSiteNoneMode)
465+
case http.SameSiteDefaultMode:
466+
fcookie.SetSameSite(fasthttp.CookieSameSiteDefaultMode)
467+
default:
468+
fcookie.SetSameSite(fasthttp.CookieSameSiteDisabled)
424469
}
425470

426-
// CHIPS allows to partition cookie jar by top-level site.
427-
// refer: https://developers.google.com/privacy-sandbox/3pcd/chips
428-
fcookie.SetPartitioned(cookie.Partitioned)
471+
fcookie.SetPartitioned(hc.Partitioned)
429472

473+
// Set resp header
430474
c.fasthttp.Response.Header.SetCookie(fcookie)
431475
fasthttp.ReleaseCookie(fcookie)
432476
}

ctx_test.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -920,6 +920,108 @@ func Test_Ctx_Cookie(t *testing.T) {
920920
require.Equal(t, expect, c.Res().Get(HeaderSetCookie))
921921
}
922922

923+
// go test -run Test_Ctx_Cookie_PartitionedSecure
924+
func Test_Ctx_Cookie_PartitionedSecure(t *testing.T) {
925+
t.Parallel()
926+
app := New()
927+
c := app.AcquireCtx(&fasthttp.RequestCtx{})
928+
929+
ck := &Cookie{
930+
Name: "ps",
931+
Value: "v",
932+
Secure: true,
933+
SameSite: CookieSameSiteNoneMode,
934+
Partitioned: true,
935+
}
936+
c.Res().Cookie(ck)
937+
require.Equal(t, "ps=v; path=/; secure; SameSite=None; Partitioned", c.Res().Get(HeaderSetCookie))
938+
}
939+
940+
// go test -run Test_Ctx_Cookie_Invalid
941+
func Test_Ctx_Cookie_Invalid(t *testing.T) {
942+
t.Parallel()
943+
app := New()
944+
945+
cases := []*Cookie{
946+
{Name: "", Value: "a"}, // empty name
947+
{Name: "foo bar", Value: "a"}, // invalid char in name
948+
{Name: "n", Value: "bad\nval"}, // invalid value byte
949+
{Name: "d", Value: "b", Domain: "in valid"}, // invalid domain spaces
950+
{Name: "d", Value: "b", Domain: "example..com"}, // invalid domain dots
951+
{Name: "i", Value: "b", Domain: "2001:db8::1"}, // ipv6 not allowed
952+
{Name: "p", Value: "b", Path: "\x00"}, // invalid path byte
953+
{Name: "e", Value: "b", Expires: time.Date(1500, 1, 1, 0, 0, 0, 0, time.UTC)}, // invalid expires
954+
{Name: "s", Value: "b", Partitioned: true}, // partitioned but not secure
955+
}
956+
957+
for _, invalid := range cases {
958+
c := app.AcquireCtx(&fasthttp.RequestCtx{})
959+
c.Res().Cookie(invalid)
960+
require.Empty(t, c.Res().Get(HeaderSetCookie))
961+
c.Response().Header.Reset()
962+
app.ReleaseCtx(c)
963+
}
964+
}
965+
966+
// go test -run Test_Ctx_Cookie_DefaultPath
967+
func Test_Ctx_Cookie_DefaultPath(t *testing.T) {
968+
t.Parallel()
969+
app := New()
970+
c := app.AcquireCtx(&fasthttp.RequestCtx{})
971+
972+
ck := &Cookie{
973+
Name: "p",
974+
Value: "v",
975+
// Path intentionally empty to verify defaulting
976+
}
977+
978+
c.Res().Cookie(ck)
979+
require.Equal(t,
980+
"p=v; path=/; SameSite=Lax",
981+
c.Res().Get(HeaderSetCookie),
982+
)
983+
}
984+
985+
// go test -run Test_Ctx_Cookie_MaxAgeOnly
986+
func Test_Ctx_Cookie_MaxAgeOnly(t *testing.T) {
987+
t.Parallel()
988+
app := New()
989+
c := app.AcquireCtx(&fasthttp.RequestCtx{})
990+
991+
ck := &Cookie{
992+
Name: "ttl",
993+
Value: "v",
994+
MaxAge: 3600,
995+
}
996+
c.Res().Cookie(ck)
997+
998+
require.Equal(t,
999+
"ttl=v; max-age=3600; path=/; SameSite=Lax",
1000+
c.Res().Get(HeaderSetCookie),
1001+
)
1002+
}
1003+
1004+
// go test -run Test_Ctx_Cookie_StrictPartitioned
1005+
func Test_Ctx_Cookie_StrictPartitioned(t *testing.T) {
1006+
t.Parallel()
1007+
app := New()
1008+
c := app.AcquireCtx(&fasthttp.RequestCtx{})
1009+
1010+
ck := &Cookie{
1011+
Name: "sp",
1012+
Value: "v",
1013+
Secure: true,
1014+
SameSite: CookieSameSiteStrictMode,
1015+
Partitioned: true,
1016+
}
1017+
c.Res().Cookie(ck)
1018+
1019+
require.Equal(t,
1020+
"sp=v; path=/; secure; SameSite=Strict; Partitioned",
1021+
c.Res().Get(HeaderSetCookie),
1022+
)
1023+
}
1024+
9231025
// go test -v -run=^$ -bench=Benchmark_Ctx_Cookie -benchmem -count=4
9241026
func Benchmark_Ctx_Cookie(b *testing.B) {
9251027
app := New()

0 commit comments

Comments
 (0)