Welcome to mirror list, hosted at ThFree Co, Russian Federation.

same_site_cookies.rb « middleware « gitlab « lib - gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: 37ccc5abb10374473dd309a98bdb00b63dac639b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
# frozen_string_literal: true

# This middleware sets the SameSite directive to None on all cookies.
# It also adds the Secure directive if HTTPS is enabled.
#
# Chrome v80, rolled out in March 2020, treats any cookies without the
# SameSite directive set as though they are SameSite=Lax
# (https://www.chromestatus.com/feature/5088147346030592). This is a
# breaking change from the previous default behavior, which was to treat
# those cookies as SameSite=None.
#
# This middleware is needed until we upgrade to Rack v2.1.0+
# (https://github.com/rack/rack/commit/c859bbf7b53cb59df1837612a8c330dfb4147392)
# and a version of Rails that has native support
# (https://github.com/rails/rails/commit/7ccaa125ba396d418aad1b217b63653d06044680).
#
module Gitlab
  module Middleware
    class SameSiteCookies
      COOKIE_SEPARATOR = "\n".freeze

      def initialize(app)
        @app = app
      end

      def call(env)
        status, headers, body = @app.call(env)
        result = [status, headers, body]

        set_cookie = headers['Set-Cookie']&.strip

        return result if set_cookie.blank? || !ssl?
        return result if same_site_none_incompatible?(env['HTTP_USER_AGENT'])

        cookies = set_cookie.split(COOKIE_SEPARATOR)

        cookies.each do |cookie|
          next if cookie.blank?

          # Chrome will drop SameSite=None cookies without the Secure
          # flag. If we remove this middleware, we may need to ensure
          # that all cookies set this flag.
          unless SECURE_REGEX.match?(cookie)
            cookie << '; Secure'
          end

          unless SAME_SITE_REGEX.match?(cookie)
            cookie << '; SameSite=None'
          end
        end

        headers['Set-Cookie'] = cookies.join(COOKIE_SEPARATOR)

        result
      end

      private

      # Taken from https://www.chromium.org/updates/same-site/incompatible-clients
      # We use RE2 instead of the browser gem for performance.
      IOS_REGEX = RE2('\(iP.+; CPU .*OS (\d+)[_\d]*.*\) AppleWebKit\/')
      MACOS_REGEX = RE2('\(Macintosh;.*Mac OS X (\d+)_(\d+)[_\d]*.*\) AppleWebKit\/')
      SAFARI_REGEX = RE2('Version\/.* Safari\/')
      CHROMIUM_REGEX = RE2('Chrom(e|ium)')
      CHROMIUM_VERSION_REGEX = RE2('Chrom[^ \/]+\/(\d+)')
      UC_BROWSER_REGEX = RE2('UCBrowser\/')
      UC_BROWSER_VERSION_REGEX = RE2('UCBrowser\/(\d+)\.(\d+)\.(\d+)')

      SECURE_REGEX = RE2(';\s*secure', case_sensitive: false)
      SAME_SITE_REGEX = RE2(';\s*samesite=', case_sensitive: false)

      def ssl?
        Gitlab.config.gitlab.https
      end

      def same_site_none_incompatible?(user_agent)
        return false if user_agent.blank?

        has_webkit_same_site_bug?(user_agent) || drops_unrecognized_same_site_cookies?(user_agent)
      end

      def has_webkit_same_site_bug?(user_agent)
        ios_version?(12, user_agent) ||
          (macos_version?(10, 14, user_agent) && safari?(user_agent))
      end

      def drops_unrecognized_same_site_cookies?(user_agent)
        if uc_browser?(user_agent)
          return !uc_browser_version_at_least?(12, 13, 2, user_agent)
        end

        chromium_based?(user_agent) && chromium_version_between?(51, 66, user_agent)
      end

      def ios_version?(major, user_agent)
        m = IOS_REGEX.match(user_agent)

        return false if m.nil?

        m[1].to_i == major
      end

      def macos_version?(major, minor, user_agent)
        m = MACOS_REGEX.match(user_agent)

        return false if m.nil?

        m[1].to_i == major && m[2].to_i == minor
      end

      def safari?(user_agent)
        SAFARI_REGEX.match?(user_agent)
      end

      def chromium_based?(user_agent)
        CHROMIUM_REGEX.match?(user_agent)
      end

      def chromium_version_between?(from_major, to_major, user_agent)
        m = CHROMIUM_VERSION_REGEX.match(user_agent)

        return false if m.nil?

        version = m[1].to_i
        version >= from_major && version <= to_major
      end

      def uc_browser?(user_agent)
        UC_BROWSER_REGEX.match?(user_agent)
      end

      def uc_browser_version_at_least?(major, minor, build, user_agent)
        m = UC_BROWSER_VERSION_REGEX.match(user_agent)

        return false if m.nil?

        major_version = m[1].to_i
        minor_version = m[2].to_i
        build_version = m[3].to_i

        return major_version > major if major_version != major
        return minor_version > minor if minor_version != minor

        build_version >= build
      end
    end
  end
end