Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CookieJar.setCookie does not slide expiration for set-cookie headers with max-age #154

Closed
jstewmon opened this issue Apr 25, 2019 · 12 comments · Fixed by #345
Closed

CookieJar.setCookie does not slide expiration for set-cookie headers with max-age #154

jstewmon opened this issue Apr 25, 2019 · 12 comments · Fixed by #345
Assignees

Comments

@jstewmon
Copy link
Contributor

The creation time from old-cookie is carried forward per the spec, but this causes the expiryTime() to be unaffected by subsequent set-cookie headers, so the cookie will expire when its creation + max-age > now, even if subsequent set-cookie headers specified max-age with the intent of making the expiration now + max-age.

I'm currently working around this by just having a Store implementation that passes null to the callback, so that the creation is not carried forward.

@stash
Copy link
Collaborator

stash commented May 6, 2019

I reviewed the code and the RFC and I am confident that what tough-cookie is doing is the standard behaviour. I don't think there's a bug to fix here so I'd like to close this issue.

In both the code and in the RFC, creation time doesn't get changed when updating a cookie. RFC 6265 Section 5.3 defines the logic for storing a cookie and step 11.3 in particular describes what happens if a cookie is being updated. For convenience:

   11.  If the cookie store contains a cookie with the same name,
        domain, and path as the newly created cookie:

        1.  Let old-cookie be the existing cookie with the same name,
            domain, and path as the newly created cookie.  (Notice that
            this algorithm maintains the invariant that there is at most
            one such cookie.)

        2.  If the newly created cookie was received from a "non-HTTP"
            API and the old-cookie's http-only-flag is set, abort these
            steps and ignore the newly created cookie entirely.

        3.  Update the creation-time of the newly created cookie to
            match the creation-time of the old-cookie.

        4.  Remove the old-cookie from the cookie store.

And in the setCookie() code:

    if (oldCookie) {
      // S5.3 step 11 - "If the cookie store contains a cookie with the same name,
      // domain, and path as the newly created cookie:"
      if (options.http === false && oldCookie.httpOnly) { // step 11.2
        err = new Error("old Cookie is HttpOnly and this isn't an HTTP API");
        return cb(options.ignoreError ? null : err);
      }
      cookie.creation = oldCookie.creation; // step 11.3
      cookie.creationIndex = oldCookie.creationIndex; // preserve tie-breaker
      cookie.lastAccessed = now;
      // Step 11.4 (delete cookie) is implied by just setting the new one:
      store.updateCookie(oldCookie, cookie, next); // step 12


    } else {
      cookie.creation = cookie.lastAccessed = now;
      store.putCookie(cookie, next); // step 12
    }
  }

You can see that the creation time is deliberately not updated. Therefore, Max-Age really only takes effect on creation. Another way to put it is that (in the absence of an Expires cookie-attribute), the expiry for Max-Age cookies is always creation + max-age and never now + max-age.

I don't think there's a bug to fix since tough-cookie is following the RFC. Still, if you can demonstrate that there are browsers that are following the behaviour you describe (i.e., the now + max-age sliding expiry), then I'd accept a pull request that adds this as a non-default option to setCookie.

@stash stash closed this as completed May 6, 2019
@stash stash self-assigned this May 6, 2019
@jstewmon
Copy link
Contributor Author

jstewmon commented May 6, 2019

Sorry, I should have mentioned that I was reporting the issue because the spec doesn't mention the affect of max-age on updates, but browsers treat it as a sliding expiration.

Chrome, Firefox, and I suspect all other browsers, will update the expiration every time a response contains a set-cookie with max-age.

All you need to see for yourself is a server that sends a cookie with max-age. Open the dev inspector in your browser of choice and observe the expiration of the cookie change every time you refresh.

const http = require("http");
http
  .createServer((req, res) => {
    res.setHeader("set-cookie", "foo=bar;max-age=30");
    res.end("check cookies");
  })
  .listen(3000);

@jstewmon
Copy link
Contributor Author

@awaterma while you’re looking through my PRs, would you mind taking a look at this too? I still think it is a valid issue.

@tennox
Copy link

tennox commented Sep 4, 2020

I also see this problematic behaviour.
My Server sets a Cookie into Insomnia's Cookie jar, the next day I query it again - the creation time is not updated, and thus I can't use the cookie in template variables, as it won't pass the expiry check.

In a browser, the old cookie's creation time is updated, as @jstewmon mentioned

@ghost
Copy link

ghost commented Mar 24, 2021

My reading of RFC6265 is that expiry-time and creation-time are two distinct fields (Section 5.3), and creation-time is not used for expiration at all. Steps 5.3.2 and 5.3.11.3 cover creation-time and expiry-time is covered by 5.3.3, and sections 5.2.1 and 5.2.2.

@ricellis
Copy link

We've just run into this problem too. I agree that the spec is a little ambiguous on this point because it doesn't explicitly describe the behavior of Max-Age when a cookie is updated. However, as pointed out by @mehvvy section 5.2.2 seems to cover this when describing parsing of the Set-Cookie header; it says:

... Otherwise, let the
expiry-time be the current date and time plus delta-seconds seconds.

Append an attribute to the cookie-attribute-list with an attribute
-name of Max-Age and an attribute-value of expiry-time.

