class Railroader::CheckCrossSiteScripting

This check looks for unescaped output in templates which contains parameters or model attributes.

For example:

<%= User.find(:id).name %> <%= params %>

Constants

CGI
FORM_BUILDER
HAML_HELPERS
IGNORE_LIKE
IGNORE_MODEL_METHODS

Model methods which are known to be harmless

MODEL_METHODS
URI
XML_HELPER

Public Instance Methods

actually_process_call(exp) click to toggle source
# File lib/railroader/checks/check_cross_site_scripting.rb, line 208
def actually_process_call exp
  return if @matched
  target = exp.target
  if sexp? target
    target = process target
  end

  method = exp.method

  # Ignore safe items
  if ignore_call? target, method
    @matched = false
  elsif sexp? target and model_name? target[1] # TODO: use method call?
    @matched = Match.new(:model, exp)
  elsif cookies? exp
    @matched = Match.new(:cookies, exp)
  elsif @inspect_arguments and params? exp
    @matched = Match.new(:params, exp)
  elsif @inspect_arguments
    process_call_args exp
  end
end
cgi_escaped?(target, method) click to toggle source
# File lib/railroader/checks/check_cross_site_scripting.rb, line 361
def cgi_escaped? target, method
  method == :escape and
  (target == URI or target == CGI)
end
check_for_immediate_xss(exp) click to toggle source
# File lib/railroader/checks/check_cross_site_scripting.rb, line 55
def check_for_immediate_xss exp
  return :duplicate if duplicate? exp

  if exp.node_type == :output
    out = exp.value
  elsif exp.node_type == :escaped_output
    if raw_call? exp
      out = exp.value.first_arg
    elsif html_safe_call? exp
      out = exp.value.target
    end
  end

  return if call? out and ignore_call? out.target, out.method

  if input = has_immediate_user_input?(out)
    add_result exp

    message = "Unescaped #{friendly_type_of input}"

    warn :template => @current_template,
      :warning_type => "Cross-Site Scripting",
      :warning_code => :cross_site_scripting,
      :message => message,
      :code => input.match,
      :confidence => :high

  elsif not tracker.options[:ignore_model_output] and match = has_immediate_model?(out)
    method = if call? match
               match.method
             else
               nil
             end

    unless IGNORE_MODEL_METHODS.include? method
      add_result exp

      if likely_model_attribute? match
        confidence = :high
      else
        confidence = :medium
      end

      message = "Unescaped model attribute"
      link_path = "cross_site_scripting"
      warning_code = :cross_site_scripting

      if node_type?(out, :call, :safe_call, :attrasgn, :safe_attrasgn) && out.method == :to_json
        message += " in JSON hash"
        link_path += "_to_json"
        warning_code = :xss_to_json
      end

      warn :template => @current_template,
        :warning_type => "Cross-Site Scripting",
        :warning_code => warning_code,
        :message => message,
        :code => match,
        :confidence => confidence,
        :link_path => link_path
    end

  else
    false
  end
end
form_builder_method?(target, method) click to toggle source
# File lib/railroader/checks/check_cross_site_scripting.rb, line 374
def form_builder_method? target, method
  target == FORM_BUILDER and @ignore_methods.include? method
end
haml_escaped?(target, method) click to toggle source
# File lib/railroader/checks/check_cross_site_scripting.rb, line 366
def haml_escaped? target, method
  method == :html_escape and target == HAML_HELPERS
end
html_safe_call?(exp) click to toggle source
# File lib/railroader/checks/check_cross_site_scripting.rb, line 336
def html_safe_call? exp
  call? exp.value and exp.value.method == :html_safe
end
ignore_call?(target, method) click to toggle source
# File lib/railroader/checks/check_cross_site_scripting.rb, line 340
def ignore_call? target, method
  ignored_method?(target, method) or
  safe_input_attribute?(target, method) or
  ignored_model_method?(target, method) or
  form_builder_method?(target, method) or
  haml_escaped?(target, method) or
  boolean_method?(method) or
  cgi_escaped?(target, method) or
  xml_escaped?(target, method)
end
ignored_method?(target, method) click to toggle source
# File lib/railroader/checks/check_cross_site_scripting.rb, line 357
def ignored_method? target, method
  @ignore_methods.include? method or method.to_s =~ IGNORE_LIKE
end
ignored_model_method?(target, method) click to toggle source
# File lib/railroader/checks/check_cross_site_scripting.rb, line 351
def ignored_model_method? target, method
  ((@matched and @matched.type == :model) or
     model_name? target) and
     IGNORE_MODEL_METHODS.include? method
end
likely_model_attribute?(exp) click to toggle source

Call already involves a model, but might not be acting on a record

# File lib/railroader/checks/check_cross_site_scripting.rb, line 123
def likely_model_attribute? exp
  return false unless call? exp

  method = exp.method

  if MODEL_METHODS.include? method or method.to_s.start_with? "find_by_"
    true
  else
    likely_model_attribute? exp.target
  end
end
process_call(exp) click to toggle source

Check a call for user input

Since we want to report an entire call and not just part of one, use @mark to mark when a call is started. Any dangerous values inside will then report the entire call chain.

# File lib/railroader/checks/check_cross_site_scripting.rb, line 161
def process_call exp
  if @mark
    actually_process_call exp
  else
    @mark = true
    actually_process_call exp
    message = nil

    if @matched
      unless @matched.type and tracker.options[:ignore_model_output]
        message = "Unescaped #{friendly_type_of @matched}"
      end

      if message and not duplicate? exp
        add_result exp

        link_path = "cross_site_scripting"
        warning_code = :cross_site_scripting

        if @known_dangerous.include? exp.method
          confidence = :high
          if exp.method == :to_json
            message += " in JSON hash"
            link_path += "_to_json"
            warning_code = :xss_to_json
          end
        else
          confidence = :weak
        end

        warn :template => @current_template,
          :warning_type => "Cross-Site Scripting",
          :warning_code => warning_code,
          :message => message,
          :code => exp,
          :user_input => @matched,
          :confidence => confidence,
          :link_path => link_path
      end
    end

    @mark = @matched = false
  end

  exp
