Skip to content

CookieLogin: client can bypass remember-me TTL by setting expires=0 #125

Closed

Description

@Sakanweb

What steps will reproduce the problem?

Auto-login cookie value is plain JSON: [id, key, expires] (no integrity check on the payload).

CookieLoginMiddleware enforces lifetime only when expires !== 0:

if ($expires !== 0 && $expires < time()) {
    // reject as expired
    return;
}

$this->currentUser->login($identity);

Steps:

  1. Issue a timed cookie via CookieLogin::addCookie() with a non-null DateInterval
    → payload like ["user-1","valid-key",<future-timestamp>] → auth accepted.
  2. Keep the same id and key, set expires to a past timestamp
    → auth rejected (TTL works).
  3. Keep the same id and key, set expires to 0
    → auth accepted again.

So a client who can edit the cookie value turns a finite remember-me cookie into one the server never treats as expired.

Note: browser Set-Cookie Max-Age/Expires is separate. An attacker who sets the cookie themselves can also choose a long browser lifetime; the bug is that server-side TTL in JSON is not binding.

What is the expected result?

If the application issued a finite-duration remember-me cookie, the server must reject auth after that time even if the client changes the expires field (unless the whole payload is integrity-protected or the token is looked up server-side).

What do you get instead?

After validateCookieLoginKey() succeeds, expires === 0 skips the lifetime check. Changing expires to 0 disables that check for an otherwise valid key.

Additional info

Q A
Version yiisoft/user 2.3.2
PHP version 8.3

Relevant code

CookieLoginMiddleware (TTL gate):

if ($expires !== 0 && $expires < time()) {
    $this->logger->warning('Unable to authenticate user by cookie. Lifetime has expired.');
    return;
}

$this->currentUser->login($identity);

CookieLogin::addCookie() (writes 0 for session-cookie mode, otherwise a timestamp; value is json_encode of [id, key, expires] with no MAC):

$data = [$identity->getId(), $identity->getCookieLoginKey()];

if ($duration !== null) {
    $expires = (new DateTimeImmutable())->add($duration);
    $data[] = $expires->getTimestamp();
} else {
    $expires = null;
    $data[] = 0;
}

$cookieValue = json_encode($data, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);

Suggested fix

Make the lifetime field unforgeable, e.g.:

  • MAC/sign at least id + expires (or the full payload) with a server secret, or
  • store an opaque token + TTL server-side and put only the token in the cookie.

Rotating getCookieLoginKey() still revokes cookies, but it does not make a client-editable expires a real TTL by itself.

Activity

  1. vjik commented on Aug 16, 2026

    @vjik
    Member

    Protecting a cookie's value against client-side tampering is not something yiisoft/user implements itself — it's already handled by yiisoft/cookies that provides CookieSigner / CookieEncryptor and a CookieMiddleware that signs/encrypts outgoing Set-Cookie values and validates/decrypts incoming ones by cookie name.

    It would be good to add a note about this to the README's "Auto login through cookie" section.

  2. Sakanweb commented on Aug 16, 2026

    @Sakanweb
    Author

    Thanks - documenting this via yiisoft/cookies is a good approach. PR looks good.

  3. samdark commented on Aug 30, 2026

    @samdark
    Member

    The bad thing about #126 is that it stays insecure by default and it doesn't error. It's silent.

    We can make it secure by default by introducing built-in HMAC signing of the value right into this package. The drawback — while configuring CookieMiddleware to sign cookies, we should exclude our cookie or else it silently breaks auto-login functionality. Also, it is a backwards compatibility break to introduce it.

  4. vjik commented on Sep 1, 2026

    @vjik
    Member

    The bad thing about #126 is that it stays insecure by default and it doesn't error. It's silent.

    We can make it secure by default by introducing built-in HMAC signing of the value right into this package. The drawback — while configuring CookieMiddleware to sign cookies, we should exclude our cookie or else it silently breaks auto-login functionality. Also, it is a backwards compatibility break to introduce it.

    It is implemented as optional signing: #126
    In the major version, we will make the $signatureKey specification mandatory so that the user makes an informed choice.

  5. klsoft-web commented on Sep 1, 2026

    @klsoft-web
    Contributor

    It would be better to use the existing functionality than to create a duplicate:

    use Yiisoft\Cookies\CookieEncryptor;
    use Yiisoft\Cookies\CookieSigner;
    
    final class CookieLogin
    {
        // ...
        public function __construct(
            private readonly ?DateInterval $duration = null,
            private readonly ?CookieEncryptor $encryptor = null,
            private readonly ?CookieSigner $signer = null
        ) {}
        
        public function addCookie(
            CookieLoginIdentityInterface $identity,
            ResponseInterface $response,
            DateInterval|false|null $duration = false,
        ): ResponseInterface {
           // ...
           $cookie = new Cookie(name: $this->cookieName, value: $cookieValue, expires: $expires);
           if($this->signer !== null)
           {
               $cookie = $this->signer->sign($cookie);
           }
           if($this->encryptor !== null)
           {
                 $cookie = $this->encryptor->encrypt($cookie);
           }
           
           return $cookie->addToResponse($response);
        }
    }

    Then add this to CookieLoginMiddleware:

    final class CookieLoginMiddleware implements MiddlewareInterface
    {
        // ...
        private function authenticateUserByCookieFromRequest(ServerRequestInterface $request): void
        {
           // ...
           if($this->cookieLogin->encryptor !== null)
           {
                 $cookie = $this->encryptor->decrypt(new Cookie($cookieName, $cookies[$cookieName]));
                 $cookies[$cookieName] = $cookie->getValue();
           }
           if($this->cookieLogin->signer !== null)
           {
               $cookie = $this->cookieLogin->signer->validate(new Cookie($cookieName, $cookies[$cookieName]));
               $cookies[$cookieName] = $cookie->getValue();
           }
        }
    }
  6. vjik commented on Sep 2, 2026

    @vjik
    Member

    It would be better to use the existing functionality than to create a duplicate:

    This is a fairly simple functionality, so I prefer remove yiisoft/cookies dependency (see #134)

  7. klsoft-web commented on Sep 2, 2026

    @klsoft-web
    Contributor

    It would be better to use the existing functionality than to create a duplicate:

    This is a fairly simple functionality, so I prefer remove yiisoft/cookies dependency (see #134)

    It not clear from the #134 issue what exactly you plan to do beside remove yiisoft/cookies dependency.
    I'm wondering whether classes CookieLogin, CookieLoginIdentityInterface and CookieLoginMiddleware have been removed too?

  8. vjik commented on Sep 2, 2026

    @vjik
    Member

    It not clear from the #134 issue what exactly you plan to do beside remove yiisoft/cookies dependency. I'm wondering whether classes CookieLogin, CookieLoginIdentityInterface and CookieLoginMiddleware have been removed too?

    Implement cookie creating into yiisoft/user without yiisoft/cookie usage. CookieLogin, CookieLoginIdentityInterface, and CookieLoginMiddleware will remain.

  9. klsoft-web commented on Sep 2, 2026

    @klsoft-web
    Contributor
    1. Users of the yiisoft/cookies package may have encountered a configuration issue when signing and/or encrypting cookies, as both the yiisoft/cookies and yiisoft/user packages require a key. However, the yiisoft/cookies package allows you to use the same key for both.
    2. Using Yiisoft\Cookies\CookieEncryptor for encryption in addition to signing would be a good option.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions