Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,12 @@ Note that 0.3.3 was never released; 0.3.4 follows 0.3.2.
like a miss, `set()` dropped the write, `increment()` returned 0 and
`delete_if_equals()` raised `TypeError`. A failed server is now taken out of
rotation at once and tried again after one second, instead of after 60.
([#197](https://github.com/allen0099/FastAPI-CacheX/issues/197))
([#197](https://github.com/allen0099/FastAPI-CacheX/issues/197))- **`SessionConfig` warns when `cookie_same_site="none"` is set without
`cookie_https_only=True`.** Browsers reject a `SameSite=None` cookie that is
not `Secure`, so the session cookie was silently never stored. The
combination is still accepted.
([#167](https://github.com/allen0099/FastAPI-CacheX/issues/167))


## [0.3.7] - 2026-09-25

Expand Down
2 changes: 1 addition & 1 deletion docs/SESSION.md
Original file line number Diff line number Diff line change
Expand Up @@ -418,7 +418,7 @@ SessionConfig(
* 60
* 60, # None = no Max-Age (cookie ends with the browser session)
cookie_path="/",
cookie_same_site="lax", # "lax" / "strict" / "none"
cookie_same_site="lax", # "lax" / "strict" / "none" ("none" needs cookie_https_only=True)
cookie_https_only=False, # True adds the Secure flag
cookie_domain=None, # None = no Domain attribute
)
Expand Down
19 changes: 19 additions & 0 deletions fastapi_cachex/session/config.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Session configuration settings."""

import ipaddress
import warnings
from functools import lru_cache
from typing import Literal

Expand All @@ -9,6 +10,7 @@
from pydantic import Field
from pydantic import SecretStr
from pydantic import field_validator
from pydantic import model_validator

SameSitePolicy = Literal["lax", "strict", "none"]

Expand Down Expand Up @@ -224,6 +226,23 @@ def is_trusted_proxy(self, address: str) -> bool:
return True
return False

@model_validator(mode="after")
def _warn_insecure_same_site_none(self) -> "SessionConfig":
"""Warn about a SameSite=None cookie without the Secure flag.

Browsers drop such a cookie, so the session would silently never stick.
Rejecting the combination would break existing configurations.
"""
if self.cookie_same_site == "none" and not self.cookie_https_only:
warnings.warn(
'cookie_same_site="none" requires cookie_https_only=True: browsers '
"reject a SameSite=None cookie without the Secure flag, so the "
"session cookie would never be stored.",
UserWarning,
stacklevel=3,
)
return self

@field_validator("jwt_algorithm")
@classmethod
def _check_jwt_algorithm(cls, value: str) -> str:
Expand Down
2 changes: 1 addition & 1 deletion i18n/zh-TW/docs/SESSION.md
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@ SessionConfig(
* 60
* 60, # None = 不設 Max-Age(Cookie 隨瀏覽器工作階段結束)
cookie_path="/",
cookie_same_site="lax", # "lax" / "strict" / "none"
cookie_same_site="lax", # "lax" / "strict" / "none"("none" 需要 cookie_https_only=True)
cookie_https_only=False, # True 會加上 Secure 旗標
cookie_domain=None, # None = 不設 Domain 屬性
)
Expand Down
22 changes: 22 additions & 0 deletions tests/session/test_config.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Tests for SessionConfig validation."""

import warnings

import pytest
from pydantic import ValidationError

Expand All @@ -24,3 +26,23 @@ def test_session_config_rejects_unknown_fields() -> None:

with pytest.raises(ValidationError):
SessionConfig(secret_key="a" * 32, enable_csrf=True) # type: ignore[call-arg]


def test_same_site_none_without_https_only_warns() -> None:
"""Browsers drop a SameSite=None cookie that is not Secure (#167)."""
with pytest.warns(UserWarning, match='cookie_same_site="none"'):
SessionConfig(secret_key="a" * 32, cookie_same_site="none")


@pytest.mark.parametrize(
("same_site", "https_only"),
[("none", True), ("lax", False), ("strict", False)],
)
def test_other_cookie_settings_do_not_warn(same_site, https_only) -> None:
with warnings.catch_warnings():
warnings.simplefilter("error")
SessionConfig(
secret_key="a" * 32,
cookie_same_site=same_site,
cookie_https_only=https_only,
)
Loading