Class: Arachni::Element::Cookie

Inherits:
Base show all
Includes:
Arachni::Element::Capabilities::Analyzable, Arachni::Element::Capabilities::Auditable, Arachni::Element::Capabilities::Auditable::Buffered, Arachni::Element::Capabilities::Auditable::LineBuffered, Arachni::Element::Capabilities::Inputtable, Arachni::Element::Capabilities::Mutable, Arachni::Element::Capabilities::Submittable, Arachni::Element::Capabilities::WithDOM, Arachni::Element::Capabilities::WithSource
Defined in:
lib/arachni/element/cookie.rb,
lib/arachni/element/cookie/dom.rb,
lib/arachni/element/cookie/capabilities/mutable.rb,
lib/arachni/element/cookie/capabilities/with_dom.rb,
lib/arachni/element/cookie/capabilities/inputtable.rb

Overview

Represents a Cookie object and provides helper class methods for parsing, encoding, etc.

Author:

Defined Under Namespace

Modules: Capabilities Classes: DOM

Constant Summary collapse

ENCODE_CHARACTERS =
['+', ';', '%', "\0", '&', ' ', '"', "\n", "\r", '=']
ENCODE_CHARACTERS_LIST =
ENCODE_CHARACTERS.join
ENCODE_CACHE =
Arachni::Support::Cache::LeastRecentlyPushed.new( 1_000 )
DEFAULT =

Default cookie values

{
    name:        nil,
    value:       nil,
    raw_name:    nil,
    raw_value:   nil,
    version:     0,
    port:        nil,
    discard:     nil,
    comment_url: nil,
    expires:     nil,
    max_age:     nil,
    comment:     nil,
    secure:      nil,
    path:        nil,
    domain:      nil,
    httponly:    false
}

Constants included from Arachni::Element::Capabilities::Mutable

Arachni::Element::Capabilities::Mutable::EXTRA_NAME, Arachni::Element::Capabilities::Mutable::FUZZ_NAME, Arachni::Element::Capabilities::Mutable::FUZZ_NAME_VALUE, Arachni::Element::Capabilities::Mutable::MUTATION_OPTIONS

Constants included from Arachni::Element::Capabilities::Inputtable

Arachni::Element::Capabilities::Inputtable::INPUTTABLE_CACHE

Constants included from Arachni::Element::Capabilities::Analyzable::Differential

Arachni::Element::Capabilities::Analyzable::Differential::DIFFERENTIAL_ALLOWED_STATUS, Arachni::Element::Capabilities::Analyzable::Differential::DIFFERENTIAL_OPTIONS

Constants included from Arachni::Element::Capabilities::Analyzable::Timeout

Arachni::Element::Capabilities::Analyzable::Timeout::TIMEOUT_OPTIONS

Constants included from Arachni::Element::Capabilities::Analyzable::Signature

Arachni::Element::Capabilities::Analyzable::Signature::FILE_SIGNATURES, Arachni::Element::Capabilities::Analyzable::Signature::FILE_SIGNATURES_PER_PLATFORM, Arachni::Element::Capabilities::Analyzable::Signature::LINE_BUFFER_SIZE, Arachni::Element::Capabilities::Analyzable::Signature::SIGNATURE_CACHE, Arachni::Element::Capabilities::Analyzable::Signature::SIGNATURE_OPTIONS, Arachni::Element::Capabilities::Analyzable::Signature::SOURCE_CODE_SIGNATURES_PER_PLATFORM

Constants included from Arachni::Element::Capabilities::Auditable::LineBuffered

Arachni::Element::Capabilities::Auditable::LineBuffered::DEFAULT_LINE_BUFFER_SIZE

Constants included from Arachni::Element::Capabilities::Auditable::Buffered

Arachni::Element::Capabilities::Auditable::Buffered::DEFAULT_BUFFER_SIZE

Constants included from Arachni::Element::Capabilities::Auditable

Arachni::Element::Capabilities::Auditable::OPTIONS

Constants inherited from Base

