diff --git a/changelog.d/scrubber-hidden-schemes.security b/changelog.d/scrubber-hidden-schemes.security new file mode 100644 index 000000000..fc1807e72 --- /dev/null +++ b/changelog.d/scrubber-hidden-schemes.security @@ -0,0 +1 @@ +Scrubbers: remove links and images whose scheme is hidden with tabs, newlines or leading control characters (for example javascript:), which browsers still run as javascript:. Custom scrubbers should use Pleroma.HTML.URIAttributes.allow_tag_with_uri_attributes instead of the FastSanitize macro \ No newline at end of file diff --git a/lib/pleroma/html/uri_attributes.ex b/lib/pleroma/html/uri_attributes.ex new file mode 100644 index 000000000..491f43e3d --- /dev/null +++ b/lib/pleroma/html/uri_attributes.ex @@ -0,0 +1,40 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.HTML.URIAttributes do + @moduledoc """ + URI attribute rules for the scrubbers. + + Works like `FastSanitize.Sanitizer.Meta.allow_tag_with_uri_attributes/3`, but + checks the scheme the way a browser reads the URL. Browsers remove tabs and + newlines anywhere and control characters and spaces at the start, so + `java\\tscript:` is a `javascript:` link to them, while `URI.parse/1` finds no + scheme and FastSanitize lets it through. + """ + + defmacro allow_tag_with_uri_attributes(tag, attrs, valid_schemes) do + for attr <- attrs do + quote do + def scrub_attribute(unquote(tag), {unquote(attr), "&" <> _}), do: nil + + def scrub_attribute(unquote(tag), {unquote(attr), uri} = attribute) do + if Pleroma.HTML.URIAttributes.valid_scheme?(uri, unquote(valid_schemes)), + do: attribute + end + end + end + end + + @spec valid_scheme?(String.t(), [String.t()]) :: boolean() + def valid_scheme?(uri, valid_schemes) do + scheme = + uri + |> String.replace(~r/[\t\n\r]/, "") + |> String.replace(~r/\A[\x00-\x20]+/, "") + |> URI.parse() + |> Map.get(:scheme) + + is_nil(scheme) or scheme in valid_schemes + end +end diff --git a/priv/scrubbers/default.ex b/priv/scrubbers/default.ex index 8e2d16650..b294ce4ee 100644 --- a/priv/scrubbers/default.ex +++ b/priv/scrubbers/default.ex @@ -6,6 +6,7 @@ defmodule Pleroma.HTML.Scrubber.Default do @doc "The default HTML scrubbing policy: no " require FastSanitize.Sanitizer.Meta + require Pleroma.HTML.URIAttributes alias FastSanitize.Sanitizer.Meta # credo:disable-for-previous-line @@ -15,7 +16,11 @@ defmodule Pleroma.HTML.Scrubber.Default do Meta.strip_comments() - Meta.allow_tag_with_uri_attributes(:a, ["href", "data-user", "data-tag"], @valid_schemes) + Pleroma.HTML.URIAttributes.allow_tag_with_uri_attributes( + :a, + ["href", "data-user", "data-tag"], + @valid_schemes + ) Meta.allow_tag_with_this_attribute_values(:a, "class", [ "hashtag", @@ -145,7 +150,7 @@ defmodule Pleroma.HTML.Scrubber.Default do Meta.allow_tag_with_this_attribute_values(:img, "class", ["emoji"]) # restrict img tags to http/https only, because of MediaProxy. - Meta.allow_tag_with_uri_attributes(:img, ["src"], ["http", "https"]) + Pleroma.HTML.URIAttributes.allow_tag_with_uri_attributes(:img, ["src"], ["http", "https"]) Meta.allow_tag_with_these_attributes(:img, [ "width", diff --git a/priv/scrubbers/links_only.ex b/priv/scrubbers/links_only.ex index 7f434fb25..42050d05a 100644 --- a/priv/scrubbers/links_only.ex +++ b/priv/scrubbers/links_only.ex @@ -10,12 +10,13 @@ defmodule Pleroma.HTML.Scrubber.LinksOnly do @valid_schemes Pleroma.Config.get([:uri_schemes, :valid_schemes], []) require FastSanitize.Sanitizer.Meta + require Pleroma.HTML.URIAttributes alias FastSanitize.Sanitizer.Meta Meta.strip_comments() # links - Meta.allow_tag_with_uri_attributes(:a, ["href"], @valid_schemes) + Pleroma.HTML.URIAttributes.allow_tag_with_uri_attributes(:a, ["href"], @valid_schemes) Meta.allow_tag_with_this_attribute_values(:a, "rel", [ "tag", diff --git a/priv/scrubbers/twitter_text.ex b/priv/scrubbers/twitter_text.ex index 4df840735..266411e72 100644 --- a/priv/scrubbers/twitter_text.ex +++ b/priv/scrubbers/twitter_text.ex @@ -11,12 +11,17 @@ defmodule Pleroma.HTML.Scrubber.TwitterText do @valid_schemes Pleroma.Config.get([:uri_schemes, :valid_schemes], []) require FastSanitize.Sanitizer.Meta + require Pleroma.HTML.URIAttributes alias FastSanitize.Sanitizer.Meta Meta.strip_comments() # links - Meta.allow_tag_with_uri_attributes(:a, ["href", "data-user", "data-tag"], @valid_schemes) + Pleroma.HTML.URIAttributes.allow_tag_with_uri_attributes( + :a, + ["href", "data-user", "data-tag"], + @valid_schemes + ) Meta.allow_tag_with_this_attribute_values(:a, "class", [ "hashtag", @@ -49,7 +54,7 @@ defmodule Pleroma.HTML.Scrubber.TwitterText do Meta.allow_tag_with_this_attribute_values(:img, "class", ["emoji"]) # restrict img tags to http/https only, because of MediaProxy. - Meta.allow_tag_with_uri_attributes(:img, ["src"], ["http", "https"]) + Pleroma.HTML.URIAttributes.allow_tag_with_uri_attributes(:img, ["src"], ["http", "https"]) Meta.allow_tag_with_these_attributes(:img, [ "width", diff --git a/test/pleroma/html_test.exs b/test/pleroma/html_test.exs index d17b07540..9266f62ed 100644 --- a/test/pleroma/html_test.exs +++ b/test/pleroma/html_test.exs @@ -141,6 +141,44 @@ defmodule Pleroma.HTMLTest do end end + describe "link schemes as a browser sees them" do + # Browsers remove tabs and newlines in URLs and control characters and + # spaces at the start, so these are javascript: links to them. + test "removes javascript: links hidden with control characters" do + for href <- [ + "java\tscript:alert(1)", + "java script:alert(1)", + "java\nscript:alert(1)", + " javascript:alert(1)", + " javascript:alert(1)", + "\u0001javascript:alert(1)" + ], + scrubber <- [ + Pleroma.HTML.Scrubber.Default, + Pleroma.HTML.Scrubber.TwitterText, + Pleroma.HTML.Scrubber.LinksOnly + ] do + html = HTML.filter_tags(~s(x), scrubber) + + refute html =~ "href", "#{inspect(href)} survived #{inspect(scrubber)}: #{html}" + end + end + + test "keeps normal links" do + for href <- ["https://example.org/a?b=c", "/relative/path", "#fragment"] do + assert HTML.filter_tags(~s(x), Pleroma.HTML.Scrubber.Default) == + ~s(x) + end + end + + test "removes images with hidden schemes" do + html = + HTML.filter_tags(~s||, Pleroma.HTML.Scrubber.Default) + + refute html =~ "src" + end + end + describe "default scrubber" do test "normalizes HTML as expected" do expected = """