File: cookie.rb

package info (click to toggle)
ruby-secure-headers 7.1.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 508 kB
  • sloc: ruby: 3,353; makefile: 5
file content (150 lines) | stat: -rw-r--r-- 3,267 bytes parent folder | download
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
149
150
# frozen_string_literal: true
require "cgi"
require "secure_headers/utils/cookies_config"


module SecureHeaders
  class CookiesConfigError < StandardError; end
  class Cookie

    class << self
      def validate_config!(config)
        CookiesConfig.new(config).validate!
      end
    end

    attr_reader :raw_cookie, :config

    COOKIE_DEFAULTS = {
      httponly: true,
      secure: true,
      samesite: { lax: true },
    }.freeze

    def initialize(cookie, config)
      @raw_cookie = cookie
      unless config == OPT_OUT
        config ||= {}
        config = COOKIE_DEFAULTS.merge(config)
      end
      @config = config
      @attributes = {
        httponly: nil,
        samesite: nil,
        secure: nil,
      }

      parse(cookie)
    end

    def to_s
      @raw_cookie.dup.tap do |c|
        c << "; secure" if secure?
        c << "; HttpOnly" if httponly?
        c << "; #{samesite_cookie}" if samesite?
      end
    end

    def secure?
      flag_cookie?(:secure) && !already_flagged?(:secure)
    end

    def httponly?
      flag_cookie?(:httponly) && !already_flagged?(:httponly)
    end

    def samesite?
      flag_samesite? && !already_flagged?(:samesite)
    end

    private

    def parsed_cookie
      @parsed_cookie ||= CGI::Cookie.parse(raw_cookie)
    end

    def already_flagged?(attribute)
      @attributes[attribute]
    end

    def flag_cookie?(attribute)
      return false if config == OPT_OUT
      case config[attribute]
      when TrueClass
        true
      when Hash
        conditionally_flag?(config[attribute])
      else
        false
      end
    end

    def conditionally_flag?(configuration)
      if (Array(configuration[:only]).any? && (Array(configuration[:only]) & parsed_cookie.keys).any?)
        true
      elsif (Array(configuration[:except]).any? && (Array(configuration[:except]) & parsed_cookie.keys).none?)
        true
      else
        false
      end
    end

    def samesite_cookie
      if flag_samesite_lax?
        "SameSite=Lax"
      elsif flag_samesite_strict?
        "SameSite=Strict"
      elsif flag_samesite_none?
        "SameSite=None"
      end
    end

    def flag_samesite?
      return false if config == OPT_OUT || config[:samesite] == OPT_OUT
      flag_samesite_lax? || flag_samesite_strict? || flag_samesite_none?
    end

    def flag_samesite_lax?
      flag_samesite_enforcement?(:lax)
    end

    def flag_samesite_strict?
      flag_samesite_enforcement?(:strict)
    end

    def flag_samesite_none?
      flag_samesite_enforcement?(:none)
    end

    def flag_samesite_enforcement?(mode)
      return unless config[:samesite]

      if config[:samesite].is_a?(TrueClass) && mode == :lax
        return true
      end

      case config[:samesite][mode]
      when Hash
        conditionally_flag?(config[:samesite][mode])
      when TrueClass
        true
      else
        false
      end
    end

    def parse(cookie)
      return unless cookie

      cookie.split(/[;,]\s?/).each do |pairs|
        name, values = pairs.split("=", 2)
        name = CGI.unescape(name)

        attribute = name.downcase.to_sym
        if @attributes.has_key?(attribute)
          @attributes[attribute] = values || true
        end
      end
    end
  end
end