Base::MAX_SIZE

Instance Attribute Summary collapse

Attributes included from Arachni::Element::Capabilities::Mutable

#affected_input_name, #format, #seed

Attributes included from Arachni::Element::Capabilities::Inputtable

#default_inputs, #inputs, #raw_inputs

Attributes included from Arachni::Element::Capabilities::WithDOM

#dom, #skip_dom

Attributes included from Arachni::Element::Capabilities::WithSource

#source

Attributes included from Arachni::Element::Capabilities::Analyzable::Differential

#differential_analysis_options

Attributes included from Arachni::Element::Capabilities::Analyzable::Timeout

#timing_attack_remark_data

Attributes included from Arachni::Element::Capabilities::Auditable

#audit_options

Attributes included from Arachni::Element::Capabilities::WithAuditor

#auditor

Attributes inherited from Base

#initialization_options, #page

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Arachni::Element::Capabilities::Mutable

#affected_input_value, #affected_input_value=, #dup, #each_mutation, #immutables, #inspect, #mutation?, #mutations, #parameter_name_audit?, #reset, #switch_method, #to_h, #with_raw_payload, #with_raw_payload?

Methods included from Arachni::Element::Capabilities::Inputtable

#[], #[]=, #changes, #dup, #has_inputs?, inputtable_id, #inputtable_id, #raw_input?, #reset, #to_h, #try_input, #update, #updated?, #valid_input_data?, #valid_input_name?, #valid_input_name_data?, #valid_input_value?, #valid_input_value_data?

Methods included from Arachni::Element::Capabilities::WithDOM

#dup, #skip_dom?

Methods included from Arachni::Element::Capabilities::WithSource

#dup, #to_h

Methods included from Arachni::Element::Capabilities::Analyzable

has_timeout_candidates?, reset, timeout_audit_run

Methods included from Arachni::Element::Capabilities::Analyzable::Differential

#differential_analysis, #dup, reset

Methods included from Arachni::Element::Capabilities::Analyzable::Timeout

add_phase_2_candidate, candidates_include?, deduplicate, deduplicate?, do_not_deduplicate, #dup, #ensure_responsiveness, has_candidates?, payload_delay_from_options, reset, run, #timeout_analysis, timeout_from_options, #timeout_id, #timing_attack_probe, #timing_attack_verify

Methods included from Arachni::Element::Capabilities::Analyzable::Signature

#get_matches, #signature_analysis

Methods included from Arachni::Element::Capabilities::Auditable::LineBuffered

#line_buffered_audit

Methods included from Arachni::Element::Capabilities::Auditable::Buffered

#buffered_audit

Methods included from Arachni::Element::Capabilities::Auditable

#audit, #audit_id, #audit_status_message, #audit_status_message_action, #audit_verbose_message, #coverage_hash, #coverage_id, #dup, #matches_skip_like_blocks?, #reset, reset, #skip?, skip_like

Methods included from Arachni::Element::Capabilities::WithAuditor

#dup, #marshal_dump, #orphan?, #prepare_for_report, #remove_auditor

Methods included from Utilities

#available_port, available_port_mutex, #bytes_to_kilobytes, #bytes_to_megabytes, #caller_name, #caller_path, #cookie_decode, #cookie_encode, #cookies_from_file, #cookies_from_parser, #cookies_from_response, #exception_jail, #exclude_path?, #follow_protocol?, #form_decode, #form_encode, #forms_from_parser, #forms_from_response, #full_and_absolute_url?, #generate_token, #get_path, #hms_to_seconds, #html_decode, #html_encode, #include_path?, #links_from_parser, #links_from_response, #normalize_url, #page_from_response, #page_from_url, #parse_set_cookie, #path_in_domain?, #path_too_deep?, #port_available?, #rand_port, #random_seed, #redundant_path?, #regexp_array_match, #remove_constants, #request_parse_body, #seconds_to_hms, #skip_page?, #skip_path?, #skip_resource?, #skip_response?, #to_absolute, #uri_decode, #uri_encode, #uri_parse, #uri_parse_query, #uri_parser, #uri_rewrite