end
process_case(exp) click to toggle source
# File lib/railroader/checks/check_cross_site_scripting.rb, line 270
def process_case exp
  # Ignore user input in case value
  # TODO: also ignore when values

  current = 2
  while current < exp.length
    process exp[current] if exp[current]
    current += 1
  end

  exp
end
process_cookies(exp) click to toggle source

Note that cookies have been found

# File lib/railroader/checks/check_cross_site_scripting.rb, line 238
def process_cookies exp
  @matched = Match.new(:cookies, exp)
  exp
end
process_dstr(exp) click to toggle source

Process as default

# File lib/railroader/checks/check_cross_site_scripting.rb, line 249
def process_dstr exp
  process_default exp
end
process_escaped_output(exp) click to toggle source

Look for calls to raw() Otherwise, ignore

# File lib/railroader/checks/check_cross_site_scripting.rb, line 142
def process_escaped_output exp
  unless check_for_immediate_xss exp
    if not duplicate? exp
      if raw_call? exp
        process exp.value.first_arg
      elsif html_safe_call? exp
        process exp.value.target
      end
    end
  end
  exp
end
process_format(exp) click to toggle source

Process as default

# File lib/railroader/checks/check_cross_site_scripting.rb, line 254
def process_format exp
  process_default exp
end
process_format_escaped(exp) click to toggle source

Ignore output HTML escaped via HAML

# File lib/railroader/checks/check_cross_site_scripting.rb, line 259
def process_format_escaped exp
  exp
end
process_if(exp) click to toggle source

Ignore condition in if Sexp

# File lib/railroader/checks/check_cross_site_scripting.rb, line 264
def process_if exp
  process exp.then_clause if sexp? exp.then_clause
  process exp.else_clause if sexp? exp.else_clause
  exp
end
process_output(exp) click to toggle source

Process an output Sexp

# File lib/railroader/checks/check_cross_site_scripting.rb, line 136
def process_output exp
  process exp.value.dup
end
process_params(exp) click to toggle source

Note that params have been found

# File lib/railroader/checks/check_cross_site_scripting.rb, line 232
def process_params exp
  @matched = Match.new(:params, exp)
  exp
end
process_render(exp) click to toggle source

Ignore calls to render

# File lib/railroader/checks/check_cross_site_scripting.rb, line 244
def process_render exp
  exp
end
raw_call?(exp) click to toggle source
# File lib/railroader/checks/check_cross_site_scripting.rb, line 332
def raw_call? exp
  exp.value.node_type == :call and exp.value.method == :raw
end
run_check() click to toggle source

Run check

# File lib/railroader/checks/check_cross_site_scripting.rb, line 37
def run_check
  setup

  tracker.each_template do |name, template|
    Railroader.debug "Checking #{name} for XSS"

    @current_template = template

    template.each_output do |out|
      unless check_for_immediate_xss out
        @matched = false
        @mark = false
        process out
      end
    end
  end
end
safe_input_attribute?(target, method) click to toggle source
# File lib/railroader/checks/check_cross_site_scripting.rb, line 378
def safe_input_attribute? target, method
  target and always_safe_method? method
end
setup() click to toggle source
# File lib/railroader/checks/check_cross_site_scripting.rb, line 283
def setup
  @ignore_methods = Set[:==, :!=, :button_to, :check_box, :content_tag, :escapeHTML, :escape_once,
                        :field_field, :fields_for, :h, :hidden_field,
                        :hidden_field, :hidden_field_tag, :image_tag, :label,
                        :link_to, :mail_to, :radio_button, :select,
                        :submit_tag, :text_area, :text_field,
                        :text_field_tag, :url_encode, :u, :url_for,
                        :will_paginate].merge tracker.options[:safe_methods]

  @models = tracker.models.keys
  @inspect_arguments = tracker.options[:check_arguments]

  @known_dangerous = Set[:truncate, :concat]

  if version_between? "2.0.0", "3.0.5"
    @known_dangerous << :auto_link
  elsif version_between? "3.0.6", "3.0.99"
    @ignore_methods << :auto_link
  end

  if version_between? "2.0.0", "2.3.14" or tracker.config.gem_version(:'rails-html-sanitizer') == '1.0.2'
    @known_dangerous << :strip_tags
  end

  if tracker.config.has_gem? :'rails-html-sanitizer' and
     version_between? "1.0.0", "1.0.2", tracker.config.gem_version(:'rails-html-sanitizer')

    @known_dangerous << :sanitize
  end

  json_escape_on = false
  initializers = tracker.check_initializers :ActiveSupport, :escape_html_entities_in_json=
  initializers.each {|result| json_escape_on = true?(result.call.first_arg) }

  if tracker.config.escape_html_entities_in_json?
      json_escape_on = true
  elsif version_between? "4.0.0", "9.9.9"
    json_escape_on = true
  end

  if !json_escape_on or version_between? "0.0.0", "2.0.99"
    @known_dangerous << :to_json
    Railroader.debug("Automatic to_json escaping not enabled, consider to_json dangerous")
  else
    @safe_input_attributes << :to_json
    Railroader.debug("Automatic to_json escaping is enabled.")
  end
end
xml_escaped?(target, method) click to toggle source
# File lib/railroader/checks/check_cross_site_scripting.rb, line 370
def xml_escaped? target, method
  method == :escape_xml and target == XML_HELPER
end