Remix.run Logo
maxwellg 5 days ago

Cookies are filled with weird gotchas and uncomfortable behavior that works 99.95% of the time. My favorite cookie minefield is cookie shadowing - if you set cookies with the same name but different key properties (domain, path, etc.) you can get multiple near-identical cookies set at once - with no ability for the backend or JS to tell which is which.

Try going to https://example.com/somepath and entering the following into the browser console:

  document.cookie = "foo=a"; 
  document.cookie = "foo=b; domain=.example.com";
  document.cookie = "foo=c; path=/somepath";
  document.cookie
I get

  'foo=c; foo=a; foo=b'
treflop 5 days ago | parent | next [-]

At work, whoever designed our setup put the staging and dev environments on the same domain and the entire massive company has adopted this pattern.

What a colossal mistake.

NavinF 5 days ago | parent | next [-]

Yep. Even within the prod environment it's ideal to have a separate domain (as defined by the Public Suffix List) for sketchy stuff like files uploaded by users. Eliminates a whole class of security issues and general fuckery

teaearlgraycold 5 days ago | parent | prev | next [-]

For the juniors reading this, here's what you do:

Buy a second domain, ideally using the same TLD as your production domain (some firewalls and filters will be prejudiced against specific TLDs). Mimic the subdomains exactly as they are in production for staging/dev.

anonfordays 4 days ago | parent [-]

Just use subdomains such as *.dev.example.com, *.test.example.com, *.prod.example.com, etc., no?

mcfedr 4 days ago | parent | next [-]

The reason not to do that is that dev.example.com can set cookies on example.com and other envs can see them.

thayne 4 days ago | parent | prev | next [-]

That only works if you (and any third party code that might run on such a domain) are completely consistent about always specifying the domain as one of your subdomains whenever you set a cookie.

And if your marketing/SEO/business people are ok with having something like "prod" as a subdomain for all your production web pages.

sensanaty 4 days ago | parent | next [-]

Usually it's mainsite.com for the marketing site, and then app.mainsite.com for actual production, or if you have multiple it'll have the product name, like coolproduct.mainsite.com

We then have app-stg and app-canary subdomains for our test envs which can only be accessed by us (enforced via zero trust). No reason for marketing or SEO teams to care in any case.

netdevphoenix 4 days ago | parent | prev [-]

When was the last time you saw a public website like that? prod.companyname.com websites are extremely rare especially outside tech.

graemep 4 days ago | parent [-]

The production site could be www. or something else that makes sense.

jan_g 4 days ago | parent | prev | next [-]

We have *.example.dev, *.example.qa, *.example.com for development, staging/qa and production. Works well and we haven't had any issues with cookies.

teaearlgraycold 4 days ago | parent [-]

This works fine and is what I’ve done. But if you’re sending email from those domains or working with enterprise customers using the same TLD will be helpful.

teaearlgraycold 4 days ago | parent | prev [-]

Ah yes if you use a CNAME that would work. You know better than me.

jamesfinlayson 4 days ago | parent | prev | next [-]