Methods included from Arachni::Element::Capabilities::Submittable

#action, #action=, #dup, #http, #id, #method, #method=, #platforms, #submit, #to_h

Methods inherited from Base

#==, #action, #dup, #hash, #id, #marshal_dump, #marshal_load, #persistent_hash, #prepare_for_report, #reset, #to_h, #to_hash, too_big?, type, #type, #url, #url=

Methods included from Arachni::Element::Capabilities::WithScope

#scope

Constructor Details

#initialize(options) ⇒ Cookie

Returns a new instance of Cookie.

Parameters:

  • options (Hash)

    For options see DEFAULT, with the following extras:

Options Hash (options):

  • :url (String)

    URL of the page which created the cookie -- required.

  • :action (String)

    URL of the page to submit the cookie -- defaults to :url.

  • :inputs (Hash)

    Allows you to pass cookie data as a name => value pair instead of the more complex DEFAULT structure.



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
# File 'lib/arachni/element/cookie.rb', line 75

def initialize( options )
    @data = {}
    super( options )

    if options[:name] && options[:value]
        options[:name]  = options[:name].to_s.recode
        options[:value] = options[:value].to_s.recode

        self.inputs = { options[:name] => options[:value] }
        @data.merge!( options )
    else
        self.inputs = (options[:inputs] || {}).dup
    end

    @data.merge!( DEFAULT.merge( @data ) )
    @data[:value] = decode( @data[:value].to_s ) rescue @data[:value].to_s

    parsed_uri = uri_parse( action )
    if !@data[:path]
        path = parsed_uri.path
        path = !path.empty? ? path : '/'
        @data[:path] = path
    end

    if @data[:expires] && !@data[:expires].is_a?( Time )
        @data[:expires] = Time.parse( @data[:expires].to_s ) rescue nil
    end

    @data[:domain] ||= parsed_uri.host

    @default_inputs = self.inputs.dup.freeze
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(sym, *args, &block) ⇒ Object

Uses the method name as a key to cookie attributes in DEFAULT.



161
162
163
164
# File 'lib/arachni/element/cookie.rb', line 161

def method_missing( sym, *args, &block )
    return @data[sym] if @data.include? sym
    super( sym, *args, &block )
end

Instance Attribute Details

#dataObject (readonly)

Returns the value of attribute data.



64
65
66
# File 'lib/arachni/element/cookie.rb', line 64

def data
  @data
end

Class Method Details

.decode(str) ⇒ String

Decodes a String encoded for the Cookie header field.

Examples:

p Cookie.decode "%2B%3B%25%3D%00+"
#=> "+;%=\x00 "

Parameters:

Returns:



500
501
502
# File 'lib/arachni/element/cookie.rb', line 500

def decode( str )
    Form.decode str
end

.encode(str) ⇒ String

Encodes a String's reserved characters in order to prepare it for the Cookie header field.

Examples:

p Cookie.encode "+;%=\0 "
#=> "%2B%3B%25%3D%00+"

Parameters:

Returns:



467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
# File 'lib/arachni/element/cookie.rb', line 467

def encode( str )
    str = str.to_s

    ENCODE_CACHE.fetch( [str, name] )  do
        if ENCODE_CHARACTERS.find { |c| str.include? c }

            # Instead of just encoding everything we do this selectively because:
            #
            #  * Some webapps don't actually decode some cookies, they just get
            #    the raw value, so if we encode something may break.
            #  * We need to encode spaces as '+' because of the above.
            #    Since we decode values, any un-encoded '+' will be converted
            #    to spaces, and in order to send back a value that the server
            #    expects we use '+' for spaces.

            s = ::URI.encode( str, ENCODE_CHARACTERS_LIST )
            s.gsub!( '%20', '+' )
            s
        else
            str
        end
    end
end

.expires_to_time(expires) ⇒ Time