This seems to suggest that Max-Age is added to the current time when the Set-Cookie header is parsed, so in that context a sliding update of the expiry-time attribute-value of an existing cookie when parsing a new Set-Cookie header probably makes sense.

@ricellis
Copy link

@ShivanKaul are you looking into this or any chance this is going to be re-opened?

@ShivanKaul
Copy link
Contributor

@ricellis I recently left Salesforce and sorting out my contribution plans, I'll look at this soon.

@byroncoetsee
Copy link

Experiencing this too.
Also, because storage can't be accessed from the cookieJar object, one can't use handy functions like updateCookie or removeCookie to get around the problem.

Any ideas or updates? Maybe this has been solved since and I'm missing something?

@awaterma
Copy link
Member

We'll discuss this in our next meeting of the team; I'll try my best to look into this before hand. :)

Thanks for asking about this @byroncoetsee and @ricellis

@byroncoetsee
Copy link

For anyone landing here, and looking for a simple workaround while this issue gets looked at, here's what I did.
NOTE: Doing it this way will drop things like creationIndex etc. It essentially recreates the cookie from scratch - and in doing so, sets the creationDate to now.

class CustomMemoryCookieStore extends MemoryCookieStore {
    updateCookie(oldCookie: Cookie, newCookie: Cookie, cb: (err: Error | null) => void): void
    updateCookie(oldCookie: Cookie, newCookie: Cookie): Promise<void>;
    updateCookie(_: any, newCookie: any, cb?: any): void | Promise<void> {
        newCookie.creation = new Date()
        this.putCookie(newCookie, cb)
        if (cb == null) {
            return Promise.resolve()
        }
    }
}

@VelynM
Copy link

VelynM commented Dec 5, 2023

My reading of RFC6265 is that expiry-time and creation-time are two distinct fields (Section 5.3), and creation-time is not used for expiration at all. Steps 5.3.2 and 5.3.11.3 cover creation-time and expiry-time is covered by 5.3.3, and sections 5.2.1 and 5.2.2.

As far as I can tell, this issue still exists, assuming my past interpretation of RFC6265 was correct.

colincasey added a commit that referenced this issue Dec 13, 2023
The `cookie.expiryTime()` method was calculating an `expiry-time` value using `creation-time + max-age` if the `max-age` value was set. This is not correct as RFC6265 does not say anything about `creation-time` having any effect on `max-age`.

The main issue is that `max-age` should be stored as a date (see text below), not an offset number as it currently is implemented.

> 5.2.2.  The Max-Age Attribute
>
>     If the attribute-name case-insensitively matches the string "Max-
>     Age", the user agent MUST process the cookie-av as follows.
>
>     If the first character of the attribute-value is not a DIGIT or a "-"
>     character, ignore the cookie-av.
>
>     If the remainder of attribute-value contains a non-DIGIT character,
>     ignore the cookie-av.
>
>     Let delta-seconds be the attribute-value converted to an integer.
>
>     If delta-seconds is less than or equal to zero (0), let expiry-time
>     be the earliest representable date and time.  Otherwise, let the
>     expiry-time be the current date and time plus delta-seconds seconds.
>
>     Append an attribute to the cookie-attribute-list with an attribute-
>     name of Max-Age and an attribute-value of expiry-time.

Because `max-age` is an offset, the `expiry-time` now needs to be calculated from "some" point in time and `creation-time` was chosen. In many cases this works fine but, if you have an existing cookie stored with a `max-age` attribute and then update it, the `expiry-time` will be incorrectly report the previous value's `expiry-time`. This is due to the following rule regarding `creation-time` from the spec:

> 5.3.  Storage Model
>
>     11.  If the cookie store contains a cookie with the same name,
>          domain, and path as the newly created cookie:
>
>          1.  Let old-cookie be the existing cookie with the same name,
>              domain, and path as the newly created cookie.  (Notice that
>              this algorithm maintains the invariant that there is at most
>              one such cookie.)
>
>          2.  If the newly created cookie was received from a "non-HTTP"
>              API and the old-cookie's http-only-flag is set, abort these
>              steps and ignore the newly created cookie entirely.
>
>          3.  Update the creation-time of the newly created cookie to
>              match the creation-time of the old-cookie.
>
>          4.  Remove the old-cookie from the cookie store.

This should make it clear that `creation-time` should not be used to calculate the `expiry-time`. Given that we still need to calculate the `expiry-time` due to how `max-age` is stored, the better value to use in this circumstance is the `last-access-time` which is always updated when `setCookie(...)` is called.

Fixes #343
Fixes #154
wjhsf pushed a commit that referenced this issue Feb 8, 2024
* chore: prepare for summer20 release (#143)

* 0.8.0

* chore: update jest to v25.5.4 (#152)

* chore: upgrade jest to 25.5.4

* 0.9.0 (#154)

* Update stubs for Summer 20 (#150)

* update stubs for Summer 20

* APPLICATION_SCOPE as symbol

* 0.9.1

* chore: update yarn lock file

Co-authored-by: Aliaksandr Papko <apapko@salesforce.com>
Co-authored-by: Trevor <tbliss@salesforce.com>
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 a pull request may close this issue.

8 participants