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

Remove Cookie Name Decoding #10442

Merged

Conversation

security-curious
Copy link
Contributor

Per response from [email protected] posting as public PR. Proof of concept to demonstrate the vulnerability can be seen at https://github.com/security-curious/cookie-prefix-crystal-poc


Crystal automatically encodes and decodes cookie names. The cookie spec encourages the value to be encoded but says nothing about the cookie name (it could be encoded or not).

While encoding is allowed, a vulnerability can occur if the cookie name is decoded in such a way that a cookie not using a correctly formed cookie prefix will overwrite a cookie that is using a proper cookie prefix rendering the protections provided cookie prefixes void.

Since Crystal is encoding the entire cookie name using URL encoding two cookies can be sent by the browser and if in the correct order the server will read the cookie without the proper cookie prefix. An example cookie header value is:

__Secure-Name=safe; __%53ecure-Name=evil

The exact same issue was recently corrected in the rack library for the Ruby language as well as the werkzeug library used by Python's Flask web framework. In fact those corrects are what prompted me to look to see if Crystal had the same issue.

Although NIST rated the Rack vulnerability "high" the Rack maintainers consider that a mistake and consider is a low priority vulnerability. I agree.

The solution taken by both rack and werkzeug were to simply stop encoding the cookie name. If an application needs it encoded because they have a funky name that might mess with header parsing that application can do the encoding itself. This is the solution implemented on this commit.

An alternative solution that is probably more compatible but also more complex is to encode the part of the cookie name after
the cookie prefix if the cookie has a prefix that matches one of the official cookie prefixes (__Secure- and __Host-).
While this would preserve the encoding for the most part I don't feel the complexity introduced is worth the benefit.

Crystal automatically encodes and decodes cookie names. The cookie
spec[1] encourages the value to be encoded but says nothing about
the cookie name (it could be encoded or not).

While encoding is allowed, a vunerability can occur if the cookie
name is decoded in such a way that a cookie not using a correctly
formed cookie prefix will overwrite a cookie that is using a
proper cookie prefix rendering the protections provided cookie
prefixes void.

Since Crystal is encoding the entire cookie name using URL encoding
two cookies can be sent by the browser and if in the correct order
the server will read the cookie without the proper cookie prefix.
An example cookie header value is:

    __Secure-Name=safe; __%53ecure-Name=evil

The exact same issue was recently corrected in the `rack` library
for the Ruby language[2] as well as the `werkzeug` library used
by Python's Flask web framework[3]. In fact those corrects are
what prompted me to look to see if Crystal had the same issue.

Although NIST rated the Rack vunerability "high"[4] the Rack
maintainers consider that a mistake and consider is a low priority
vunerability[5]. I agree.

The solution taken by both `rack` and `werkzeug` were to simply
stop encoding the cookie name. If an application needs it encoded
because they have a funky name that might mess with header parsing
that application can do the encoding itself. This is the solution
implemented on this commit.

An alternative solution that is probably more compatible but
also more complex is to encode the part of the cookie name after
the cookie prefix if the cookie has a prefix that matches
one of the official cookie prefixes (`__Secure-` and `__Host-`).
While this would preserve the encoding for the most part I
don't feel the complexity introduced is worth the benefit.

1. https://tools.ietf.org/html/rfc6265#section-4.1.1
2. https://hackerone.com/reports/895727
3. pallets/werkzeug#1965
4. https://nvd.nist.gov/vuln/detail/CVE-2020-8184
5. rack/rack#1677 (comment)
Copy link
Member

@bcardiff bcardiff left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For existing projects the following monkey-patch can be applied meanwhile after the require "http".

module HTTP
  class Cookie
    def to_cookie_header(io)
      io << @name
      io << '='
      URI.encode_www_form(value, io)
    end

    def parse_cookies(header)
      header.scan(CookieString).each do |pair|
        yield Cookie.new(pair["name"], URI.decode_www_form(pair["value"]))
      end
    end
  end
end

Thanks @security-curious

@straight-shoota
Copy link
Member

Related: #9306

@straight-shoota straight-shoota added kind:bug A bug in the code. Does not apply to documentation, specs, etc. security topic:stdlib:networking labels Feb 25, 2021
@straight-shoota straight-shoota added this to the 1.0.0 milestone Feb 25, 2021
@bcardiff bcardiff merged commit eef60c4 into crystal-lang:master Feb 25, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
kind:bug A bug in the code. Does not apply to documentation, specs, etc. security topic:stdlib:networking
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants