Known bugs or issues:
+- randomize each voting list
+
+- it'd be nice if the ordering were persistent, so I could drag
+ the candidates around, then come back later (loading the page anew),
+ make tweaks, and submit.
+
+- the results page says "the winner is Elizabeth Stark"; it'd be nice
+ if it were aware that it's a five-seat election and could say
+ explicitly who the whole board is.
+
+- it's probably a good thing that users can't check out what alternate
+ voting systems would have done at the click of a button. =)
+ (though of course they could work it out themselves.)
+
+- is there rhyme or reason to the ordering within each row of the
+ second table in pref_tables? it could be, eg, sorted by magnitude
+ of victory, narrowest first.
+
+- it'd be nice to attach names to the email identities, on eg the
+ details page; in this election we'd have said MIT for gregp@mit.edu,
+ Northeastern for cbudnick@gmail.com, etc. I don't know many of these
+ addresses, and names would be more informative. (They'd be more
+ helpful for auditing, too, as one could separately match names to the
+ correct list and match addresses to names.)
+
+- just a tiny suggestion - possibly it could be a good idea to add a
+ counter/line-count to the table with keys and vote results so in one
+ glance someone can see that the number of votes matches the number of
+ voters.
+
+
# Methods added to this helper will be available to all templates in the application.
module ApplicationHelper
-
end
<ol>
<% @election.ssd_result.ranked_candidates.each do |place| %>
- <li><%= h(place.collect {|c| @names[c].capitalize}.join( " <em>and</em> " )) %>
+ <li><%= white_list place.collect {|c| @names[c].capitalize}.join( " <em>and</em> " ) %>
<%= "<strong>(TIE)</strong>" if place.length > 1 %></li>
<% end %>
</ol>
<tr>
<td></td>
<% candidates.each do |candidate| -%>
- <th><%=h @names[candidate] -%></th>
+ <th><%= white_list(@names[candidate]) -%></th>
<% end -%>
</tr>
<% candidates.each do |winner| -%>
<tr>
- <th><%=h @names[winner] %></th>
+ <th><%= white_list(@names[winner]) %></th>
<% candidates.each do |loser| -%>
<% if winner == loser -%>
<td> -- </td>
<table class="preftable">
<% candidates.each do |victor| %>
<tr>
- <th><%=h @names[victor] %></th>
+ <th><%= white_list(@names[victor]) %></th>
<% victories[victor].keys.each do |loser| %>
<% margin = victories[victor][loser]%>
- <td><%=h @names[loser] %>
+ <td><%= white_list(@names[loser]) %>
<% if margin == 0%>
Tied!
<% else -%>
<p class="winner_text">
<% if result.winner? and result.winners.length == 1 -%>
The winner is:
- <strong><%=h @candidates[result.winner].name.capitalize %></strong>
+ <strong><%= white_list(@candidates[result.winner].name.capitalize) %></strong>
<% elsif result.winner? and result.winners.length > 1 %>
- There was a tie. The winners are: <strong><%=h( result.winners.collect {|w| @candidates[w].to_s.capitalize}.join(", ") )%></strong>
+ There was a tie. The winners are: <strong><%= white_list(result.winners.collect {|w| @candidates[w].to_s.capitalize}.join(", ") )%></strong>
<% else %>
<p>There is no winner using this method. </strong>
<% end %>
<ol id="rankings-list">
<% for ranking in @voter.vote.rankings %>
<li class="moveable" id="ranking_<%= ranking.candidate.id %>">
- <%=h ranking.candidate.name.capitalize %></li>
+ <%= white_list(ranking.candidate.name.capitalize) %></li>
<% end %>
</ol>
</div>
<p>The voting rolls -- displayed here in alphabetical order -- for the
last election are are follows.</p>
-<p>Information is displayed here to help voters verify that their own
+<p>Information is displayed here to help voters verifying that their own
vote was recorded correctly and that the election was not tampered
with.</p>
<tr>
<td>Candidate</td>
<% @election.approval_result.points.keys.sort.each do |candidate| %>
- <th><%=h @names[candidate] %></th>
+ <th><%= white_list(@names[candidate]) %></th>
<% end -%>
</tr>
<td><%= points %></td>
<% end -%>
</tr>
-</table>
\ No newline at end of file
+</table>
<% if flash[:candidate_names] %>
<ul>
<% for cand in flash[:candidate_names] %>
- <li><%=h cand.capitalize %></li>
+ <li><%= white_list(cand.capitalize) %></li>
<% end %>
</ul>
<% end %>
<ol>
<% for candidate in @election.candidates.sort %>
- <li><%=h candidate.name.capitalize %></li>
+ <li><%= white_list(candidate.name.capitalize) %></li>
<% end %>
</ol>
<ol>
<% for rank in @voter.vote.rankings.sort %>
- <li><%=h rank.candidate.name.capitalize %> </li>
+ <li><%= white_list(rank.candidate.name.capitalize) %> </li>
<% end %>
</ol>
</div>
<p>This page contains information useful for auditing elections and
-verify that votes were tabulated correctly.</p>
+verifying that votes were tabulated correctly.</p>
<p>The following invididuals (in random order) voted in this
election:</p>
<p>The column marked <em>Verification Token</em> lists tokens that were
given to voters at the time of voting. Voters can check to see that the
vote that corresponds to their token was recorded correctly. The column
-marks "vote" lists the candidates in order of the voter's preference. To
-read these votes, please refer to the key below.</p>
+marked <em>Vote</em> lists the candidates in order of the voter's
+preference. To read these votes, refer to the key below.</p>
<table class="preftable">
<tr>
+<th></th>
<th>Verification Token</th>
<th>Vote</th>
-<%- @votes.each do |vote| -%>
+<%- @votes.each_with_index do |vote, i| -%>
<tr>
+<td><%= i + 1 %></td>
<td><%= vote.token %></td><td><%= vote.votestring%></td>
</tr>
<%- end -%>
post :confirm, { 'ident' => 'variable', 'rankings-list' => votes.sort_by {rand} }
assert_redirected_to :controller => 'quickvote', :ident => 'variable'
end
+
def test_display_tainted_quickvote
+ # create quickvote with tainted data
test_create_quickvote
qv=QuickVote.ident_to_quickvote('variable')
qv.description="<object>foo</object>"
- qv.candidate_names = ["<object>foo", "bar<object>", "<foobar>"]
+ qv.candidate_names = ["<object>foo", "bar<object>", "<foobar>",
+ '<img src="foo" alt="bar" />']
qv.save!
+
+ # display the vote/index page and check for bad tags and the ability
+ # to make an image tag
get :index, { 'ident' => 'variable' }
assert_response :success
assert_no_tag :tag => "object"
assert_no_tag :tag => "foobar"
+ assert_tag :tag => "img",
+ :parent => { :tag => "li", :attributes => { :class => "moveable" } }
+
+ # actually vote
votes = QuickVote.ident_to_quickvote('variable').candidates.collect { |c| c.id}
post :confirm, { 'ident' => 'variable', 'rankings-list' => votes.sort_by {rand} }
+
+ # check for bad/good tags
assert_template('quickvote/thanks')
assert_no_tag :tag => "object"
assert_no_tag :tag => "foobar"
+ assert_tag :tag => "img", :parent => { :tag => "li" }
+
+ # get the results page and check for good/bad tags
get :results, { 'ident' => 'variable' }
assert_response :success
assert_no_tag :tag => "object"
assert_no_tag :tag => "foobar"
+ assert_tag :tag => "img", :parent => { :tag => "li" }
end
end
--- /dev/null
+WhiteList
+=========
+
+This White Listing helper will html encode all tags and strip all attributes that aren't specifically allowed.
+It also strips href/src tags with invalid protocols, like javascript: especially. It does its best to counter any
+tricks that hackers may use, like throwing in unicode/ascii/hex values to get past the javascript: filters. Check out
+the extensive test suite.
+
+ <%= white_list @article.body %>
+
+You can add or remove tags/attributes if you want to customize it a bit.
+
+Add table tags
+
+ WhiteListHelper.tags.merge %w(table td th)
+
+Remove tags
+
+ WhiteListHelper.tags.delete 'div'
+
+Change allowed attributes
+
+ WhiteListHelper.attributes.merge %w(id class style)
+
+white_list accepts a block for custom tag escaping. Shown below is the default block that white_list uses if none is given.
+The block is called for all bad tags, and every text node. node is an instance of HTML::Node (either HTML::Tag or HTML::Text).
+bad is nil for text nodes inside good tags, or is the tag name of the bad tag.
+
+ <%= white_list(@article.body) { |node, bad| white_listed_bad_tags.include?(bad) ? nil : node.to_s.gsub(/</, '<') } %>
\ No newline at end of file
--- /dev/null
+require 'rake'
+require 'rake/testtask'
+require 'rake/rdoctask'
+
+desc 'Default: run unit tests.'
+task :default => :test
+
+desc 'Test the white_list plugin.'
+Rake::TestTask.new(:test) do |t|
+ t.libs << 'lib'
+ t.pattern = 'test/**/*_test.rb'
+ t.verbose = true
+end
+
+desc 'Generate documentation for the white_list plugin.'
+Rake::RDocTask.new(:rdoc) do |rdoc|
+ rdoc.rdoc_dir = 'rdoc'
+ rdoc.title = 'WhiteList'
+ rdoc.options << '--line-numbers' << '--inline-source'
+ rdoc.rdoc_files.include('README')
+ rdoc.rdoc_files.include('lib/**/*.rb')
+end
--- /dev/null
+require 'white_list_helper'
+ActionView::Base.send :include, WhiteListHelper
\ No newline at end of file
--- /dev/null
+module WhiteListHelper
+ @@protocol_attributes = Set.new %w(src href)
+ @@protocol_separator = /:|(�*58)|(p)|(%|%)3A/
+ mattr_reader :protocol_attributes, :protocol_separator
+
+ def self.contains_bad_protocols?(white_listed_protocols, value)
+ value =~ protocol_separator && !white_listed_protocols.include?(value.split(protocol_separator).first)
+ end
+
+ klass = class << self; self; end
+ klass_methods = []
+ inst_methods = []
+ [:bad_tags, :tags, :attributes, :protocols].each do |attr|
+ # Add class methods to the module itself
+ klass_methods << <<-EOS
+ def #{attr}=(value) @@#{attr} = Set.new(value) end
+ def #{attr}() @@#{attr} end
+ EOS
+
+ # prefix the instance methods with white_listed_*
+ inst_methods << "def white_listed_#{attr}() ::WhiteListHelper.#{attr} end"
+ end
+
+ klass.class_eval klass_methods.join("\n"), __FILE__, __LINE__
+ class_eval inst_methods.join("\n"), __FILE__, __LINE__
+
+ # This White Listing helper will html encode all tags and strip all attributes that aren't specifically allowed.
+ # It also strips href/src tags with invalid protocols, like javascript: especially. It does its best to counter any
+ # tricks that hackers may use, like throwing in unicode/ascii/hex values to get past the javascript: filters. Check out
+ # the extensive test suite.
+ #
+ # <%= white_list @article.body %>
+ #
+ # You can add or remove tags/attributes if you want to customize it a bit.
+ #
+ # Add table tags
+ #
+ # WhiteListHelper.tags.merge %w(table td th)
+ #
+ # Remove tags
+ #
+ # WhiteListHelper.tags.delete 'div'
+ #
+ # Change allowed attributes
+ #
+ # WhiteListHelper.attributes.merge %w(id class style)
+ #
+ # white_list accepts a block for custom tag escaping. Shown below is the default block that white_list uses if none is given.
+ # The block is called for all bad tags, and every text node. node is an instance of HTML::Node (either HTML::Tag or HTML::Text).
+ # bad is nil for text nodes inside good tags, or is the tag name of the bad tag.
+ #
+ # <%= white_list(@article.body) { |node, bad| white_listed_bad_tags.include?(bad) ? nil : node.to_s.gsub(/</, '<') } %>
+ #
+ def white_list(html, options = {}, &block)
+ return html if html.blank? || !html.include?('<')
+ attrs = Set.new(options[:attributes]).merge(white_listed_attributes)
+ tags = Set.new(options[:tags] ).merge(white_listed_tags)
+ block ||= lambda { |node, bad| white_listed_bad_tags.include?(bad) ? nil : node.to_s.gsub(/</, '<') }
+ returning [] do |new_text|
+ tokenizer = HTML::Tokenizer.new(html)
+ bad = nil
+ while token = tokenizer.next
+ node = HTML::Node.parse(nil, 0, 0, token, false)
+ new_text << case node
+ when HTML::Tag
+ node.attributes.keys.each do |attr_name|
+ value = node.attributes[attr_name].to_s
+ if !attrs.include?(attr_name) || (protocol_attributes.include?(attr_name) && contains_bad_protocols?(value))
+ node.attributes.delete(attr_name)
+ else
+ node.attributes[attr_name] = CGI::escapeHTML(value)
+ end
+ end if node.attributes
+ if tags.include?(node.name)
+ bad = nil
+ node
+ else
+ bad = node.name
+ block.call node, bad
+ end
+ else
+ block.call node, bad
+ end
+ end
+ end.join
+ end
+
+ protected
+ def contains_bad_protocols?(value)
+ WhiteListHelper.contains_bad_protocols?(white_listed_protocols, value)
+ end
+end
+
+WhiteListHelper.bad_tags = %w(script)
+WhiteListHelper.tags = %w(strong em b i p code pre tt output samp kbd var sub sup dfn cite big small address hr br div span h1 h2 h3 h4 h5 h6 ul ol li dt dd abbr acronym a img blockquote del ins fieldset legend)
+WhiteListHelper.attributes = %w(href src width height alt cite datetime title class)
+WhiteListHelper.protocols = %w(ed2k ftp http https irc mailto news gopher nntp telnet webcal xmpp callto feed)
\ No newline at end of file
--- /dev/null
+require 'test/unit'
+require File.expand_path(File.join(File.dirname(__FILE__), '../../../../config/environment.rb'))
+
+class WhiteListTest < Test::Unit::TestCase
+ include WhiteListHelper
+ public :contains_bad_protocols?
+
+ WhiteListHelper.tags.each do |tag_name|
+ define_method "test_should_allow_#{tag_name}_tag" do
+ assert_white_listed "start <#{tag_name} title=\"1\" name=\"foo\">foo <bad>bar</bad> baz</#{tag_name}> end", %(start <#{tag_name} title="1">foo <bad>bar</bad> baz</#{tag_name}> end)
+ end
+ end
+
+ def test_should_allow_anchors
+ assert_white_listed %(<a href="foo" onclick="bar"><script>baz</script></a>), %(<a href="foo"></a>)
+ end
+
+ %w(src width height alt).each do |img_attr|
+ define_method "test_should_allow_image_#{img_attr}_attribute" do
+ assert_white_listed %(<img #{img_attr}="foo" onclick="bar" />), %(<img #{img_attr}="foo" />)
+ end
+ end
+
+ def test_should_handle_non_html
+ assert_white_listed 'abc'
+ end
+
+ def test_should_handle_blank_text
+ assert_white_listed nil
+ assert_white_listed ''
+ end
+
+ def test_should_allow_custom_tags
+ text = "<u>foo</u>"
+ assert_equal(text, white_list(text, :tags => %w(u)))
+ end
+
+ def test_should_allow_custom_tags_with_attributes
+ text = %(<fieldset foo="bar">foo</fieldset>)
+ assert_equal(text, white_list(text, :attributes => ['foo']))
+ end
+
+ [%w(img src), %w(a href)].each do |(tag, attr)|
+ define_method "test_should_strip_#{attr}_attribute_in_#{tag}_with_bad_protocols" do
+ assert_white_listed %(<#{tag} #{attr}="javascript:bang" title="1">boo</#{tag}>), %(<#{tag} title="1">boo</#{tag}>)
+ end
+ end
+
+ def test_should_flag_bad_protocols
+ %w(about chrome data disk hcp help javascript livescript lynxcgi lynxexec ms-help ms-its mhtml mocha opera res resource shell vbscript view-source vnd.ms.radio wysiwyg).each do |proto|
+ assert contains_bad_protocols?("#{proto}://bad")
+ end
+ end
+
+ def test_should_accept_good_protocols
+ WhiteListHelper.protocols.each do |proto|
+ assert !contains_bad_protocols?("#{proto}://good")
+ end
+ end
+
+ def test_should_reject_hex_codes_in_protocol
+ assert contains_bad_protocols?("%6A%61%76%61%73%63%72%69%70%74%3A%61%6C%65%72%74%28%22%58%53%53%22%29")
+ assert_white_listed %(<a href="%6A%61%76%61%73%63%72%69%70%74%3A%61%6C%65%72%74%28%22%58%53%53%22%29">1</a>), "<a>1</a>"
+ end
+
+ def test_should_block_script_tag
+ assert_white_listed %(<SCRIPT\nSRC=http://ha.ckers.org/xss.js></SCRIPT>), ""
+ end
+
+ [%(<IMG SRC="javascript:alert('XSS');">),
+ %(<IMG SRC=javascript:alert('XSS')>),
+ %(<IMG SRC=JaVaScRiPt:alert('XSS')>),
+ %(<IMG """><SCRIPT>alert("XSS")</SCRIPT>">),
+ %(<IMG SRC=javascript:alert("XSS")>),
+ %(<IMG SRC=javascript:alert(String.fromCharCode(88,83,83))>),
+ %(<IMG SRC=javascript:alert('XSS')>),
+ %(<IMG SRC=javascript:alert('XSS')>),
+ %(<IMG SRC=javascript:alert('XSS')>),
+ %(<IMG SRC="jav\tascript:alert('XSS');">),
+ %(<IMG SRC="jav	ascript:alert('XSS');">),
+ %(<IMG SRC="jav
ascript:alert('XSS');">),
+ %(<IMG SRC="jav
ascript:alert('XSS');">),
+ %(<IMG SRC="  javascript:alert('XSS');">),
+ %(<IMG SRC=`javascript:alert("RSnake says, 'XSS'")`>)].each_with_index do |img_hack, i|
+ define_method "test_should_not_fall_for_xss_image_hack_#{i}" do
+ assert_white_listed img_hack, "<img>"
+ end
+ end
+
+ def test_should_sanitize_tag_broken_up_by_null
+ assert_white_listed %(<SCR\0IPT>alert(\"XSS\")</SCR\0IPT>), "<scr>alert(\"XSS\")</scr>"
+ end
+
+ def test_should_sanitize_invalid_script_tag
+ assert_white_listed %(<SCRIPT/XSS SRC="http://ha.ckers.org/xss.js"></SCRIPT>), ""
+ end
+
+ def test_should_sanitize_script_tag_with_multiple_open_brackets
+ assert_white_listed %(<<SCRIPT>alert("XSS");//<</SCRIPT>), "<"
+ assert_white_listed %(<iframe src=http://ha.ckers.org/scriptlet.html\n<), %(<iframe src="http:" /><)
+ end
+
+ def test_should_sanitize_unclosed_script
+ assert_white_listed %(<SCRIPT SRC=http://ha.ckers.org/xss.js?<B>), "<b>"
+ end
+
+ def test_should_sanitize_half_open_scripts
+ assert_white_listed %(<IMG SRC="javascript:alert('XSS')"), "<img>"
+ end
+
+ def test_should_not_fall_for_ridiculous_hack
+ img_hack = %(<IMG\nSRC\n=\n"\nj\na\nv\na\ns\nc\nr\ni\np\nt\n:\na\nl\ne\nr\nt\n(\n'\nX\nS\nS\n'\n)\n"\n>)
+ assert_white_listed img_hack, "<img>"
+ end
+
+ def test_should_allow_custom_block
+ html = %(<SCRIPT type="javascript">foo</SCRIPT><img>blah</img><blink>blah</blink>)
+ safe = white_list html do |node, bad|
+ bad == 'script' ? nil : node
+ end
+ assert_equal "<img>blah</img><blink>blah</blink>", safe
+ end
+
+ def test_should_sanitize_attributes
+ assert_white_listed %(<SPAN title="'><script>alert()</script>">blah</SPAN>), %(<span title="'><script>alert()</script>">blah</span>)
+ end
+
+ protected
+ def assert_white_listed(text, expected = nil)
+ assert_equal((expected || text), white_list(text))
+ end
+end