class Arachni::Element::Cookie

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

@author Tasos “Zapotek” Laskos <tasos.laskos@arachni-scanner.com>

Constants

DEFAULT

Default cookie values

ENCODE_CACHE
ENCODE_CHARACTERS
ENCODE_CHARACTERS_LIST

Attributes

data[R]

Private Class Methods

decode( str ) click to toggle source

Decodes a {String} encoded for the ‘Cookie` header field.

@example

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

@param [String] str

@return [String]

# File lib/arachni/element/cookie.rb, line 500
def decode( str )
    Form.decode str
end
encode( str ) click to toggle source

Encodes a {String}‘s reserved characters in order to prepare it for the `Cookie` header field.

@example

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

@param [String] str

@return [String]

# 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 ) click to toggle source

Converts a cookie’s expiration date to a Ruby ‘Time` object.

@example String time format

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

@example 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

@param [String] expires

@return [Time]

# 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 ) click to toggle source

Parses a Netscape Cookie-jar into an Array of {Cookie}.

@param [String] url

{HTTP::Request} URL.

@param [String] filepath

Netscape HTTP cookiejar file.

@return [Array<Cookie>]

@see curl.haxx.se/rfc/cookie_spec.html

# 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 ) click to toggle source

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

@param [String] url

{HTTP::Request} URL.

@param [Hash] headers

@return [Array<Cookie>]

@see .forms_set_cookie

# 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 ) click to toggle source

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

@param [Arachni::Parser] parser

@return [Array<Cookie>]

@see .parse_set_cookie

# 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 ) click to toggle source

Extracts cookies from an HTTP {Arachni::HTTP::Response response}.

@param [Arachni::HTTP::Response] response

@return [Array<Cookie>]

@see .from_parser @see .from_headers

# 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 ) click to toggle source
Calls superclass method Arachni::Element::Base::from_rpc_data
# 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
from_string( url, string ) click to toggle source

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

@param [String] url

{HTTP::Request} URL.

@param [Hash] string

`Cookie` string.

@return [Array<Cookie>]

# 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 ) click to toggle source
# File lib/arachni/element/cookie.rb, line 351
def in_html?( html )
    html =~ /set-cookie/i
end
new( options ) click to toggle source

@param [Hash] options

For options see {DEFAULT}, with the following extras:

@option options [String] :url

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

@option options [String] :action

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

@option options [Hash] :inputs

Allows you to pass cookie data as a `name => value` pair instead of the
more complex {DEFAULT} structure.
# 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
value_to_v0( value ) click to toggle source
# 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

Private Instance Methods

decode( str ) click to toggle source

@see .decode

# File lib/arachni/element/cookie.rb, line 220
def decode( str )
    self.class.decode( str )
end
encode( *args ) click to toggle source

@see .encode

# File lib/arachni/element/cookie.rb, line 215
def encode( *args )
    self.class.encode( *args )
end
expired?( time = Time.now ) click to toggle source

Indicates whether or not the cookie has expired.

@param [Time] time

To compare against.

@return [Boolean]

# File lib/arachni/element/cookie.rb, line 144
def expired?( time = Time.now )
    expires_at != nil && time > expires_at
end
expires_at() click to toggle source

@return [Time, NilClass]

Expiration `Time` of the cookie or `nil` if it doesn't have one
(i.e. is a session cookie).
# File lib/arachni/element/cookie.rb, line 134
def expires_at
    @data[:expires]
end
http_only?() click to toggle source

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

@return [Bool]

# File lib/arachni/element/cookie.rb, line 118
def http_only?
    @data[:httponly] == true
end
http_request( opts = {}, &block ) click to toggle source
# File lib/arachni/element/cookie.rb, line 524
def http_request( opts = {}, &block )
    opts[:cookies] = opts.delete( :parameters )

    self.method == :get ?
        http.get( self.action, opts, &block ) :
        http.post( self.action, opts, &block )
end
method_missing( sym, *args, &block ) click to toggle source

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

Calls superclass method
# 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
respond_to?( *args ) click to toggle source

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

@return [Bool]

Calls superclass method
# File lib/arachni/element/cookie.rb, line 170
def respond_to?( *args )
    (@data && @data.include?( args.first )) || super
end
secure?() click to toggle source

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

@return [Bool]

# File lib/arachni/element/cookie.rb, line 111
def secure?
    @data[:secure] == true
end
session?() click to toggle source

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.

@return [Bool]

# File lib/arachni/element/cookie.rb, line 127
def session?
    @data[:expires].nil?
end
simple() click to toggle source

@example

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

@return [Hash]

Simple representation of the cookie as a hash -- with the cookie name as
`key` and the cookie value as `value`.
# File lib/arachni/element/cookie.rb, line 156
def simple
    self.inputs.dup
end
to_rpc_data() click to toggle source
# 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_s() click to toggle source

@return [String]

To be used in a `Cookie` HTTP request header.
# 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