feat: fallback to SameSite Lax mode if cookie is not secure (#1105)
Also, will allow to set cookie `SameSite` mode on command line or environment. Note that `None` mode will be forced to ``Lax`` if cookie is set to not be secure. Signed-off-by: Valentin Lab <valentin.lab@kalysto.org>
This commit is contained in:
parent
401e18f29f
commit
29ae2a4b87
6 changed files with 94 additions and 2 deletions
|
|
@ -56,6 +56,7 @@ var (
|
||||||
forcedLanguage = flag.String("forced-language", "", "if set, this language is being used instead of the one from the request's Accept-Language header")
|
forcedLanguage = flag.String("forced-language", "", "if set, this language is being used instead of the one from the request's Accept-Language header")
|
||||||
hs512Secret = flag.String("hs512-secret", "", "secret used to sign JWTs, uses ed25519 if not set")
|
hs512Secret = flag.String("hs512-secret", "", "secret used to sign JWTs, uses ed25519 if not set")
|
||||||
cookieSecure = flag.Bool("cookie-secure", true, "if true, sets the secure flag on Anubis cookies")
|
cookieSecure = flag.Bool("cookie-secure", true, "if true, sets the secure flag on Anubis cookies")
|
||||||
|
cookieSameSite = flag.String("cookie-same-site", "None", "sets the same site option on Anubis cookies, will auto-downgrade None to Lax if cookie-secure is false. Valid values are None, Lax, Strict, and Default.")
|
||||||
ed25519PrivateKeyHex = flag.String("ed25519-private-key-hex", "", "private key used to sign JWTs, if not set a random one will be assigned")
|
ed25519PrivateKeyHex = flag.String("ed25519-private-key-hex", "", "private key used to sign JWTs, if not set a random one will be assigned")
|
||||||
ed25519PrivateKeyHexFile = flag.String("ed25519-private-key-hex-file", "", "file name containing value for ed25519-private-key-hex")
|
ed25519PrivateKeyHexFile = flag.String("ed25519-private-key-hex-file", "", "file name containing value for ed25519-private-key-hex")
|
||||||
metricsBind = flag.String("metrics-bind", ":9090", "network address to bind metrics to")
|
metricsBind = flag.String("metrics-bind", ":9090", "network address to bind metrics to")
|
||||||
|
|
@ -143,6 +144,22 @@ func parseBindNetFromAddr(address string) (string, string) {
|
||||||
return "", address
|
return "", address
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseSameSite(s string) (http.SameSite) {
|
||||||
|
switch strings.ToLower(s) {
|
||||||
|
case "none":
|
||||||
|
return http.SameSiteNoneMode
|
||||||
|
case "lax":
|
||||||
|
return http.SameSiteLaxMode
|
||||||
|
case "strict":
|
||||||
|
return http.SameSiteStrictMode
|
||||||
|
case "default":
|
||||||
|
return http.SameSiteDefaultMode
|
||||||
|
default:
|
||||||
|
log.Fatalf("invalid cookie same-site mode: %s, valid values are None, Lax, Strict, and Default", s)
|
||||||
|
}
|
||||||
|
return http.SameSiteDefaultMode
|
||||||
|
}
|
||||||
|
|
||||||
func setupListener(network string, address string) (net.Listener, string) {
|
func setupListener(network string, address string) (net.Listener, string) {
|
||||||
formattedAddress := ""
|
formattedAddress := ""
|
||||||
|
|
||||||
|
|
@ -432,6 +449,7 @@ func main() {
|
||||||
WebmasterEmail: *webmasterEmail,
|
WebmasterEmail: *webmasterEmail,
|
||||||
OpenGraph: policy.OpenGraph,
|
OpenGraph: policy.OpenGraph,
|
||||||
CookieSecure: *cookieSecure,
|
CookieSecure: *cookieSecure,
|
||||||
|
CookieSameSite: parseSameSite(*cookieSameSite),
|
||||||
PublicUrl: *publicUrl,
|
PublicUrl: *publicUrl,
|
||||||
JWTRestrictionHeader: *jwtRestrictionHeader,
|
JWTRestrictionHeader: *jwtRestrictionHeader,
|
||||||
DifficultyInJWT: *difficultyInJWT,
|
DifficultyInJWT: *difficultyInJWT,
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
|
|
||||||
<!-- This changes the project to: -->
|
<!-- This changes the project to: -->
|
||||||
|
|
||||||
|
- Add `COOKIE_SAME_SITE_MODE` to force anubis cookies SameSite value, and downgrade automatically from `None` to `Lax` if cookie is insecure.
|
||||||
- Fix lock convoy problem in decaymap ([#1103](https://github.com/TecharoHQ/anubis/issues/1103)).
|
- Fix lock convoy problem in decaymap ([#1103](https://github.com/TecharoHQ/anubis/issues/1103)).
|
||||||
- Fix lock convoy problem in bbolt by implementing the actor pattern ([#1103](https://github.com/TecharoHQ/anubis/issues/1103)).
|
- Fix lock convoy problem in bbolt by implementing the actor pattern ([#1103](https://github.com/TecharoHQ/anubis/issues/1103)).
|
||||||
- Document missing environment variables in installation guide: `SLOG_LEVEL`, `COOKIE_PREFIX`, `FORCED_LANGUAGE`, and `TARGET_DISABLE_KEEPALIVE` ([#1086](https://github.com/TecharoHQ/anubis/pull/1086)).
|
- Document missing environment variables in installation guide: `SLOG_LEVEL`, `COOKIE_PREFIX`, `FORCED_LANGUAGE`, and `TARGET_DISABLE_KEEPALIVE` ([#1086](https://github.com/TecharoHQ/anubis/pull/1086)).
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,7 @@ Anubis uses these environment variables for configuration:
|
||||||
| `COOKIE_PARTITIONED` | `false` | If set to `true`, enables the [partitioned (CHIPS) flag](https://developers.google.com/privacy-sandbox/cookies/chips), meaning that Anubis inside an iframe has a different set of cookies than the domain hosting the iframe. |
|
| `COOKIE_PARTITIONED` | `false` | If set to `true`, enables the [partitioned (CHIPS) flag](https://developers.google.com/privacy-sandbox/cookies/chips), meaning that Anubis inside an iframe has a different set of cookies than the domain hosting the iframe. |
|
||||||
| `COOKIE_PREFIX` | `anubis-cookie` | The prefix used for browser cookies created by Anubis. Useful for customization or avoiding conflicts with other applications. |
|
| `COOKIE_PREFIX` | `anubis-cookie` | The prefix used for browser cookies created by Anubis. Useful for customization or avoiding conflicts with other applications. |
|
||||||
| `COOKIE_SECURE` | `true` | If set to `true`, enables the [Secure flag](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Cookies#block_access_to_your_cookies), meaning that the cookies will only be transmitted over HTTPS. If Anubis is used in an unsecure context (plain HTTP), this will be need to be set to false |
|
| `COOKIE_SECURE` | `true` | If set to `true`, enables the [Secure flag](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Cookies#block_access_to_your_cookies), meaning that the cookies will only be transmitted over HTTPS. If Anubis is used in an unsecure context (plain HTTP), this will be need to be set to false |
|
||||||
|
| `COOKIE_SAME_SITE` | `None` | Controls the cookie’s [`SameSite` attribute](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value). Allowed: `None`, `Lax`, `Strict`, `Default`. `None` permits cross-site use but modern browsers require it to be **Secure**—so if `COOKIE_SECURE=false` or you serve over plain HTTP, use `Lax` (recommended) or `Strict` or the cookie will be rejected. `Default` uses the Go runtime’s `SameSiteDefaultMode`. `None` will be downgraded to `Lax` automatically if cookie is set NOT to be secure. |
|
||||||
| `DIFFICULTY` | `4` | The difficulty of the challenge, or the number of leading zeroes that must be in successful responses. |
|
| `DIFFICULTY` | `4` | The difficulty of the challenge, or the number of leading zeroes that must be in successful responses. |
|
||||||
| `DIFFICULTY_IN_JWT` | `false` | If set to `true`, adds the `difficulty` field into JWT claims, which indicates the difficulty the token has been generated. This may be useful for statistics and debugging. |
|
| `DIFFICULTY_IN_JWT` | `false` | If set to `true`, adds the `difficulty` field into JWT claims, which indicates the difficulty the token has been generated. This may be useful for statistics and debugging. |
|
||||||
| `ED25519_PRIVATE_KEY_HEX` | unset | The hex-encoded ed25519 private key used to sign Anubis responses. If this is not set, Anubis will generate one for you. This should be exactly 64 characters long. **Required when using persistent storage backends** (like bbolt) to ensure challenges survive service restarts. When running multiple instances on the same base domain, the key must be the same across all instances. See below for details. |
|
| `ED25519_PRIVATE_KEY_HEX` | unset | The hex-encoded ed25519 private key used to sign Anubis responses. If this is not set, Anubis will generate one for you. This should be exactly 64 characters long. **Required when using persistent storage backends** (like bbolt) to ensure challenges survive service restarts. When running multiple instances on the same base domain, the key must be the same across all instances. See below for details. |
|
||||||
|
|
|
||||||
|
|
@ -299,6 +299,7 @@ func TestCookieSettings(t *testing.T) {
|
||||||
CookieDomain: "127.0.0.1",
|
CookieDomain: "127.0.0.1",
|
||||||
CookiePartitioned: true,
|
CookiePartitioned: true,
|
||||||
CookieSecure: true,
|
CookieSecure: true,
|
||||||
|
CookieSameSite: http.SameSiteNoneMode,
|
||||||
CookieExpiration: anubis.CookieDefaultExpirationTime,
|
CookieExpiration: anubis.CookieDefaultExpirationTime,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -339,6 +340,65 @@ func TestCookieSettings(t *testing.T) {
|
||||||
if ckie.Secure != srv.opts.CookieSecure {
|
if ckie.Secure != srv.opts.CookieSecure {
|
||||||
t.Errorf("wanted secure flag %v, got: %v", srv.opts.CookieSecure, ckie.Secure)
|
t.Errorf("wanted secure flag %v, got: %v", srv.opts.CookieSecure, ckie.Secure)
|
||||||
}
|
}
|
||||||
|
if ckie.SameSite != srv.opts.CookieSameSite {
|
||||||
|
t.Errorf("wanted same site option %v, got: %v", srv.opts.CookieSameSite, ckie.SameSite)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCookieSettingsSameSiteNoneModeDowngradedToLaxWhenUnsecure(t *testing.T) {
|
||||||
|
pol := loadPolicies(t, "testdata/zero_difficulty.yaml", 0)
|
||||||
|
|
||||||
|
srv := spawnAnubis(t, Options{
|
||||||
|
Next: http.NewServeMux(),
|
||||||
|
Policy: pol,
|
||||||
|
|
||||||
|
CookieDomain: "127.0.0.1",
|
||||||
|
CookiePartitioned: true,
|
||||||
|
CookieSecure: false,
|
||||||
|
CookieSameSite: http.SameSiteNoneMode,
|
||||||
|
CookieExpiration: anubis.CookieDefaultExpirationTime,
|
||||||
|
})
|
||||||
|
|
||||||
|
ts := httptest.NewServer(internal.RemoteXRealIP(true, "tcp", srv))
|
||||||
|
defer ts.Close()
|
||||||
|
|
||||||
|
cli := httpClient(t)
|
||||||
|
chall := makeChallenge(t, ts, cli)
|
||||||
|
|
||||||
|
resp := handleChallengeZeroDifficulty(t, ts, cli, chall)
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusFound {
|
||||||
|
resp.Write(os.Stderr)
|
||||||
|
t.Errorf("wanted %d, got: %d", http.StatusFound, resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var ckie *http.Cookie
|
||||||
|
for _, cookie := range resp.Cookies() {
|
||||||
|
t.Logf("%#v", cookie)
|
||||||
|
if cookie.Name == anubis.CookieName {
|
||||||
|
ckie = cookie
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ckie == nil {
|
||||||
|
t.Errorf("Cookie %q not found", anubis.CookieName)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ckie.Domain != "127.0.0.1" {
|
||||||
|
t.Errorf("cookie domain is wrong, wanted 127.0.0.1, got: %s", ckie.Domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ckie.Partitioned != srv.opts.CookiePartitioned {
|
||||||
|
t.Errorf("wanted partitioned flag %v, got: %v", srv.opts.CookiePartitioned, ckie.Partitioned)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ckie.Secure != srv.opts.CookieSecure {
|
||||||
|
t.Errorf("wanted secure flag %v, got: %v", srv.opts.CookieSecure, ckie.Secure)
|
||||||
|
}
|
||||||
|
if ckie.SameSite != http.SameSiteLaxMode {
|
||||||
|
t.Errorf("wanted same site Lax option %v, got: %v", http.SameSiteLaxMode, ckie.SameSite)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCheckDefaultDifficultyMatchesPolicy(t *testing.T) {
|
func TestCheckDefaultDifficultyMatchesPolicy(t *testing.T) {
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ type Options struct {
|
||||||
OpenGraph config.OpenGraph
|
OpenGraph config.OpenGraph
|
||||||
ServeRobotsTXT bool
|
ServeRobotsTXT bool
|
||||||
CookieSecure bool
|
CookieSecure bool
|
||||||
|
CookieSameSite http.SameSite
|
||||||
Logger *slog.Logger
|
Logger *slog.Logger
|
||||||
PublicUrl string
|
PublicUrl string
|
||||||
JWTRestrictionHeader string
|
JWTRestrictionHeader string
|
||||||
|
|
|
||||||
15
lib/http.go
15
lib/http.go
|
|
@ -56,6 +56,8 @@ func (s *Server) SetCookie(w http.ResponseWriter, cookieOpts CookieOpts) {
|
||||||
var domain = s.opts.CookieDomain
|
var domain = s.opts.CookieDomain
|
||||||
var name = anubis.CookieName
|
var name = anubis.CookieName
|
||||||
var path = "/"
|
var path = "/"
|
||||||
|
var sameSite = s.opts.CookieSameSite
|
||||||
|
|
||||||
if cookieOpts.Name != "" {
|
if cookieOpts.Name != "" {
|
||||||
name = cookieOpts.Name
|
name = cookieOpts.Name
|
||||||
}
|
}
|
||||||
|
|
@ -72,11 +74,15 @@ func (s *Server) SetCookie(w http.ResponseWriter, cookieOpts CookieOpts) {
|
||||||
cookieOpts.Expiry = s.opts.CookieExpiration
|
cookieOpts.Expiry = s.opts.CookieExpiration
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if s.opts.CookieSameSite == http.SameSiteNoneMode && !s.opts.CookieSecure {
|
||||||
|
sameSite = http.SameSiteLaxMode
|
||||||
|
}
|
||||||
|
|
||||||
http.SetCookie(w, &http.Cookie{
|
http.SetCookie(w, &http.Cookie{
|
||||||
Name: name,
|
Name: name,
|
||||||
Value: cookieOpts.Value,
|
Value: cookieOpts.Value,
|
||||||
Expires: time.Now().Add(cookieOpts.Expiry),
|
Expires: time.Now().Add(cookieOpts.Expiry),
|
||||||
SameSite: http.SameSiteNoneMode,
|
SameSite: sameSite,
|
||||||
Domain: domain,
|
Domain: domain,
|
||||||
Secure: s.opts.CookieSecure,
|
Secure: s.opts.CookieSecure,
|
||||||
Partitioned: s.opts.CookiePartitioned,
|
Partitioned: s.opts.CookiePartitioned,
|
||||||
|
|
@ -88,6 +94,8 @@ func (s *Server) ClearCookie(w http.ResponseWriter, cookieOpts CookieOpts) {
|
||||||
var domain = s.opts.CookieDomain
|
var domain = s.opts.CookieDomain
|
||||||
var name = anubis.CookieName
|
var name = anubis.CookieName
|
||||||
var path = "/"
|
var path = "/"
|
||||||
|
var sameSite = s.opts.CookieSameSite
|
||||||
|
|
||||||
if cookieOpts.Name != "" {
|
if cookieOpts.Name != "" {
|
||||||
name = cookieOpts.Name
|
name = cookieOpts.Name
|
||||||
}
|
}
|
||||||
|
|
@ -99,13 +107,16 @@ func (s *Server) ClearCookie(w http.ResponseWriter, cookieOpts CookieOpts) {
|
||||||
domain = etld
|
domain = etld
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if s.opts.CookieSameSite == http.SameSiteNoneMode && !s.opts.CookieSecure {
|
||||||
|
sameSite = http.SameSiteLaxMode
|
||||||
|
}
|
||||||
|
|
||||||
http.SetCookie(w, &http.Cookie{
|
http.SetCookie(w, &http.Cookie{
|
||||||
Name: name,
|
Name: name,
|
||||||
Value: "",
|
Value: "",
|
||||||
MaxAge: -1,
|
MaxAge: -1,
|
||||||
Expires: time.Now().Add(-1 * time.Minute),
|
Expires: time.Now().Add(-1 * time.Minute),
|
||||||
SameSite: http.SameSiteNoneMode,
|
SameSite: sameSite,
|
||||||
Partitioned: s.opts.CookiePartitioned,
|
Partitioned: s.opts.CookiePartitioned,
|
||||||
Domain: domain,
|
Domain: domain,
|
||||||
Secure: s.opts.CookieSecure,
|
Secure: s.opts.CookieSecure,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue