Class: Arachni::Element::Form

Inherits:
Base show all
Includes:
Capabilities::Analyzable, Capabilities::Auditable, Capabilities::Auditable::Buffered, Capabilities::Auditable::LineBuffered, Capabilities::Inputtable, Capabilities::Mutable, Capabilities::Refreshable, Capabilities::Submittable, Capabilities::WithDOM, Capabilities::WithNode
Defined in:
lib/arachni/element/form.rb,
lib/arachni/element/form/dom.rb,
lib/arachni/element/form/capabilities/mutable.rb,
lib/arachni/element/form/capabilities/with_dom.rb,
lib/arachni/element/form/capabilities/auditable.rb,
lib/arachni/element/form/capabilities/submittable.rb

Overview

Represents an auditable form element

Author:

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

Defined Under Namespace

Modules: Capabilities Classes: DOM, Error

Constant Summary collapse

DECODE_CACHE =
Arachni::Support::Cache::LeastRecentlyPushed.new( 1_000 )
ORIGINAL_VALUES =
'__original_values__'
SAMPLE_VALUES =
'__sample_values__'

Constants included from Capabilities::Auditable::LineBuffered

Capabilities::Auditable::LineBuffered::DEFAULT_LINE_BUFFER_SIZE

Constants included from Capabilities::Auditable::Buffered

Capabilities::Auditable::Buffered::DEFAULT_BUFFER_SIZE

Constants included from Capabilities::Mutable

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

Constants included from Capabilities::Auditable

Capabilities::Auditable::OPTIONS

Constants included from Capabilities::Analyzable::Differential

Capabilities::Analyzable::Differential::DIFFERENTIAL_ALLOWED_STATUS, Capabilities::Analyzable::Differential::DIFFERENTIAL_OPTIONS

Constants included from Capabilities::Analyzable::Timeout

Capabilities::Analyzable::Timeout::TIMEOUT_OPTIONS

Constants included from Capabilities::Analyzable::Signature

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

Constants included from Capabilities::Inputtable

Capabilities::Inputtable::INPUTTABLE_CACHE

Constants inherited from Base

Base::MAX_SIZE

Instance Attribute Summary collapse

Attributes included from Capabilities::Mutable

#affected_input_name, #format, #seed

Attributes included from Capabilities::Auditable

#audit_options

Attributes included from Capabilities::WithAuditor

#auditor

Attributes included from Capabilities::WithDOM

#dom, #skip_dom

Attributes included from Capabilities::Analyzable::Differential

#differential_analysis_options

Attributes included from Capabilities::Analyzable::Timeout

#timing_attack_remark_data

Attributes included from Capabilities::Inputtable

#default_inputs, #inputs, #raw_inputs

Attributes included from Capabilities::WithSource

#source

Attributes inherited from Base

#initialization_options, #page

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Capabilities::Auditable::LineBuffered

#line_buffered_audit

Methods included from Capabilities::Auditable::Buffered

#buffered_audit

Methods included from Capabilities::Mutable

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

Methods included from Capabilities::Submittable

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

Methods included from Capabilities::Auditable

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

Methods included from Capabilities::WithAuditor

#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 Capabilities::WithDOM

#skip_dom?

Methods included from Capabilities::Refreshable

#refresh, #refresh_id

Methods included from Capabilities::Analyzable

has_timeout_candidates?, reset, timeout_audit_run

Methods included from Capabilities::Analyzable::Differential

#differential_analysis, reset, #to_rpc_data

Methods included from Capabilities::Analyzable::Timeout

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

Methods included from Capabilities::Analyzable::Signature

#get_matches, #signature_analysis

Methods included from Capabilities::Inputtable

#[], #[]=, #changes, #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 Capabilities::WithNode

#node

Methods included from Capabilities::WithSource

#to_h, #to_rpc_data

Methods inherited from Base

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

Methods included from Capabilities::WithScope

#scope

Constructor Details

#initialize(options) ⇒ Form

Returns a new instance of Form.

Parameters:

Options Hash (options):

  • :name (String)

    Form name.

  • :id (String)

    Form ID.

  • :method (String) — default: :get

    Form method.

  • :url (String)

    URL of the page which includes the form.

  • :action (String)

    Form action – defaults to `:url`.

  • :inputs (Hash)

    Form inputs, can either be simple `name => value` pairs or a more detailed representation such as:

    {
        'my_token'  => {
            type:  :hidden,
            value: 'token-value'
        }
    }
    


86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/arachni/element/form.rb', line 86

def initialize( options )
    super( options )

    @name = options[:name]
    @id   = options[:id]

    @input_details = {}

    cinputs = (options[:inputs] || {}).inject({}) do |h, (name, value_or_info)|
         if value_or_info.is_a? Hash
             h[name]                   = value_or_info[:value]
             @input_details[name.to_s] = value_or_info
         else
             h[name] = value_or_info
         end
            h
        end

    self.inputs = (method == :get ?
        (self.inputs || {}).merge(cinputs) : cinputs )

    @default_inputs = self.inputs.dup.freeze
end

Instance Attribute Details

#nameString?

Returns Name of the form, if it has one.

