Fix #5365: Don't allow whitespace-only text submission.

Fix bug where it was possible to submit blank text in various text fields.

Caused by `String#blank?` not considering certain Unicode characters as blank. `blank?` is defined
as `match?(/\A[[:space:]]*\z/)`, where `[[:space:]]` matches ASCII spaces (space, tab, newline, etc)
and Unicode characters in the Space category ([1]). However, there are other space-like characters
not in the Space category. This includes U+200B (Zero-Width Space), and many more.

It turns out the "Default ignorable code points" [2][3] are what we're after. These are the set of 400
or so formatting and control characters that are invisible when displayed.

Note that there are other control characters that aren't invisible when rendered, instead they're
shown with a placeholder glyph. These include the ASCII C0 and C1 control codes [4], certain Unicode
control characters [5], and unassigned, reserved, and private use codepoints.

There is one outlier: the Braille pattern blank (U+2800) [6]. This character is visually blank, but is
not considered to be a space or an ignorable code point.

[1]: https://codepoints.net/search?gc[]=Z
[2]: https://codepoints.net/search?DI=1
[3]: https://www.unicode.org/review/pr-5.html
[4]: https://codepoints.net/search?gc[]=Cc
[5]: https://codepoints.net/search?gc[]=Cf
[6]: https://codepoints.net/U+2800
[7]: https://en.wikipedia.org/wiki/Whitespace_character
[8]: https://character.construction/blanks
[9]: https://invisible-characters.com
This commit is contained in:
evazion
2022-12-05 01:22:20 -06:00
parent 640a20d81c
commit d9dc84325f
30 changed files with 152 additions and 23 deletions

View File

@@ -202,8 +202,11 @@ class CommentTest < ActiveSupport::TestCase
end
context "during validation" do
subject { FactoryBot.build(:comment) }
subject { build(:comment) }
should_not allow_value("").for(:body)
should_not allow_value(" ").for(:body)
should_not allow_value("\u200B").for(:body)
end
end
end

View File

@@ -149,8 +149,12 @@ class DmailTest < ActiveSupport::TestCase
context "during validation" do
subject { FactoryBot.build(:dmail) }
should_not allow_value("").for(:title)
should_not allow_value(" ").for(:title)
should_not allow_value("\u200B").for(:title)
should_not allow_value("").for(:body)
should_not allow_value(" ").for(:body)
should_not allow_value("\u200B").for(:body)
should_not allow_value(nil).for(:to)
should_not allow_value(nil).for(:from)
should_not allow_value(nil).for(:owner)

View File

@@ -68,5 +68,8 @@ class FavoriteGroupTest < ActiveSupport::TestCase
should_not allow_value("_").for(:name)
should_not allow_value("any").for(:name)
should_not allow_value("none").for(:name)
should_not allow_value("").for(:name)
should_not allow_value(" ").for(:name)
should_not allow_value("\u200B").for(:name)
end
end

View File

@@ -165,5 +165,13 @@ class ForumPostTest < ActiveSupport::TestCase
assert_equal(@second_user.id, @post.updater_id)
end
end
context "during validation" do
subject { build(:forum_post) }
should_not allow_value("").for(:body)
should_not allow_value(" ").for(:body)
should_not allow_value("\u200B").for(:body)
end
end
end

View File

@@ -82,5 +82,13 @@ class ForumTopicTest < ActiveSupport::TestCase
end
end
end
context "during validation" do
subject { build(:forum_topic) }
should_not allow_value("").for(:title)
should_not allow_value(" ").for(:title)
should_not allow_value("\u200B").for(:title)
end
end
end

View File

@@ -145,5 +145,11 @@ class NoteTest < ActiveSupport::TestCase
end
end
end
context "when validating notes" do
should_not allow_value("").for(:body)
should_not allow_value(" ").for(:body)
should_not allow_value("\u200B").for(:body)
end
end
end

View File

@@ -244,9 +244,17 @@ class PoolTest < ActiveSupport::TestCase
end
context "when validating names" do
["foo,bar", "foo*bar", "123", "___", " ", "any", "none", "series", "collection"].each do |bad_name|
should_not allow_value(bad_name).for(:name)
end
should_not allow_value("foo,bar").for(:name)
should_not allow_value("foo*bar").for(:name)
should_not allow_value("123").for(:name)
should_not allow_value("any").for(:name)
should_not allow_value("none").for(:name)
should_not allow_value("series").for(:name)
should_not allow_value("collection").for(:name)
should_not allow_value("___").for(:name)
should_not allow_value(" ").for(:name)
should_not allow_value("\u200B").for(:name)
should_not allow_value("").for(:name)
end
end

