Feature #32424 » 0007-Relax-allowed-protocols-in-links-by-denying-specific.patch
| lib/redmine/helpers/url.rb | ||
|---|---|---|
| 22 | 22 |
module Redmine |
| 23 | 23 |
module Helpers |
| 24 | 24 |
module URL |
| 25 |
# safe for resources fetched without user interaction? |
|
| 25 | 26 |
def uri_with_safe_scheme?(uri, schemes = ['http', 'https', 'ftp', 'mailto', nil]) |
| 26 | 27 |
# URLs relative to the current document or document root (without a protocol |
| 27 | 28 |
# separator, should be harmless |
| ... | ... | |
| 32 | 33 |
rescue URI::Error |
| 33 | 34 |
false |
| 34 | 35 |
end |
| 36 | ||
| 37 |
# safe to render links to given uri? |
|
| 38 |
def uri_with_link_safe_scheme?(uri) |
|
| 39 |
# regexp adapted from Sanitize (we need to catch even invalid protocol specs) |
|
| 40 |
return true unless uri =~ /\A\s*([^\/#]*?)(?:\:|�*58|�*3a)/i |
|
| 41 | ||
| 42 |
# absolute scheme |
|
| 43 |
scheme = $1.downcase |
|
| 44 |
return false unless /\A[a-z][a-z0-9\+\.\-]*\z/.match?(scheme) # RFC 3986 |
|
| 45 | ||
| 46 |
%w(data javascript vbscript).none?(scheme) |
|
| 47 |
end |
|
| 35 | 48 |
end |
| 36 | 49 |
end |
| 37 | 50 |
end |
| lib/redmine/wiki_formatting/common_mark/sanitization_filter.rb | ||
|---|---|---|
| 22 | 22 |
module CommonMark |
| 23 | 23 |
# sanitizes rendered HTML using the Sanitize gem |
| 24 | 24 |
class SanitizationFilter < HTML::Pipeline::SanitizationFilter |
| 25 |
include Redmine::Helpers::URL |
|
| 26 |
RELAXED_PROTOCOL_ATTRS = {
|
|
| 27 |
"a" => %w(href).freeze, |
|
| 28 |
}.freeze |
|
| 29 | ||
| 25 | 30 |
def whitelist |
| 26 | 31 |
@@whitelist ||= customize_whitelist(super.deep_dup) |
| 27 | 32 |
end |
| ... | ... | |
| 72 | 77 |
node.remove_attribute("id")
|
| 73 | 78 |
} |
| 74 | 79 | |
| 75 |
# allow the same set of URL schemes for links as is the default in |
|
| 76 |
# Redmine::Helpers::URL#uri_with_safe_scheme? |
|
| 77 |
whitelist[:protocols]["a"]["href"] = [ |
|
| 78 |
'http', 'https', 'ftp', 'mailto', :relative |
|
| 79 |
] |
|
| 80 |
# https://github.com/rgrove/sanitize/issues/209 |
|
| 81 |
whitelist[:protocols].delete("a")
|
|
| 82 |
whitelist[:transformers].push lambda{|env|
|
|
| 83 |
node = env[:node] |
|
| 84 |
return if node.type != Nokogiri::XML::Node::ELEMENT_NODE |
|
| 85 | ||
| 86 |
name = env[:node_name] |
|
| 87 |
return unless RELAXED_PROTOCOL_ATTRS.include?(name) |
|
| 88 | ||
| 89 |
RELAXED_PROTOCOL_ATTRS[name].each do |attr| |
|
| 90 |
next unless node.has_attribute?(attr) |
|
| 91 | ||
| 92 |
node[attr] = node[attr].strip |
|
| 93 |
unless !node[attr].empty? && uri_with_link_safe_scheme?(node[attr]) |
|
| 94 |
node.remove_attribute(attr) |
|
| 95 |
end |
|
| 96 |
end |
|
| 97 |
} |
|
| 80 | 98 | |
| 81 | 99 |
whitelist |
| 82 | 100 |
end |
| test/unit/lib/redmine/helpers/url_test.rb | ||
|---|---|---|
| 33 | 33 |
assert_not uri_with_safe_scheme?("httpx://example.com/")
|
| 34 | 34 |
assert_not uri_with_safe_scheme?("mailto:root@")
|
| 35 | 35 |
end |
| 36 | ||
| 37 |
LINK_SAFE_URIS = [ |
|
| 38 |
"http://example.com/", |
|
| 39 |
"https://example.com/", |
|
| 40 |
"ftp://example.com/", |
|
| 41 |
"foo://example.org", |
|
| 42 |
"mailto:foo@example.org", |
|
| 43 |
" http://example.com/", |
|
| 44 |
"", |
|
| 45 |
"/javascript:alert(\'filename\')", |
|
| 46 |
] |
|
| 47 | ||
| 48 |
def test_uri_with_link_safe_scheme_should_recognize_safe_uris |
|
| 49 |
LINK_SAFE_URIS.each do |uri| |
|
| 50 |
assert uri_with_link_safe_scheme?(uri), "'#{uri}' should be safe"
|
|
| 51 |
end |
|
| 52 |
end |
|
| 53 | ||
| 54 |
LINK_UNSAFE_URIS = [ |
|
| 55 |
"javascript:alert(\'XSS\');", |
|
| 56 |
"javascript :alert(\'XSS\');", |
|
| 57 |
"javascript: alert(\'XSS\');", |
|
| 58 |
"javascript : alert(\'XSS\');", |
|
| 59 |
":javascript:alert(\'XSS\');", |
|
| 60 |
"javascript:", |
|
| 61 |
"javascript:", |
|
| 62 |
"javascript:", |
|
| 63 |
"javascript:", |
|
| 64 |
"java\0script:alert(\"XSS\")", |
|
| 65 |
"java\script:alert(\"XSS\")", |
|
| 66 |
" \x0e javascript:alert(\'XSS\');", |
|
| 67 |
"data:image/png;base64,foobar", |
|
| 68 |
"vbscript:foobar", |
|
| 69 |
"data:text/html;base64,foobar", |
|
| 70 |
] |
|
| 71 | ||
| 72 |
def test_uri_with_link_safe_scheme_should_recognize_unsafe_uris |
|
| 73 |
LINK_UNSAFE_URIS.each do |uri| |
|
| 74 |
assert_not uri_with_link_safe_scheme?(uri), "'#{uri}' should not be safe"
|
|
| 75 |
end |
|
| 76 |
end |
|
| 36 | 77 |
end |
| test/unit/lib/redmine/wiki_formatting/common_mark/sanitization_filter_test.rb | ||
|---|---|---|
| 71 | 71 |
assert_equal %(<code>foo</code>), filter(input) |
| 72 | 72 |
end |
| 73 | 73 | |
| 74 |
def test_should_allow_links_with_safe_url_schemes |
|
| 75 |
%w(http https ftp ssh foo).each do |scheme| |
|
| 76 |
input = %(<a href="#{scheme}://example.org/">foo</a>)
|
|
| 77 |
assert_equal input, filter(input) |
|
| 78 |
end |
|
| 79 |
end |
|
| 80 | ||
| 81 |
def test_should_allow_mailto_links |
|
| 82 |
input = %(<a href="mailto:foo@example.org">bar</a>) |
|
| 83 |
assert_equal input, filter(input) |
|
| 84 |
end |
|
| 85 | ||
| 86 |
def test_should_remove_empty_link |
|
| 87 |
input = %(<a href="">bar</a>) |
|
| 88 |
assert_equal %(<a>bar</a>), filter(input) |
|
| 89 |
input = %(<a href=" ">bar</a>) |
|
| 90 |
assert_equal %(<a>bar</a>), filter(input) |
|
| 91 |
end |
|
| 92 | ||
| 74 | 93 |
# samples taken from the Sanitize test suite |
| 75 | 94 |
# rubocop:disable Layout/LineLength |
| 76 | 95 |
STRINGS = [ |
| ... | ... | |
| 194 | 213 |
'<a href="vbscript:foobar">XSS</a>', |
| 195 | 214 |
'<a>XSS</a>' |
| 196 | 215 |
], |
| 197 | ||
| 198 |
'invalid URIs' => [ |
|
| 199 |
'<a href="foo://example.org">link</a>', |
|
| 200 |
'<a>link</a>' |
|
| 201 |
], |
|
| 202 | 216 |
} |
| 203 | 217 | |
| 204 | 218 |
PROTOCOLS.each do |name, strings| |