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<
and > with>
:valid_xml
substitutes < > & " ’ similar to Racksescape_html
(also known ash
)
:paranoid
deletes < >< > %3C %3E < > < &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 => { '<' => %w[ < %3C ], '>' => %w[ > %3E ] }, # similar to Racks escape_html + url_encoded variants :valid_xml => { '<' => %w[ < %3C ], '>' => %w[ > %3E ], '&' => %w[ & %26 ], ''' => %w[ ' %27 ], '"' => %w[ " %22 ] }, # encodings which might be interpreted as < or > in some situations :paranoid => { '' => %w[ < > %3C %3E ] + [ /&[lg]t;?/i, /�{0,5}6[02];?/, /�{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