View File

@@ -52,6 +52,15 @@ class PostAppealTest < ActiveSupport::TestCase
end
end
end
context "validation" do
subject { build(:post_appeal) }
should allow_value("").for(:reason)
should_not allow_value(" ").for(:reason)
should_not allow_value("\u200B").for(:reason)
end
end
end
end

View File

@@ -114,5 +114,13 @@ class PostFlagTest < ActiveSupport::TestCase
end
end
end
context "during validation" do
subject { build(:post_flag) }
should_not allow_value("").for(:reason)
should_not allow_value(" ").for(:reason)
should_not allow_value("\u200B").for(:reason)
end
end
end

View File

@@ -41,4 +41,26 @@ class StringTest < ActiveSupport::TestCase
assert_equal("foo\r\nbar", "foo\u2029bar".normalize_whitespace)
end
end
context "String#invisible?" do
should "work" do
assert_equal(true, "".invisible?)
assert_equal(true, " ".invisible?)
assert_equal(true, "\v\t\f\r\n".invisible?)
assert_equal(true, "\u00A0\u00AD\u034F\u061C\u115F\u1160\u17B4\u17B5\u180B\u180C\u180D\u180E".invisible?)
assert_equal(true, "\u200B\u200C\u200D\u200E\u200F\u2028\u2029\u2060\u206F\u2800\u3000\u3164".invisible?)
assert_equal(true, "\uFE00\uFE0F\uFEFF\uFFA0".invisible?)
assert_equal(true, "\u{E0001}\u{E007F}".invisible?)
assert_equal(false, "foo".invisible?)
assert_equal(false, "\u0000".invisible?) # https://codepoints.net/U+0000 (NULL)
assert_equal(false, "\u0001".invisible?) # https://codepoints.net/U+0001 (START OF HEADING)
assert_equal(false, "\uE000".invisible?) # https://codepoints.net/U+E000 (PRIVATE USE CHARACTER)
assert_equal(false, "\uFDD0".invisible?) # https://codepoints.net/U+FDD0 (NONCHARACTER)
assert_equal(false, "\uFFF9".invisible?) # https://codepoints.net/U+FFF9 (INTERLINEAR ANNOTATION ANCHOR)
assert_equal(false, "\uFFFF".invisible?) # https://codepoints.net/U+FFFF (NONCHARACTER)
assert_equal(false, "\u{1D159}".invisible?) # https://codepoints.net/U+1D159 (MUSICAL SYMBOL NULL NOTEHEAD)
assert_equal(false, "\u{1D455}".invisible?) # https://codepoints.net/U+1D455 (<reserved>)
end
end
end

View File

@@ -17,5 +17,11 @@ class UserFeedbackTest < ActiveSupport::TestCase
assert_equal(dmail, user.dmails.last.body)
end
end
context "on validation" do
should_not allow_value("").for(:body)
should_not allow_value(" ").for(:body)
should_not allow_value("\u200B").for(:body)
end
end
end

View File

@@ -114,6 +114,8 @@ class WikiPageTest < ActiveSupport::TestCase
should normalize_attribute(:title).from(" Foo___ Bar ").to("foo_bar")
should_not allow_value("").for(:title).on(:create)
should_not allow_value(" ").for(:title).on(:create)
should_not allow_value("\u200B").for(:title).on(:create)
should_not allow_value("___").for(:title).on(:create)
should_not allow_value("-foo").for(:title).on(:create)
should_not allow_value("/foo").for(:title).on(:create)
@@ -126,6 +128,12 @@ class WikiPageTest < ActiveSupport::TestCase
should_not allow_value("X"*171).for(:title).on(:create)
end
context "during body validation" do
should_not allow_value("").for(:body)
should_not allow_value(" ").for(:body)
should_not allow_value("\u200B").for(:body)
end
context "with other names" do
should "not allow artist wikis to have other names" do
tag = create(:artist_tag)