Class: Arachni::Element::Form

Inherits:
Base show all
Includes:
Capabilities::Analyzable, Capabilities::Refreshable, Capabilities::WithDOM, Capabilities::WithNode
Defined in:
lib/arachni/element/form.rb,
lib/arachni/element/form/dom.rb

Overview

Represents an auditable form element

Author:

Defined Under Namespace

Classes: DOM, Error

Constant Summary collapse

ORIGINAL_VALUES =
'__original_values__'
SAMPLE_VALUES =
'__sample_values__'

Constants included from Capabilities::Analyzable::Differential

Capabilities::Analyzable::Differential::DIFFERENTIAL_OPTIONS

Constants included from Capabilities::Analyzable::Taint

Capabilities::Analyzable::Taint::TAINT_OPTIONS

Constants included from Capabilities::Auditable

Capabilities::Auditable::OPTIONS

Constants included from Capabilities::Mutable

Capabilities::Mutable::MUTATION_OPTIONS

Instance Attribute Summary collapse

Attributes included from Capabilities::Auditable

#audit_options

Attributes included from Capabilities::WithAuditor

#auditor

Attributes included from Capabilities::Mutable

#affected_input_name, #format, #seed

Attributes included from Capabilities::Inputtable

#default_inputs, #inputs

Attributes included from Capabilities::WithNode

#html

Attributes inherited from Base

#initialization_options, #page

Class Method Summary collapse

Instance Method Summary collapse

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

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

Methods included from Capabilities::Analyzable::Taint

#taint_analysis

Methods included from Capabilities::Auditable

#audit, #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 Capabilities::Mutable

#affected_input_value, #affected_input_value=, #immutables, #mutation?, #mutations, #reset, #switch_method, #to_h

Methods included from Capabilities::Submittable

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

Methods included from Capabilities::Inputtable

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

Methods included from Utilities

#available_port, #caller_name, #caller_path, #cookie_decode, #cookie_encode, #cookies_from_document, #cookies_from_file, #cookies_from_response, #exception_jail, #exclude_path?, #follow_protocol?, #form_decode, #form_encode, #forms_from_document, #forms_from_response, #generate_token, #get_path, #hms_to_seconds, #html_decode, #html_encode, #include_path?, #links_from_document, #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?, #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::WithNode

#node, #to_h

Methods inherited from Base

#==, #action, from_rpc_data, #hash, #id, #marshal_dump, #marshal_load, #persistent_hash, #prepare_for_report, #reset, #to_h, #to_hash, #to_rpc_data, 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 more a more detailed representation such as:

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


71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
# File 'lib/arachni/element/form.rb', line 71

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
             value_or_info             = value_or_info.my_symbolize_keys
             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.



48
49
50
# File 'lib/arachni/element/form.rb', line 48

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.



44
45
46
# File 'lib/arachni/element/form.rb', line 44

def nonce_name
  @nonce_name
end

Class Method Details

.attributes_to_hash(attributes) ⇒ Object



457
458
459
# File 'lib/arachni/element/form.rb', line 457

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

.decode(str) ⇒ String

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

Parameters:

Returns:



479
480
481
# File 'lib/arachni/element/form.rb', line 479

def decode( str )
    URI.decode( str.to_s.recode.gsub( '+', ' ' ) )
end

.encode(str) ⇒ String

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

Parameters:

Returns:



467
468
469
470
471
472
# File 'lib/arachni/element/form.rb', line 467

def encode( str )
    ::URI.encode(
        ::URI.encode( str, '+%' ).recode.gsub( ' ', '+' ),
        ";&\\=\0"
    )
end

.from_document(url, document, ignore_scope = false) ⇒ Array<Form>

Extracts forms from an HTML document.

Parameters:

  • url (String)

    URL of the document – used for path normalization purposes.

  • document (String, Nokogiri::HTML::Document)

Returns:



389
390
391
392
393
394
395
396
397
398
# File 'lib/arachni/element/form.rb', line 389

def from_document( url, document, ignore_scope = false )
    document = Nokogiri::HTML( document.to_s ) if !document.is_a?( Nokogiri::HTML::Document )
    base_url = (document.search( '//base[@href]' )[0]['href'] rescue url)

    document.search( '//form' ).map do |node|
        next if !(form = from_node( base_url, node, ignore_scope ))
        form.url = url.freeze
        form
    end.compact
end

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



400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
# File 'lib/arachni/element/form.rb', line 400

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[:html]   = node.to_html.freeze

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

    %w(textarea input select button).each do |attr|
        options[attr] ||= []
        node.search( ".//#{attr}" ).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
                options[:inputs][name][:type]  ||= :text
                options[:inputs][name][:value] ||= ''
                next
            end

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

                    # Prefer the selected or first option.
                    if h[:selected]
                        h[:value] ||= child.text
                        options[:inputs][name] = h
                    else
                        h[:value] ||= child.text
                        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

    new options
end

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

Extracts forms by parsing the body of an HTTP response.

Parameters:

Returns:



378
379
380
# File 'lib/arachni/element/form.rb', line 378

def from_response( response, ignore_scope = false )
    from_document( response.url, response.body, ignore_scope )
end

Instance Method Details

#action=(url) ⇒ Object