I had the option to re-use the prod domain for non-prod a few years ago (the company's other two projects use the prod domain for all non-prod environments).

I didn't really think about cookies back then but it just felt like a generally bad idea because disastrously messing up a URL in some config or related service would be much easier.

dgoldstein0 4 days ago | parent [-]

Nah dev should probably be a separate tld so the cookies are completely isolated.

Stage, it depends - if you want stage to have production data with newer code, and are fine with the session / cookies being shared - host it on the same domain and switch whether users get stage or prod based on IP, who is logged in, and/or a cookie. That way your code doesn't have to do anything different for stage vs prod every time it looks at the request domain (or wants to set cookies).

If you want an isolated stage environment, why not just use a separate top level domain? Otherwise you are likely seeing yourself up for the two interfering with each other via cookies on the TLD.

jamesfinlayson 2 days ago | parent [-]

Yeah that's what I meant by separate domain - separate top level domain.

Not that we use cookies much but it's one less thing to worry about.

anal_reactor 5 days ago | parent | prev [-]

I'm sure this will be replicated in future projects because it's much easier to argue "we're already following this pattern so let's be consistent" than "this pattern is bad and let's not have two ruined projects"

sureIy 4 days ago | parent | prev | next [-]

Seems perfectly reasonable to me?

If you are on /somepath I'd expect to get C as is the most specific value out of all three. All the values are still returned, ordered, which to me is the best of both worlds (path-specific values + knowing the globals)

The only thing I don't like is the magic `document.cookie` setter, but alas that's nearly 30 years old.

spacebanana7 5 days ago | parent | prev | next [-]

I wonder if this explains a lot of the unusual behaviour that happens when you use multiple accounts on a website in the same browser.

bazzargh 4 days ago | parent | prev | next [-]

btw, technically that leading dot in the domain isn't allowed and will be ignored; https://www.rfc-editor.org/rfc/rfc6265#section-4.1.2.3

... this came up recently after I tightened the validation in jshttp/cookie https://github.com/jshttp/cookie/pull/167 - since that PR the validation has been loosened again a bit, similar to the browser code mentioned in the article.

My changes were prompted by finding a bug in our code (not jshttp) where a cookie header was constructed by mashing the strings together without encoding; every so often a value would have a space and break requests. I was going to suggest using jshttp/cookie's serialize() to devs to avoid this but then realized that that didn't validate well enough to catch the bug we'd seen. I proposed a fix, and someone else then spotted that the validation was loose enough you could slip js into the _name_ field of the cookie which would be interpreted elsewhere as the _value_, providing an unusal vector for code injection.

jonchurch_ 4 days ago | parent [-]

This is one of those things where specs are still hard to parse.

It is considered invalid syntax to lead with a dot by the rules. But it also must be ignored if present. Its lacking a “MUST NOT” because the spec is defining valid syntax, while also defining behavior for back compat.

It would break too many things to throw here or serialize while ignoring the leading dot. Leading dots are discouraged, but shouldnt break anyone following the spec. Maybe a warn log in dev mode if serializing a domain with dot, to try and educate users. Dunno its worth it though.

The point of jshttp IMO is to smooth over these kinds of nuances from spec updates. So devs can get output which is valid in as many browsers as possible without sacrificing functionality or time studying the tomes.

bazzargh 4 days ago | parent [-]

I do sympathise somewhat with that view, but I disagree. To be valid in as many browsers as possible, and as many back-end systems too, serialize() would have to take the _narrowest_ view of the spec possible. If you make cookies that stray from the spec, you cannot know if they will work as intended when they are read, you've baked in undefined behaviour. It's not just browsers; in our systems we have myriad backends that read the cookies that are set by other microservices, that could be reading them strictly and dropping the non-conformant values.

If you want to set invalid cookie headers, it's very easy to do so, I just don't think you should expect a method that says it will validate the values to do that.

The dot I can go along with because the behaviour is defined, but I'm less comfortable that a bunch of other characters got re-added a couple of days ago. As for smoothing over nuances from spec updates...the RFC has been out there for 13 years, and jshttp/cookie has only been around for 12; there have been no updates to smooth, it has just never validated to the spec.

dgoldstein0 4 days ago | parent | prev | next [-]

Yep it's hella fraught. https://www.usenix.org/conference/usenixsecurity15/technical... goes into detail about this problem and related headaches

teaearlgraycold 5 days ago | parent | prev | next [-]

Using the path field is a code smell

NBJack 5 days ago | parent | next [-]

Can you elaborate? I'm having a tough time finding references to that. (Disclaimer: I'm not an avid JS developer)

kijin 4 days ago | parent | next [-]

It means that you are setting cookies on whatever page you're on, without considering whether the cookie will be consistently accessible on other pages.

For example, you set the currency to EUR in /product/123, but when you navigate to /cart and refresh, it's back to USD. You change it again to EUR, only to realize in /cart/checkout that the USD pricing is actually better. So you try to set it back to USD, but now the cookie at /cart conflicts with the one at /cart/checkout because each page has its own cookie.

oneeyedpigeon 4 days ago | parent | next [-]

If you want cookies to be global, set them to / or leave out the path. If you want more fine-grained cookies, use a specific path. What's the problem? Currency is—in your example—clearly a site-wide setting. I think sites should make more of their hierarchical structure, not less.

kijin 4 days ago | parent [-]

If you leave out the path, it will default to the directory of the current URL, not /.

