Test_Session_ExtractorPreservation verifies that the extractor which actually supplied the incoming session ID drives the write-back decision, preventing a session ID read from a read-only source (query/form/param) from being promoted into cookies/headers for an existing session (session fixation).
(t *testing.T)
| 708 | // a session ID read from a read-only source (query/form/param) from being |
| 709 | // promoted into cookies/headers for an existing session (session fixation). |
| 710 | func Test_Session_ExtractorPreservation(t *testing.T) { |
| 711 | t.Parallel() |
| 712 | |
| 713 | t.Run("read-only winner is not promoted for existing session", func(t *testing.T) { |
| 714 | t.Parallel() |
| 715 | // Chain prefers the cookie, but also accepts a read-only query source. |
| 716 | store := NewStore(Config{ |
| 717 | Extractor: extractors.Chain(extractors.FromCookie("session_id"), extractors.FromQuery("session_id")), |
| 718 | }) |
| 719 | app := fiber.New() |
| 720 | |
| 721 | // First request: create and persist a session (written to the cookie). |
| 722 | ctx1 := app.AcquireCtx(&fasthttp.RequestCtx{}) |
| 723 | sess, err := store.Get(ctx1) |
| 724 | require.NoError(t, err) |
| 725 | sess.Set("name", "john") |
| 726 | require.NoError(t, sess.Save()) |
| 727 | id := sess.ID() |
| 728 | require.NotNil(t, ctx1.Response().Header.PeekCookie("session_id")) |
| 729 | sess.Release() |
| 730 | app.ReleaseCtx(ctx1) |
| 731 | |
| 732 | // Second request: the existing ID is provided only via the query string |
| 733 | // (an attacker-controllable, read-only source). |
| 734 | ctx2 := app.AcquireCtx(&fasthttp.RequestCtx{}) |
| 735 | defer app.ReleaseCtx(ctx2) |
| 736 | ctx2.Request().SetRequestURI("/path?session_id=" + id) |
| 737 | |
| 738 | sess2, err := store.Get(ctx2) |
| 739 | require.NoError(t, err) |
| 740 | require.False(t, sess2.Fresh(), "existing session must not be fresh") |
| 741 | require.Equal(t, id, sess2.ID()) |
| 742 | require.NoError(t, sess2.Save()) |
| 743 | |
| 744 | // The read-only ID must NOT be promoted into a cookie/header. |
| 745 | require.Nil(t, ctx2.Response().Header.PeekCookie("session_id")) |
| 746 | require.Empty(t, string(ctx2.Response().Header.Peek("session_id"))) |
| 747 | sess2.Release() |
| 748 | }) |
| 749 | |
| 750 | t.Run("fresh session still persists to writable sink", func(t *testing.T) { |
| 751 | t.Parallel() |
| 752 | store := NewStore(Config{ |
| 753 | Extractor: extractors.Chain(extractors.FromCookie("session_id"), extractors.FromQuery("session_id")), |
| 754 | }) |
| 755 | app := fiber.New() |
| 756 | |
| 757 | // The query supplies an ID that does not exist in storage, so a fresh |
| 758 | // session with a freshly generated ID is created. |
| 759 | ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) |
| 760 | defer app.ReleaseCtx(ctx) |
| 761 | ctx.Request().SetRequestURI("/path?session_id=does-not-exist") |
| 762 | |
| 763 | sess, err := store.Get(ctx) |
| 764 | require.NoError(t, err) |
| 765 | require.True(t, sess.Fresh(), "missing data must yield a fresh session") |
| 766 | require.NotEqual(t, "does-not-exist", sess.ID(), "fresh session must use a generated ID") |
| 767 | require.NoError(t, sess.Save()) |
nothing calls this directly
no test coverage detected