Converts a cookie's expiration date to a Ruby Time object.

Examples:

String time format

p Cookie.expires_to_time "Tue, 02 Oct 2012 19:25:57 GMT"
#=> 2012-10-02 22:25:57 +0300

Seconds since Epoch

p Cookie.expires_to_time "1596981560"
#=> 2020-08-09 16:59:20 +0300

p Cookie.expires_to_time 1596981560
#=> 2020-08-09 16:59:20 +0300

Parameters:

Returns:

  • (Time)


313
314
315
316
# File 'lib/arachni/element/cookie.rb', line 313

def expires_to_time( expires )
    return nil if expires == '0'
    (expires_to_i = expires.to_i) > 0 ? Time.at( expires_to_i ) : Time.parse( expires )
end

.from_file(url, filepath) ⇒ Array<Cookie>

Parses a Netscape Cookie-jar into an Array of Arachni::Element::Cookie.

Parameters:

Returns:

See Also:



267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
# File 'lib/arachni/element/cookie.rb', line 267

def from_file( url, filepath )
    File.open( filepath, 'r' ).map do |line|
        # skip empty lines
        next if (line = line.strip).empty? || line[0] == '#'

        c = {}
        c['domain'], _, c['path'], c['secure'], c['expires'], c['name'],
            c['value'] = *line.split( "\t" )

        # expiry date is optional so if we don't have one push everything back
        begin
            c['expires'] = expires_to_time( c['expires'] )
        rescue
            c['value'] = c['name'].dup
            c['name'] = c['expires'].dup
            c['expires'] = nil
        end

        c['secure'] = (c['secure'] == 'TRUE') ? true : false

        c['raw_name'] = c['name']
        c['name'] = decode( c['name'] )

        c['raw_value'] = c['value']
        c['value'] = decode( c['value'] )

        new( { url: url }.merge( c.my_symbolize_keys ) )
    end.flatten.compact
end

.from_headers(url, headers) ⇒ Array<Cookie>

Extracts cookies from the Set-Cookie HTTP response header field.

Parameters:

Returns:

See Also:

  • forms_set_cookie


364
365
366
367
368
369
370
371
# File 'lib/arachni/element/cookie.rb', line 364

def from_headers( url, headers )
    headers = Arachni::HTTP::Headers.new( headers )
    return [] if headers.set_cookie.empty?

    exception_jail {
        headers.set_cookie.map { |c| from_set_cookie( url, c ) }.flatten
    } rescue []
end

.from_parser(parser) ⇒ Array<Cookie>

Extracts cookies from a document based on Set-Cookie http-equiv meta tags.

Parameters:

Returns:

See Also:



339
340
341
342
343
344
345
346
347
348
349
# File 'lib/arachni/element/cookie.rb', line 339

def from_parser( parser )
    return [] if parser.body && !in_html?( parser.body )

    Arachni::Utilities.exception_jail {
        parser.document.nodes_by_name( :meta ).map do |elem|
            next if elem['http-equiv'].downcase != 'set-cookie'

            from_set_cookie( parser.url, elem['content'] )
        end.flatten.compact
    } rescue []
end

.from_response(response) ⇒ Array<Cookie>

Extracts cookies from an HTTP response.

Parameters:

Returns:

See Also:



326
327
328
329
# File 'lib/arachni/element/cookie.rb', line 326

def from_response( response )
    from_parser( Arachni::Parser.new( response ) ) +
        from_headers( response.url, response.headers )
end

.from_rpc_data(data) ⇒ Object



242
243
244
245
246
247
248
249
250
251
252
253
254
255
# File 'lib/arachni/element/cookie.rb', line 242

def from_rpc_data( data )
    if data['initialization_options']['expires']
        data['initialization_options']['expires'] =
            Time.parse( data['initialization_options']['expires'] )
    end

    if data['data']['expires']
        data['data']['expires'] = Time.parse( data['data']['expires'] )
    end

    data['data'] = data['data'].my_symbolize_keys(false)

    super data
end

