Working with Ruby
Hi, I am Jan. This is my old Ruby blog. I still post about Ruby, but I now do it on idiosyncratic-ruby.com. You should also install Irbtools to improve your IRB.

Rack::NoTags

This is my submission for the CodeRack contest:

A middleware that removes < and > from all incoming requests.

About

Lots of XSS attacks try to inject some kinds of javascript. But almost every attack requires some html tags to be inserted (in which they can start javascript in some way).

This Rack middleware approaches this by radically removing all < and > in all incoming get/post paramaters.

Note: This means, if your site needs to send < and > in post or get requests, it probably will not work, anymore – you have to change your application design to use this middleware.

Currently, there are four modes available:

  • :brackets_only (default)
    substitutes < with &lt; and > with &gt;
  • :valid_xml
    substitutes < > & " ’ similar to Racks escape_html (also known as h)
  • :paranoid
    deletes < > &lt; &gt; %3C %3E &#60; &#62; &#x3c; &x3e; and similar variations

It is important to keep in mind: By using Rack::NoTags, your website is not suddenly 100% save from XSS attacks. Look at it as just another security-layer.

Example usage in Rails

In your config/environment.rb put

require 'path/to/rack-notags.rb' config.middleware.use Rack::NoTags

To activate a different filter mode, you can do it like this:

config.middleware.use Rack::NoTags, :paranoid

You can also deactivate the whole filter for a specific request, if one of the request paramaters matchs on of the entries on your ignore list (the last option):

config.middleware.use Rack::NoTags, :paranoid, :admin_field => 'supersecrethash'

The code

Also available as gist.

 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
module Rack

  # Sometimes, simple approaches to solve a problem are the best,
  # because of the danger, that complex ones have holes...
  #
  # Usage (Rails)
  #
  # In config/environment.rb add:
  # require 'path/to/rack-notags.rb'
  # config.middleware.use Rack::NoTags
  #
  # You can activate a different filter mode with:
  # config.middleware.use Rack::NoTags, :paranoid

  class NoTags
    PATTERNS = { # replacement => [ array, of, patterns ]
      :brackets_only => {
        '&lt;' => %w[ < %3C ],
        '&gt;' => %w[ > %3E ]
      },

      # similar to Racks escape_html + url_encoded variants
      :valid_xml => {
        '&lt;' => %w[ < %3C ],
        '&gt;' => %w[ > %3E ],
        '&amp;' => %w[ & %26 ],
        '&#39;' => %w[ ' %27 ],
        '&quot;' => %w[ " %22 ]
      },

      # encodings which might be interpreted as < or > in some situations
      :paranoid => {
        '' => %w[ < > %3C %3E ] + [
/&[lg]t;?/i,
/&#0{0,5}6[02];?/,
/&#x0{0,5}3[ce];?/i ]
      }
    }

    def initialize(app, mode = :brackets_only, ignore = {})
      @app = app
      @patterns = PATTERNS[mode.to_sym] # mode selects the right pattern set
      @ignore = ignore # if one entry of the ignore list matches a post param,
                        # nothing will be filtered
    end

    def call(env)
      # get params in a nice format
      post_params = Rack::Utils.parse_query(env['rack.input'].read, "&")
      get_params = Rack::Utils.parse_query(env['QUERY_STRING'], "&")


      # remove @patterns
      unless ignore?(post_params)
        post_params = strip_all(post_params)
        get_params = strip_all(get_params)
      end

      # update envirionment
      env['rack.input'] = StringIO.new(Rack::Utils.build_query(post_params))
      env['QUERY_STRING'] = Rack::Utils.build_query(get_params)

      # call framework
      @app.call(env)
    end

  private

    # check if param is on ignore list
    def ignore?(params)
      ret = false

      @ignore.each{ |ign_param, ign_value|
        params.each{ |param, value|
          if !value.is_a?(Array) &&
             ign_param.to_s == param.to_s &&
             ign_value.to_s == value.to_s

            ret = true
          end
        }
      }

      ret
    end

    # applies each 'to-substitute'-pattern to the string
    def strip(string)
      begin
        @patterns.each{ |replacement, patterns|
          patterns.each{ |pattern|
            string = string.gsub(pattern, replacement)
          }
        }
      end while catch :still_some do
        # check if there is still any pattern that needs to be aplied
        @patterns.each{ |_, patterns|
          patterns.each{ |pattern|
            if string[pattern] # like =~ but =~ is not
                               # defined for two strings
              throw :still_some, true
            end
          }
        }
        false
      end

      string
    end

    # looks at every param-element an sends it to the strip method
    def strip_all(params)
      ret = {}
      params.each{ |param, value|
        ret[strip(param)] = value.is_a?(Array) ? value.map{|v|strip(v)} : strip(value)
      }

      ret
    end

  end
end
(c) 2009 Jan Lelis .