class Arachni::Element::Form

Represents an auditable form element

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

Constants

DECODE_CACHE
ORIGINAL_VALUES
SAMPLE_VALUES

Attributes

name[RW]

@return [String, nil]

Name of the form, if it has one.
nonce_name[R]

@return [String]

The name of the input name that holds the nonce.

Private Class Methods

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

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

@param [String] string

@return [String]

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

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

@param [String] string

@return [String]

# File lib/arachni/element/form.rb, line 403
def encode( string )
    Arachni::HTTP::Request.encode string
end
from_node( url, node, ignore_scope = false ) click to toggle source
# 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 ) click to toggle source

Extracts forms from an HTML document.

@param [Arachni::Parser] parser

@return [Array<Form>]

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

Extracts forms by parsing the body of an HTTP response.

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

@return [Array<Form>]

# 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 ) click to toggle source
Calls superclass method Arachni::Element::Base::from_rpc_data
# 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 ) click to toggle source
# File lib/arachni/element/form.rb, line 276
def in_html?( html )
    html.has_html_tag? 'form'
end
new( options ) click to toggle source

@param [Hash] options @option options [String] :name

Form name.

@option options [String] :id

Form ID.

@option options [String] :method (:get)

Form method.

@option options [String] :url

URL of the page which includes the form.

@option options [String] :action

Form action -- defaults to `:url`.

@option options [Hash] :inputs

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

    {
        'my_token'  => {
            type:  :hidden,
            value: 'token-value'
        }
    }
# 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
parse_data( data, boundary ) click to toggle source

@param [String] data

`multipart/form-data` text.

@param [String] boundary

`multipart/form-data` boundary.

@return [Hash]

Name-value pairs.
# 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

Private Instance Methods

audit_single( payload, opts = {}, &block ) click to toggle source
Calls superclass method
# File lib/arachni/element/form.rb, line 449
def audit_single( payload, opts = {}, &block )
    opts = opts.dup

    if (each_m = opts.delete(:each_mutation))
        opts[:each_mutation] = proc do |mutation|
            next if mutation.mutation_with_original_values? ||
                mutation.mutation_with_sample_values?

            each_m.call( mutation )
        end
    end

    super( payload, opts, &block )
end
decode( str ) click to toggle source

@see .decode

# File lib/arachni/element/form.rb, line 229
def decode( str )
    self.class.decode( str )
end
details_for( input ) click to toggle source

@param [String] input

Input name.

@return [Hash]

Information about the given input's attributes.
# File lib/arachni/element/form.rb, line 119
def details_for( input )
    @input_details[input.to_s] || {}
end
dup() click to toggle source
# 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 ) click to toggle source

@see .encode

# File lib/arachni/element/form.rb, line 224
def encode( str )
    self.class.encode( str )
end
fake_field?( name ) click to toggle source
# File lib/arachni/element/form.rb, line 219
def fake_field?( name )
    field_type_for( name ) == :fake
end
field_type_for( name ) click to toggle source

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

@example

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

@param [String] name

Field name.

@return [String]

# File lib/arachni/element/form.rb, line 215
def field_type_for( name )
    (details_for( name )[:type] || :text).to_sym
end
force_train?() click to toggle source
# File lib/arachni/element/form.rb, line 110
def force_train?
    mutation_with_original_values? || mutation_with_sample_values?
end
has_nonce?() click to toggle source

@return [Bool]

`true` if the form contains a nonce, `false` otherwise.
# File lib/arachni/element/form.rb, line 163
def has_nonce?
    !!nonce_name
end
http_request( options, &block ) click to toggle source
# File lib/arachni/element/form.rb, line 473
def http_request( options, &block )
    if force_train? && options[:train] != false
        print_debug 'Submitting form with default or sample values,' <<
                        ' overriding trainer option.'
        options[:train] = true
        print_debug_trainer( options )
    end

    options = options.dup

    if has_nonce?
        print_info "Refreshing nonce for '#{nonce_name}'."

        if !refresh
            print_bad 'Could not refresh nonce because the original form ' <<
                          'could not be found.'
        else
            print_info "Got new nonce '#{inputs[nonce_name]}'."
            options[:mode]       = :sync
            options[:parameters] = inputs
        end
    end

    self.method == :post ?
        http.post( self.action, options, &block ) :
        http.get( self.action, options, &block )
end
mirror_password_fields() click to toggle source
# 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_id() click to toggle source

@return [String]

Name of ID HTML attributes for this form.
# File lib/arachni/element/form.rb, line 125
def name_or_id
    name || @id
end
nonce_name=( field_name ) click to toggle source

When ‘nonce_name` is set the value of the equivalent input will be refreshed every time the form is to be submitted.

Use only when strictly necessary because it comes with a hefty performance penalty as the operation will need to be in blocking mode.

Will raise an exception if ‘field_name` could not be found in the form’s inputs.

@example

Form.new( 'http://stuff.com', { nonce_input: '' } ).nonce_name = 'blah'
#=> #<Error::FieldNotFound: Could not find field named 'blah'.>

@param [String] field_name

Name of the field holding the nonce.

@raise [Error::FieldNotFound]

If `field_name` is not a form input.
# File lib/arachni/element/form.rb, line 184
def nonce_name=( field_name )
    if !has_inputs?( field_name )
        fail Error::FieldNotFound, "Could not find field named '#{field_name}'."
    end
    @nonce_name = field_name
end
requires_password=( bool ) click to toggle source
# File lib/arachni/element/form.rb, line 443
def requires_password=( bool )
    @requires_password = bool
end
requires_password?() click to toggle source

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

@return [Bool]

`true` if the form contains passwords fields, `false` otherwise.
# 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
simple() click to toggle source

@return [Hash]

A simple representation of self including attributes and inputs.
# File lib/arachni/element/form.rb, line 131
def simple
    @initialization_options.merge( url: url, action: action, inputs: inputs )
end
skip?( elem ) click to toggle source
# File lib/arachni/element/form.rb, line 464
def skip?( elem )
    if elem.mutation_with_original_values? || elem.mutation_with_sample_values?
        id = elem.audit_id
        return true if audited?( id )
        audited( id )
    end
    false
end