Skip to content

fix(lib): default CookieExpiration to CookieDefaultExpirationTime when zero#1586

Open
crishoj wants to merge 1 commit into
TecharoHQ:mainfrom
crishoj:fix/cookie-expiration-default
Open

fix(lib): default CookieExpiration to CookieDefaultExpirationTime when zero#1586
crishoj wants to merge 1 commit into
TecharoHQ:mainfrom
crishoj:fix/cookie-expiration-default

Conversation

@crishoj
Copy link
Copy Markdown

@crishoj crishoj commented Apr 25, 2026

Options.CookieExpiration time.Duration has a zero value of 0. When left unset, both the cookie's Expires attribute and the JWT's exp claim are computed as time.Now().Add(0) = now:

// lib/http.go SetCookie:
Expires: time.Now().Add(cookieOpts.Expiry)
// cookieOpts.Expiry falls back to s.opts.CookieExpiration when zero
// lib/anubis.go signJWT:
claims["exp"] = time.Now().Add(s.opts.CookieExpiration).Unix()

Result: browser solves PoW, libanubis issues an expired-on-arrival cookie, browser re-requests, libanubis sees no valid cookie, serves the challenge again. Infinite loop.

The Anubis CLI sidesteps this because --cookie-expiration-time (added in #389) carries its own default of anubis.CookieDefaultExpirationTime (7 days). Library consumers that construct Options directly — for example, plugins for other web servers — hit the zero-value bug.

This change applies the same default in lib.New() so the library has the same safe behavior as the CLI.

Test

TestCookieDefaultExpirationFilled verifies:

  1. srv.opts.CookieExpiration is filled to anubis.CookieDefaultExpirationTime after New() when the caller leaves it unset.
  2. End-to-end: the cookie issued by PassChallenge has its Expires attribute well in the future, not "now-ish".

Context

I noticed this while building a Caddy plugin against libanubis. Browser successfully solved the PoW but kept looping back to the challenge page. Workaround was to set CookieExpiration: anubis.CookieDefaultExpirationTime explicitly in the consumer; the proper fix is here, in the library.

Checklist:

  • Added a description of the changes to the [Unreleased] section of docs/docs/CHANGELOG.md
  • Added test cases to the relevant parts of the codebase (TestCookieDefaultExpirationFilled in lib/anubis_test.go)
  • Ran integration tests npm run test:integrationpartial ⚠️
    • chromium + firefox pass
    • webkit fails with Frame.Goto http://[::]:port: playwright: bad URL on every test, pre-existing env/playwright-webkit issue parsing IPv6 unspecified-address URLs (also seen on main)
  • All of my commits have verified signatures

…n zero

Options.CookieExpiration time.Duration has a zero value of 0. When
left unset, both the cookie's Expires attribute and the JWT's exp
claim are computed as time.Now().Add(0) = now:

  http.go SetCookie:
    Expires: time.Now().Add(cookieOpts.Expiry)
    // cookieOpts.Expiry falls back to s.opts.CookieExpiration when zero
  anubis.go signJWT:
    claims["exp"] = time.Now().Add(s.opts.CookieExpiration).Unix()

Result: browser solves PoW, libanubis issues an expired-on-arrival
cookie, browser re-requests, libanubis sees no valid cookie, serves
the challenge again. Infinite loop.

The Anubis CLI is unaffected because the --cookie-expiration-time
flag carries its own default of anubis.CookieDefaultExpirationTime
(7 days). Library consumers that construct Options directly — e.g.
plugins for other web servers — hit the zero-value bug.

Apply the same default in lib.New() so the library has the same safe
behavior as the CLI.

Test verifies both the option round-trip (srv.opts.CookieExpiration
filled to the constant) and the end-to-end cookie Expires attribute
on a real PassChallenge response.

Assisted-by: Claude Opus 4.7 via Claude Code
Signed-off-by: Christian Rishøj <christian@rishoj.net>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant