davideisinger.com

My personal website
Log | Files | Refs | README

index.md (3564B)


      1 ---
      2 title: "HTML Sanitization In Rails That Actually Works"
      3 date: 2009-11-23T00:00:00+00:00
      4 draft: false
      5 canonical_url: https://www.viget.com/articles/html-sanitization-in-rails-that-actually-works/
      6 ---
      7 
      8 Assuming you don't want to simply escape everything, sanitizing user
      9 input is one of the relative weak points of the Rails framework. On
     10 [SpeakerRate](http://speakerrate.com/), where users can use
     11 [Markdown](http://daringfireball.net/projects/markdown/) to format
     12 comments and descriptions, we've run up against some of the limitations
     13 of Rails' built-in sanitization features, so we decided to dig in and
     14 fix it ourselves.
     15 
     16 In creating our own sanitizer, our goals were threefold: we want to
     17 **let a subset of HTML in**. As the [Markdown
     18 documentation](http://daringfireball.net/projects/markdown/syntax#html)
     19 clearly states, "for any markup that is not covered by Markdown's
     20 syntax, you simply use HTML itself." In keeping with the Markdown
     21 philosophy, we can't simply strip all HTML from incoming comments, so
     22 the included
     23 [HTML::WhiteListSanitizer](https://github.com/rails/rails/blob/master/actionpack/lib/action_controller/vendor/html-scanner/html/sanitizer.rb#LID60)
     24 is the obvious starting point.
     25 
     26 Additionally, we want to **escape, rather than remove, non-approved
     27 tags**, since some commenters want to discuss the merits of, say,
     28 [`<h2 class="h2">`](http://speakerrate.com/talks/1698-object-oriented-css#c797).
     29 Contrary to its documentation, WhiteListSanitizer simply removes all
     30 non-whitelisted tags. Someone opened a
     31 [ticket](https://rails.lighthouseapp.com/projects/8994-ruby-on-rails/tickets/916)
     32 about this issue in August of 2008 with an included patch, but the
     33 ticket was marked as resolved without ever applying it. Probably for the
     34 best, as the patch introduces a new bug.
     35 
     36 Finally, we want to **escape unclosed tags even if they belong to the
     37 whitelist**. An unclosed `<strong>` tag can wreak havoc on the rest of a
     38 page, not to mention what a `<div>` can do. Self-closing tags are okay.
     39 
     40 With these requirements in mind, we subclassed HTML::WhiteListSanitizer
     41 and fixed it up. Introducing, then:
     42 
     43 {{<dither jason_statham.jpg "" "inline">}}Jason Statham stands in a sharp suit, glaring intently under the midday sun.{{</dither>}}
     44 
     45 [**HTML::StathamSanitizer**](https://gist.github.com/241114).
     46 User-generated markup, you're on notice: this sanitizer will take its
     47 shirt off and use it to kick your ass. At this point, I've written more
     48 about the code than code itself, so without further ado:
     49 
     50 ```ruby
     51 module HTML
     52   class StathamSanitizer < WhiteListSanitizer
     53 
     54     protected
     55 
     56     def tokenize(text, options)
     57       super.map do |token|
     58         if token.is_a?(HTML::Tag) && options[:parent].include?(token.name)
     59           token.to_s.gsub(/</, "&lt;")
     60         else
     61           token
     62         end
     63       end
     64     end
     65 
     66     def process_node(node, result, options)
     67       result << case node
     68         when HTML::Tag
     69           if node.closing == :close && options[:parent].first == node.name
     70             options[:parent].shift
     71           elsif node.closing != :self
     72             options[:parent].unshift node.name
     73           end
     74 
     75           process_attributes_for node, options
     76 
     77           if options[:tags].include?(node.name)
     78             node
     79           else
     80             bad_tags.include?(node.name) ? nil : node.to_s.gsub(/</, "&lt;")
     81           end
     82         else
     83           bad_tags.include?(options[:parent].first) ? nil : node.to_s.gsub(/</, "&lt;")
     84       end
     85     end
     86   end
     87 end
     88 ```
     89 
     90 As always, download and fork [at the
     91 'hub](https://gist.github.com/241114).