Returns:

  • (String, nil)

    Name of the form, if it has one.



63
64
65
# File 'lib/arachni/element/form.rb', line 63

def name
  @name
end

#nonce_nameString

Returns The name of the input name that holds the nonce.

Returns:

  • (String)

    The name of the input name that holds the nonce.



59
60
61
# File 'lib/arachni/element/form.rb', line 59

def nonce_name
  @nonce_name
end

Class Method Details

.attributes_to_hash(attributes) ⇒ Object



382
383
384
# File 'lib/arachni/element/form.rb', line 382

def attributes_to_hash( attributes )
    attributes.inject( {} ){ |h, (k, v)| h[k.to_sym] = v.to_s; h }
end

.decode(string) ⇒ String

Decodes a String encoded for an HTTP request's body.

Parameters:

Returns:



412
413
414
415
416
417
418
419
420
421
422
423
424
425
# File 'lib/arachni/element/form.rb', line 412

def decode( string )
    string = string.to_s

    DECODE_CACHE.fetch( string ) do
        # Fast, but could throw error.
        begin
            ::URI.decode_www_form_component string

        # Slower, but reliable.
        rescue ArgumentError
            URI.decode( string.gsub( '+', ' ' ) )
        end
    end
end

.encode(string) ⇒ String

Encodes a String's reserved characters in order to prepare it to be included in a request body.

Parameters:

Returns:



403
404
405
# File 'lib/arachni/element/form.rb', line 403

def encode( string )
    Arachni::HTTP::Request.encode string
end

.from_node(url, node, ignore_scope = false) ⇒ Object



280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
# File 'lib/arachni/element/form.rb', line 280

def from_node( url, node, ignore_scope = false )
    options          = attributes_to_hash( node.attributes )
    options[:url]    = url.freeze
    options[:action] = to_absolute( options[:action], url ).freeze
    options[:inputs] = {}
    options[:source] = node.to_html.freeze

    if (parsed_url = Arachni::URI( options[:action] ))
        return if !ignore_scope && parsed_url.scope.out?
    end

    # Forms can have many submit inputs with identical names but different
    # values, to act as a sort of multiple choice.
    # However, each Arachni Form can have only unique input names, so
    # we keep track of this here and create a new form for each choice.
    multiple_choice_submits = {}

    %w(textarea input select button).each do |tag|
        options[tag] ||= []

        node.nodes_by_name( tag ).each do |elem|
            elem_attrs = attributes_to_hash( elem.attributes )
            elem_attrs[:type] = elem_attrs[:type].to_sym if elem_attrs[:type]

            name = elem_attrs[:name] || elem_attrs[:id]
            next if !name

            # Handle the easy stuff first...
            if elem.name != :select
                options[:inputs][name] = elem_attrs

                if elem_attrs[:type] == :submit
                    multiple_choice_submits[name] ||= Set.new
                    multiple_choice_submits[name] << elem_attrs[:value]
                end

                options[:inputs][name][:type]  ||= :text
                options[:inputs][name][:value] ||= ''

                if too_big?( options[:inputs][name][:value] )
                    options[:inputs][name][:value] = ''
                end

                next
            end

            children = elem.nodes_by_name( 'option' )

            # If the select has options figure out which to use.
            if children.any?
                children.each do |child|
                    h = attributes_to_hash( child.attributes )
                    h[:type]    = :select
                    h[:value] ||= child.text.strip

                    if too_big?( h[:value] )
                        h[:value] = ''
                    end

                    # Prefer the selected or first option.
                    if h[:selected]
                        options[:inputs][name] = h
                    else
                        options[:inputs][name] ||= h
                    end
                end

            # The select has no options, use an empty string.
            else
                options[:inputs][name] = {
                    type:  :select,
                    value: ''
                }
            end
        end
    end

    return [new( options )] if multiple_choice_submits.empty?

    # If there are multiple submit with the same name but different values,
    # create forms for each value.
    multiple_choice_submits.map do |name, values|
        values.map.with_index do |value, i|

            o = options
            if values.size > 1
                o = options.deep_clone
                o[:inputs][name][:value] = value

                # We need to add this here because if the forms have the
                # same input names only the first one will be audited.
                o[:inputs]["_#{name}_#{i}"] = {
                    type: :fake,
                    value: value
                }
            end

            new( o )
        end
    end.flatten.compact
end

.from_parser(parser, ignore_scope = false) ⇒ Array<Form>

Extracts forms from an HTML document.

Parameters:

Returns:



260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
# File 'lib/arachni/element/form.rb', line 260

def from_parser( parser, ignore_scope = false )
    return [] if parser.body && !in_html?( parser.body )

    base_url = to_absolute( parser.base, parser.url )

    parser.document.nodes_by_name( :form ).map do |node|
        next if !(forms = from_node( base_url, node, ignore_scope ))
        next if forms.empty?

        forms.each do |form|
            form.url = parser.url
            form
        end
    end.flatten.compact
end

.from_response(response, ignore_scope = false) ⇒ Array<Form>

Extracts forms by parsing the body of an HTTP response.

Parameters:

Returns:



251
252
253
# File 'lib/arachni/element/form.rb', line 251

def from_response( response, ignore_scope = false )
    from_parser( Arachni::Parser.new( response ), ignore_scope )
end

.from_rpc_data(data) ⇒ Object



427
428
429
430
431
432
433
434
435
436
437
# File 'lib/arachni/element/form.rb', line 427

def from_rpc_data( data )
    # Inputs contain attribute data instead of just values, normalize them.
    if data['initialization_options']['inputs'].values.first.is_a? Hash
        data['initialization_options']['inputs'].each do |name, details|
            data['initialization_options']['inputs'][name] =
                details.my_symbolize_keys( true )
        end
    end

    super data
end

.in_html?(html) ⇒ Boolean

Returns:

  • (Boolean)


276
277
278
# File 'lib/arachni/element/form.rb', line 276

def in_html?( html )
    html.has_html_tag? 'form'
end

.parse_data(data, boundary) ⇒ Hash

Returns Name-value pairs.

Parameters:

  • data (String)

    `multipart/form-data` text.

  • boundary (String)

    `multipart/form-data` boundary.

Returns:

  • (Hash)

    Name-value pairs.



393
394
395
# File 'lib/arachni/element/form.rb', line 393

def parse_data( data, boundary )
    WEBrick::HTTPUtils.parse_form_data( data, boundary.to_s ).my_stringify
end

Instance Method Details

#decode(str) ⇒ Object

See Also:



229
230
231
# File 'lib/arachni/element/form.rb', line 229

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

#details_for(input) ⇒ Hash

Returns Information about the given input's attributes.

Parameters:

  • input (String)

    Input name.

Returns:

  • (Hash)

    Information about the given input's attributes.



119
120
121
# File 'lib/arachni/element/form.rb', line 119

def details_for( input )
    @input_details[input.to_s] || {}
end

#dupObject



233
234
235
236
237
238
239
240
241
242
# File 'lib/arachni/element/form.rb', line 233

def dup
    super.tap do |f|
        f.nonce_name = nonce_name.dup if nonce_name

        f.mutation_with_original_values if mutation_with_original_values?
        f.mutation_with_sample_values   if mutation_with_sample_values?

        f.requires_password = requires_password?
    end
end

#encode(str) ⇒ Object

See Also:



224
225
226
# File 'lib/arachni/element/form.rb', line 224

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

#fake_field?(name) ⇒ Boolean

Returns:

  • (Boolean)


219
220
221
# File 'lib/arachni/element/form.rb', line 219

def fake_field?( name )
    field_type_for( name ) == :fake
end

#field_type_for(name) ⇒ String

Retrieves a field type for the given field `name`.

Examples:

html_form = <<-HTML
<form>
    <input type='text' name='text-input' />
    <input type='password' name='passwd' />
    <input type='hidden' name='cant-see-this' />
</form>
HTML

p f.field_type_for 'text-input'
#=> :text

p f.field_type_for 'passwd'
#=> :password

p f.field_type_for 'cant-see-this'
#=> :hidden

Parameters:

  • name (String)

    Field name.

Returns:



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

def field_type_for( name )
    (details_for( name )[:type] || :text).to_sym
end

#force_train?Boolean

Returns:

  • (Boolean)


110
111
112
# File 'lib/arachni/element/form.rb', line 110

def force_train?
    mutation_with_original_values? || mutation_with_sample_values?
end

#has_nonce?Bool

Returns `true` if the form contains a nonce, `false` otherwise.

Returns:

  • (Bool)

    `true` if the form contains a nonce, `false` otherwise.



163
164
165
# File 'lib/arachni/element/form.rb', line 163

def has_nonce?
    !!nonce_name
end

#mirror_password_fieldsObject



135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/arachni/element/form.rb', line 135

def mirror_password_fields
    return if !requires_password?

    # if there are two password type fields in the form there's a good
    # chance that it's a 'please retype your password' thing so make sure
    # that we have a variation which has identical password values
    password_fields = inputs.keys.
        select { |input| field_type_for( input ) == :password }

    return if password_fields.size != 2

    self[password_fields[0]] = self[password_fields[1]]

    nil
end

#name_or_idString

Returns Name of ID HTML attributes for this form.

Returns:

  • (String)

    Name of ID HTML attributes for this form.



125
126
127
# File 'lib/arachni/element/form.rb', line 125

def name_or_id
    name || @id
end

#requires_password?Bool

Checks whether or not the form contains 1 or more password fields.

Returns:

  • (Bool)

    `true` if the form contains passwords fields, `false` otherwise.



155
156
157
158
159
# File 'lib/arachni/element/form.rb', line 155

def requires_password?
    return @requires_password if !@requires_password.nil?
    inputs.each { |k, _| return @requires_password = true if field_type_for( k ) == :password }
    @requires_password = false
end

#simpleHash

Returns A simple representation of self including attributes and inputs.

Returns:

  • (Hash)

    A simple representation of self including attributes and inputs.



131
132
133
# File 'lib/arachni/element/form.rb', line 131

def simple
    @initialization_options.merge( url: url, action: action, inputs: inputs )
end