If not for this default behavior, it would have been much easier to manage global settings such as currency. Right now, all it takes is one cookie without a path to introduce inconsistency, only on some pages, in a way that's hard to reproduce.

foldr 4 days ago | parent | prev [-]

Isn't that just the feature working as intended? Of course it is possible to introduce a bug by setting or not setting a cookie somewhere where it should/shouldn't be set.

I've never found a use for path-based cookies personally, but I'm not sure this is a particularly compelling example.

speleding 4 days ago | parent [-]

The typical example of a path-based cookie is the "remember my login name" feature, where you want the cookie with the user name only available on the login page. (And you cannot use session storage because you want it to work whilst logged out.)

Xelynega 4 days ago | parent [-]

You don't need to store multiple login names for seperate pages though, so why can't this just be a site wide cookie?

speleding 4 days ago | parent [-]

That would include the cookie with each request, which is inefficient. And potentially it also can get sent with requests to other subdomains, which may not be desirable from a security point of view (it could be cdn.example.com, owned by someone else)

teaearlgraycold 5 days ago | parent | prev [-]

For modern applications you’ll have better ways to maintain state. As shown they cause trouble in practice. Cookies should be used sparingly.

prokopton 5 days ago | parent [-]

If you want to maintain state across navigations and share that state with a server it’s the best we’ve got.

bpicolo 5 days ago | parent [-]

Server can store session state

telgareith 5 days ago | parent [-]

Server side session state for more than authentication is way worse than "code smell."

It requires a ping to a shared data source on every request. And, the same one for all of them. No sharding, No split domains... That gets expensive fast!

naasking 4 days ago | parent | next [-]

You just described how the whole web operates. It works just fine.

MBCook 4 days ago | parent [-]

Even if you want client side, we have better ways now than cookies.

inopinatus 4 days ago | parent [-]

We do, but only cookies are universally available. Plenty of unusual user-agents in the world, or people like me that browse with JS off by default.

CrimsonRain 4 days ago | parent | prev [-]

I add some products in phone. Then I login to desktop later for modification and order. Cart is empty. That's engineering smell. A really bad one.

telgareith 4 days ago | parent [-]

Thats nothing more than UX/UI.

> In computer programming, a code smell is any characteristic in the source code of a program that possibly indicates a deeper problem. Determining what is and is not a code smell is subjective, and varies by language, developer, and development methodology.

- https://en.wikipedia.org/wiki/Code_smell

tacone 3 days ago | parent | prev [-]

I am using path to wire my http only cookies to be sent only to /api not in assets/html requests. The cookie will eventually contain a JWT token I do use as an access token. Consequently I will probably wire my refresh cookie only to be sent to /api/refresh-token and not in other requests.

The client won't get to decide which cookie to send where.

Looks like a good pattern to me.

draw_down 5 days ago | parent | prev [-]

Yeah, isn’t that how you represent a list of values? (Or maybe better to say a collection, not sure if ordering is preserved)

kevincox 5 days ago | parent [-]

But if the attributes are exactly the same then the cookies replace each other. So this isn't a general mechanism for representing a list.

Not to mention that the way to delete a cookie is sending a replacement cookie that expires in the past. How are you supposed to delete the right cookie here?

maxwellg 5 days ago | parent | next [-]

And the worst is that you need to exactly match the domain and path semantics in order to delete the cookie! Domain is easy enough because there are only two options - available to subdomain and not available to subdomain. But if you have a cookie with the `/path` set and you don't know what value was used, you literally cannot delete that cookie from JS or the backend. You need to either pop open devtools and look at the path or ask the end user to clear all cookies.

HappMacDonald 5 days ago | parent | prev | next [-]

Is there a way for JS to see the attributes for each value? Because presumably setting an expire time in the past and iterating over every used set of attributes would get the job done to delete the cookie. Iterating over all possible (plausible?) attributes may also work, but knowing the specific attributes set would narrow that list of erasing writes to issue.

kijin 4 days ago | parent [-]

No, there isn't. All you get a list of values that are valid for the current page. Same on the server side.

If you're ever in a situation where you need to invalidate all possible instances of a cookie, it's easier to just use a different name.

sieabahlpark 5 days ago | parent | prev [-]

[dead]