Parses the Set-Cookie header value into cookie elements.

Parameters:

Returns:



381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
# File 'lib/arachni/element/cookie.rb', line 381

def from_set_cookie( url, str )
    WEBrick::Cookie.parse_set_cookies( str ).flatten.uniq.map do |cookie|
        cookie_hash = {}
        cookie.instance_variables.each do |var|
            cookie_hash[var.to_s.gsub( /@/, '' )] = cookie.instance_variable_get( var )
        end

        # http://stackoverflow.com/questions/1062963/how-do-browser-cookie-domains-work/1063760#1063760
        if cookie_hash['domain']
            # IP addresses must be used verbatim.
            if !Arachni::URI( "http://#{cookie_hash['domain']}/" ).ip_address?
                cookie_hash['domain'] =
                    ".#{cookie_hash['domain'].sub( /^\./, '' )}"
            end
        end

        cookie_hash['expires'] = cookie.expires

        cookie_hash['path'] ||= '/'
        cookie_hash['raw_name']  = cookie.name
        cookie_hash['name']  = decode( cookie.name )

        if too_big?( cookie.value )
            cookie_hash['value'] = ''
        else
            quoted = "\"#{cookie.value}\""
            if str.include? quoted
                cookie_hash['raw_value']  = quoted
            else
                cookie_hash['raw_value']  = cookie.value
            end

            cookie_hash['value'] = decode( cookie.value )
        end

        new( { url: url, source: str }.merge( cookie_hash.my_symbolize_keys ) )
    end.flatten.compact
end

.from_string(url, string) ⇒ Array<Cookie>

Parses a string formatted for the Cookie HTTP request header field into cookie elements.

Parameters:

Returns:



430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
# File 'lib/arachni/element/cookie.rb', line 430

def from_string( url, string )
    return [] if string.empty?

    string.split( ';' ).map do |cookie_pair|
        cookie_pair.strip!

        k, v = *cookie_pair.split( '=', 2 )

        v = '' if too_big?( v )

        new(
            url:       url,
            source:    cookie_pair,
            raw_name:  k,
            raw_value: v,
            name:      decode( k ),
            value:     value_to_v0( v )
        )
    end.flatten.compact
end

.in_html?(html) ⇒ Boolean

Returns:

  • (Boolean)


351
352
353
# File 'lib/arachni/element/cookie.rb', line 351

def in_html?( html )
    html =~ /set-cookie/i
end


504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
# File 'lib/arachni/element/cookie.rb', line 504

def keep_for_set_cookie
    return @keep if @keep

    @keep = Set.new( DEFAULT.keys )
    @keep.delete( :name )
    @keep.delete( :value )
    @keep.delete( :raw_name )
    @keep.delete( :raw_value )
    @keep.delete( :domain )
    @keep.delete( :url )
    @keep.delete( :secure )
    @keep.delete( :httponly )
    @keep.delete( :version )
    @keep
end

.value_to_v0(value) ⇒ Object



451
452
453
454
455
456
# File 'lib/arachni/element/cookie.rb', line 451

def value_to_v0( value )
    return '' if !value

    value.start_with?( '"' ) && value.end_with?( '"' ) ?
        value[1...-1] : decode( value )
end

Instance Method Details

#decode(str) ⇒ Object

See Also:



220
221
222
# File 'lib/arachni/element/cookie.rb', line 220

def decode( str )
    self.class.decode( str )
end

#encode(*args) ⇒ Object

See Also:



215
216
217
# File 'lib/arachni/element/cookie.rb', line 215

def encode( *args )
    self.class.encode( *args )
end

#expired?(time = Time.now) ⇒ Boolean

Indicates whether or not the cookie has expired.

Parameters:

  • time (Time) (defaults to: Time.now)

    To compare against.

Returns:

  • (Boolean)


144
145
146
# File 'lib/arachni/element/cookie.rb', line 144

def expired?( time = Time.now )
    expires_at != nil && time > expires_at
end

#expires_atTime, NilClass