@@return (see Capabilities::Submittable#action=)



109
110
111
112
113
114
115
116
117
118
# File 'lib/arachni/element/form.rb', line 109

def action=( url )
    if self.method == :get
        rewritten   = uri_parse( url ).rewrite
        self.inputs = rewritten.query_parameters.merge( self.inputs || {} )

        super rewritten.without_query
    else
        super url
    end
end

#audit_id(payload = nil) ⇒ String

Returns ID string used to identify the Capabilities::Auditable#audit of ‘self` by its Capabilities::WithAuditor#auditor.

Parameters:

Returns:



182
183
184
# File 'lib/arachni/element/form.rb', line 182

def audit_id( payload = nil )
    force_train? ? id : super( payload )
end

#audit_status_messageObject



164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
# File 'lib/arachni/element/form.rb', line 164

def audit_status_message
    override = nil
    if mutation_with_original_values?
        override = 'original'
    elsif mutation_with_sample_values?
        override = 'sample'
    end

    if override
        "Submitting form with #{override} values for #{inputs.keys.join(', ')}" <<
            " at '#{@action}'."
    else
        super
    end
end

#decode(str) ⇒ Object

See Also:



356
357
358
# File 'lib/arachni/element/form.rb', line 356

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.



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

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

#domDOM

Returns:



97
98
99
100
101
# File 'lib/arachni/element/form.rb', line 97

def dom
    return @dom if @dom
    return if !node || inputs.empty?
    super
end

#dupObject



360
361
362
363
364
365
366
367
368
369
# File 'lib/arachni/element/form.rb', line 360

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

#each_mutation(payload, opts = {}) {|elem| ... } ⇒ Object

Overrides Mutable#each_mutation adding support for mutations with:

  • Sample values (filled by OptionGroups::Input.fill).

  • Original values.

  • Password fields requiring identical values (in order to pass server-side validation)

Parameters:

Options Hash (opts):

  • :skip_original (Bool)

    Whether or not to skip adding a mutation holding original values and sample values.

Yields:

  • (elem)
  • (mutation)

    Each generated mutation.

Yield Parameters:

  • (Mutable)

See Also:



209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
# File 'lib/arachni/element/form.rb', line 209

def each_mutation( payload, opts = {} )
    opts = MUTATION_OPTIONS.merge( opts )

    generated = Arachni::Support::LookUp::HashSet.new( hasher: :mutable_id )

    super( payload, opts ) do |elem|
        elem.mirror_password_fields
        yield elem if !generated.include?( elem )
        generated << elem
    end

    return if opts[:skip_original]

    elem = self.dup
    elem.mutation_with_original_values
    elem.affected_input_name = ORIGINAL_VALUES
    yield elem if !generated.include?( elem )
    generated << elem

    # Default values, in case they reveal new resources.
    if node
        inputs.keys.each do |input|
            next if field_type_for( input ) != :select

            node.xpath( "select[@name=\"#{input}\"]" ).css('option').each do |option|
                try_input do
                    elem = self.dup
                    elem.mutation_with_original_values
                    elem.affected_input_name  = input
                    elem.affected_input_value = option['value'] || option.text
                    yield elem if !generated.include?( elem )
                    generated << elem
                end
            end

        end
    end

    try_input do
        # Sample values, in case they reveal new resources.
        elem = self.dup
        elem.inputs = Arachni::Options.input.fill( inputs.dup )
        elem.affected_input_name = SAMPLE_VALUES
        elem.mutation_with_sample_values
        yield elem if !generated.include?( elem )
        generated << elem
    end
end

#encode(str) ⇒ Object

See Also:



348
349
350
# File 'lib/arachni/element/form.rb', line 348

def encode( str )
    self.class.encode( str )
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

f = Form.from_document( 'http://stuff.com', html_form ).first

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:



340
341
342
# File 'lib/arachni/element/form.rb', line 340

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

#force_train?Boolean

Returns:

  • (Boolean)


103
104
105
# File 'lib/arachni/element/form.rb', line 103

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.



286
287
288
# File 'lib/arachni/element/form.rb', line 286

def has_nonce?
    !!nonce_name
end

#mirror_password_fieldsObject



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

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

#mutation_with_original_valuesObject



147
148
149
# File 'lib/arachni/element/form.rb', line 147

def mutation_with_original_values
    @mutation_with_original_values = true
end

#mutation_with_original_values?Bool

Returns ‘true` if the element has not been mutated, `false` otherwise.

Returns:

  • (Bool)

    ‘true` if the element has not been mutated, `false` otherwise.



143
144
145
# File 'lib/arachni/element/form.rb', line 143

def mutation_with_original_values?
    !!@mutation_with_original_values
end

#mutation_with_sample_valuesObject



160
161
162
# File 'lib/arachni/element/form.rb', line 160

def mutation_with_sample_values
    @mutation_with_sample_values = true
end

#mutation_with_sample_values?Bool

Returns ‘true` if the element has been populated with sample (OptionGroups::Input.fill) values, `false` otherwise.

Returns:

  • (Bool)

    ‘true` if the element has been populated with sample (OptionGroups::Input.fill) values, `false` otherwise.

See Also:



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

def mutation_with_sample_values?
    !!@mutation_with_sample_values
end

#name_or_idString

Returns Name of ID HTML attributes for this form.

Returns:

  • (String)

    Name of ID HTML attributes for this form.



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

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.



278
279
280
281
282
# File 'lib/arachni/element/form.rb', line 278

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.



137
138
139
# File 'lib/arachni/element/form.rb', line 137

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