mirror of
https://github.com/discourse/discourse.git
synced 2025-03-25 16:48:55 +08:00
SECURITY: Strip unrendered unicode bidirectional chars in code blocks (#15032)
When rendering the markdown code blocks we replace the offending characters in the output string with spans highlighting a textual representation of the character, along with a title attribute with information about why the character was highlighted. The list of characters stripped by this fix, which are the bidirectional characters considered relevant, are: U+202A U+202B U+202C U+202D U+202E U+2066 U+2067 U+2068 U+2069
This commit is contained in:
parent
10a57825c8
commit
fa6b87a1bf
@ -827,6 +827,14 @@ pre {
|
|||||||
background: var(--blend-primary-secondary-5);
|
background: var(--blend-primary-secondary-5);
|
||||||
max-height: 500px;
|
max-height: 500px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bidi-warning,
|
||||||
|
.bidi-warning span {
|
||||||
|
font-style: normal;
|
||||||
|
background-color: var(--highlight);
|
||||||
|
color: var(--danger);
|
||||||
|
cursor: help;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.copy-codeblocks {
|
.copy-codeblocks {
|
||||||
|
@ -717,6 +717,7 @@ en:
|
|||||||
post:
|
post:
|
||||||
image_placeholder:
|
image_placeholder:
|
||||||
broken: "This image is broken"
|
broken: "This image is broken"
|
||||||
|
hidden_bidi_character: "Bidirectional characters can change the order that text is rendered. This could be used to obscure malicious code."
|
||||||
has_likes:
|
has_likes:
|
||||||
one: "%{count} Like"
|
one: "%{count} Like"
|
||||||
other: "%{count} Likes"
|
other: "%{count} Likes"
|
||||||
|
@ -5,6 +5,19 @@ require 'nokogiri'
|
|||||||
require 'erb'
|
require 'erb'
|
||||||
|
|
||||||
module PrettyText
|
module PrettyText
|
||||||
|
DANGEROUS_BIDI_CHARACTERS = [
|
||||||
|
"\u202A",
|
||||||
|
"\u202B",
|
||||||
|
"\u202C",
|
||||||
|
"\u202D",
|
||||||
|
"\u202E",
|
||||||
|
"\u2066",
|
||||||
|
"\u2067",
|
||||||
|
"\u2068",
|
||||||
|
"\u2069",
|
||||||
|
].freeze
|
||||||
|
DANGEROUS_BIDI_REGEXP = Regexp.new(DANGEROUS_BIDI_CHARACTERS.join("|")).freeze
|
||||||
|
|
||||||
@mutex = Mutex.new
|
@mutex = Mutex.new
|
||||||
@ctx_init = Mutex.new
|
@ctx_init = Mutex.new
|
||||||
|
|
||||||
@ -278,6 +291,7 @@ module PrettyText
|
|||||||
|
|
||||||
add_nofollow = !options[:omit_nofollow] && SiteSetting.add_rel_nofollow_to_user_content
|
add_nofollow = !options[:omit_nofollow] && SiteSetting.add_rel_nofollow_to_user_content
|
||||||
add_rel_attributes_to_user_content(doc, add_nofollow)
|
add_rel_attributes_to_user_content(doc, add_nofollow)
|
||||||
|
strip_hidden_unicode_bidirectional_characters(doc)
|
||||||
|
|
||||||
if SiteSetting.enable_mentions
|
if SiteSetting.enable_mentions
|
||||||
add_mentions(doc, user_id: opts[:user_id])
|
add_mentions(doc, user_id: opts[:user_id])
|
||||||
@ -290,6 +304,24 @@ module PrettyText
|
|||||||
loofah_fragment.scrub!(scrubber).to_html
|
loofah_fragment.scrub!(scrubber).to_html
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.strip_hidden_unicode_bidirectional_characters(doc)
|
||||||
|
return if !DANGEROUS_BIDI_REGEXP.match?(doc.content)
|
||||||
|
|
||||||
|
doc.css("code,pre").each do |code_tag|
|
||||||
|
next if !DANGEROUS_BIDI_REGEXP.match?(code_tag.content)
|
||||||
|
|
||||||
|
DANGEROUS_BIDI_CHARACTERS.each do |bidi|
|
||||||
|
next if !code_tag.content.include?(bidi)
|
||||||
|
|
||||||
|
formatted = "<U+#{bidi.ord.to_s(16).upcase}>"
|
||||||
|
code_tag.inner_html = code_tag.inner_html.gsub(
|
||||||
|
bidi,
|
||||||
|
"<span class=\"bidi-warning\" title=\"#{I18n.t("post.hidden_bidi_character")}\">#{formatted}</span>"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def self.add_rel_attributes_to_user_content(doc, add_nofollow)
|
def self.add_rel_attributes_to_user_content(doc, add_nofollow)
|
||||||
allowlist = []
|
allowlist = []
|
||||||
|
|
||||||
|
@ -552,6 +552,95 @@ describe PrettyText do
|
|||||||
Discourse.redis.flushdb
|
Discourse.redis.flushdb
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "strips out unicode bidirectional (bidi) override characters and replaces with a highlighted span" do
|
||||||
|
code = <<~MD
|
||||||
|
X
|
||||||
|
```js
|
||||||
|
var isAdmin = false;
|
||||||
|
/* begin admin only */ if (isAdmin) {
|
||||||
|
console.log("You are an admin.");
|
||||||
|
/* end admins only */ }
|
||||||
|
```
|
||||||
|
MD
|
||||||
|
cooked = PrettyText.cook(code)
|
||||||
|
hidden_bidi_title = I18n.t("post.hidden_bidi_character")
|
||||||
|
|
||||||
|
html = <<~HTML
|
||||||
|
<p>X</p>
|
||||||
|
<pre><code class="lang-auto">var isAdmin = false;
|
||||||
|
/*<span class="bidi-warning" title="#{hidden_bidi_title}"><U+202E></span> begin admin only */<span class="bidi-warning" title="#{hidden_bidi_title}"><U+2066></span> if (isAdmin) <span class="bidi-warning" title="#{hidden_bidi_title}"><U+2069></span> <span class="bidi-warning" title="#{hidden_bidi_title}"><U+2066></span> {
|
||||||
|
console.log("You are an admin.");
|
||||||
|
/* end admins only <span class="bidi-warning" title="#{hidden_bidi_title}"><U+202E></span>*/<span class="bidi-warning" title="#{hidden_bidi_title}"><U+2066></span> }
|
||||||
|
</code></pre>
|
||||||
|
HTML
|
||||||
|
|
||||||
|
expect(cooked).to eq(html.strip)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "fuzzes all possible dangerous unicode bidirectional (bidi) override characters, making sure they are replaced" do
|
||||||
|
bad_bidi = [
|
||||||
|
"\u202A",
|
||||||
|
"\u202B",
|
||||||
|
"\u202C",
|
||||||
|
"\u202D",
|
||||||
|
"\u202E",
|
||||||
|
"\u2066",
|
||||||
|
"\u2067",
|
||||||
|
"\u2068",
|
||||||
|
"\u2069",
|
||||||
|
]
|
||||||
|
bad_bidi.each do |bidi|
|
||||||
|
code = <<~MD
|
||||||
|
```
|
||||||
|
#{bidi}
|
||||||
|
```
|
||||||
|
MD
|
||||||
|
cooked = PrettyText.cook(code)
|
||||||
|
formatted_bidi = format("<U+%04X>", bidi.ord)
|
||||||
|
html = <<~HTML
|
||||||
|
<pre><code class="lang-auto"><span class="bidi-warning" title="#{I18n.t("post.hidden_bidi_character")}">#{formatted_bidi}</span>
|
||||||
|
</code></pre>
|
||||||
|
HTML
|
||||||
|
expect(cooked).to eq(html.strip)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it "fuzzes all possible dangerous unicode bidirectional (bidi) override characters in solo code and pre nodes, making sure they are replaced" do
|
||||||
|
bad_bidi = [
|
||||||
|
"\u202A",
|
||||||
|
"\u202B",
|
||||||
|
"\u202C",
|
||||||
|
"\u202D",
|
||||||
|
"\u202E",
|
||||||
|
"\u2066",
|
||||||
|
"\u2067",
|
||||||
|
"\u2068",
|
||||||
|
"\u2069",
|
||||||
|
]
|
||||||
|
bad_bidi.each do |bidi|
|
||||||
|
code = <<~MD
|
||||||
|
<code>#{bidi}</code>
|
||||||
|
MD
|
||||||
|
cooked = PrettyText.cook(code)
|
||||||
|
formatted_bidi = format("<U+%04X>", bidi.ord)
|
||||||
|
html = <<~HTML
|
||||||
|
<p><code><span class="bidi-warning" title="#{I18n.t("post.hidden_bidi_character")}">#{formatted_bidi}</span></code></p>
|
||||||
|
HTML
|
||||||
|
expect(cooked).to eq(html.strip)
|
||||||
|
end
|
||||||
|
bad_bidi.each do |bidi|
|
||||||
|
code = <<~MD
|
||||||
|
<pre>#{bidi}</pre>
|
||||||
|
MD
|
||||||
|
cooked = PrettyText.cook(code)
|
||||||
|
formatted_bidi = format("<U+%04X>", bidi.ord)
|
||||||
|
html = <<~HTML
|
||||||
|
<pre><span class="bidi-warning" title="#{I18n.t("post.hidden_bidi_character")}">#{formatted_bidi}</span></pre>
|
||||||
|
HTML
|
||||||
|
expect(cooked).to eq(html.strip)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "rel attributes" do
|
describe "rel attributes" do
|
||||||
|
Loading…
x
Reference in New Issue
Block a user