Returns Expiration Time of the cookie or nil if it doesn't have one (i.e. is a session cookie).

Returns:

  • (Time, NilClass)

    Expiration Time of the cookie or nil if it doesn't have one (i.e. is a session cookie).



134
135
136
# File 'lib/arachni/element/cookie.rb', line 134

def expires_at
    @data[:expires]
end

#http_only?Bool

Indicates whether the cookie is safe from modification from client-side code.

Returns:

  • (Bool)


118
119
120
# File 'lib/arachni/element/cookie.rb', line 118

def http_only?
    @data[:httponly] == true
end

#respond_to?(*args) ⇒ Bool

Used by #method_missing to determine if it should process the call.

Returns:

  • (Bool)


170
171
172
# File 'lib/arachni/element/cookie.rb', line 170

def respond_to?( *args )
    (@data && @data.include?( args.first )) || super
end

#secure?Bool

Indicates whether the cookie must be only sent over an encrypted channel.

Returns:

  • (Bool)


111
112
113
# File 'lib/arachni/element/cookie.rb', line 111

def secure?
    @data[:secure] == true
end

#session?Bool

Indicates whether the cookie is to be discarded at the end of the session.

Doesn't play a role during the scan but it can provide useful info to checks and such.

Returns:

  • (Bool)


127
128
129
# File 'lib/arachni/element/cookie.rb', line 127

def session?
    @data[:expires].nil?
end

#simpleHash

Returns Simple representation of the cookie as a hash -- with the cookie name as key and the cookie value as value.

Examples:

p Cookie.from_set_cookie( 'http://owner-url.com', 'session=stuffstuffstuff' ).first.simple
#=> {"session"=>"stuffstuffstuff"}

Returns:

  • (Hash)

    Simple representation of the cookie as a hash -- with the cookie name as key and the cookie value as value.



156
157
158
# File 'lib/arachni/element/cookie.rb', line 156

def simple
    self.inputs.dup
end

#to_rpc_dataObject



224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
# File 'lib/arachni/element/cookie.rb', line 224

def to_rpc_data
    h = super

    if h['initialization_options']['expires']
        h['initialization_options']['expires'] =
            h['initialization_options']['expires'].to_s
    end

    h['data'] = h['data'].my_stringify_keys(false)
    if h['data']['expires']
        h['data']['expires'] = h['data']['expires'].to_s
    end

    h
end

#to_sString

Returns To be used in a Cookie HTTP request header.

Returns:

  • (String)

    To be used in a Cookie HTTP request header.



176
177
178
179
180
181
182
183
184
185
# File 'lib/arachni/element/cookie.rb', line 176

def to_s
    # Only do encoding if we're dealing with updated inputs, otherwise pass
    # along the raw data as set in order to deal with server-side decoding
    # quirks.
    if updated? || !(raw_name || raw_value )
        "#{encode( name )}=#{encode( value )}"
    else
        "#{raw_name}=#{raw_value}"
    end
end

Returns Converts self to a Set-Cookie string.

Returns:

  • (String)

    Converts self to a Set-Cookie string.



189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
# File 'lib/arachni/element/cookie.rb', line 189

def to_set_cookie
    set_cookie = "#{self.to_s}"

    @data.each do |k, v|
        next if !v || !self.class.keep_for_set_cookie.include?( k )

        set_cookie << "; #{k.capitalize}=#{v}"
    end

    set_cookie << '; Secure'   if secure?
    set_cookie << '; HttpOnly' if http_only?

    # If we want to set a cookie for only the domain that responded to the
    # request, Set-Cookie should not specify a domain.
    #
    # If we want the cookie to apply to all subdomains, we need to either
    # specify a dot-prefixed domain or a domain, the browser client will
    # prefix the dot anyways.
    #
    # http://stackoverflow.com/questions/1062963/how-do-browser-cookie-domains-work/1063760#1063760
    set_cookie << "; Domain=#{domain}" if domain.start_with?( '.' )

    set_cookie
end