diff options
| -rw-r--r-- | CHANGELOG.md | 1 | ||||
| -rw-r--r-- | docs/config.md | 1 | ||||
| -rw-r--r-- | lib/pleroma/web/activity_pub/mrf/anti_link_spam_policy.ex | 48 | ||||
| -rw-r--r-- | test/web/activity_pub/mrf/anti_link_spam_policy_test.exs | 140 | 
4 files changed, 190 insertions, 0 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b7e5c9a1..0dc8b547d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -62,6 +62,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).  - MRF: Support for running subchains.  - Configuration: `skip_thread_containment` option  - Configuration: `rate_limit` option. See `Pleroma.Plugs.RateLimiter` documentation for details. +- MRF: Support for filtering out likely spam messages by rejecting posts from new users that contain links.  ### Changed  - **Breaking:** Configuration: move from Pleroma.Mailer to Pleroma.Emails.Mailer diff --git a/docs/config.md b/docs/config.md index ed8e465c6..b75193545 100644 --- a/docs/config.md +++ b/docs/config.md @@ -90,6 +90,7 @@ config :pleroma, Pleroma.Emails.Mailer,    * `Pleroma.Web.ActivityPub.MRF.SubchainPolicy`: Selectively runs other MRF policies when messages match (see ``:mrf_subchain`` section)    * `Pleroma.Web.ActivityPub.MRF.RejectNonPublic`: Drops posts with non-public visibility settings (See ``:mrf_rejectnonpublic`` section)    * `Pleroma.Web.ActivityPub.MRF.EnsureRePrepended`: Rewrites posts to ensure that replies to posts with subjects do not have an identical subject and instead begin with re:. +  * `Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicy`: Rejects posts from likely spambots by rejecting posts from new users that contain links.  * `public`: Makes the client API in authentificated mode-only except for user-profiles. Useful for disabling the Local Timeline and The Whole Known Network.  * `quarantined_instances`: List of ActivityPub instances where private(DMs, followers-only) activities will not be send.  * `managed_config`: Whenether the config for pleroma-fe is configured in this config or in ``static/config.json`` diff --git a/lib/pleroma/web/activity_pub/mrf/anti_link_spam_policy.ex b/lib/pleroma/web/activity_pub/mrf/anti_link_spam_policy.ex new file mode 100644 index 000000000..2da3eac2f --- /dev/null +++ b/lib/pleroma/web/activity_pub/mrf/anti_link_spam_policy.ex @@ -0,0 +1,48 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicy do +  alias Pleroma.User + +  require Logger + +  # has the user successfully posted before? +  defp old_user?(%User{} = u) do +    u.info.note_count > 0 || u.info.follower_count > 0 +  end + +  # does the post contain links? +  defp contains_links?(%{"content" => content} = _object) do +    content +    |> Floki.filter_out("a.mention,a.hashtag,a[rel~=\"tag\"],a.zrl") +    |> Floki.attribute("a", "href") +    |> length() > 0 +  end + +  defp contains_links?(_), do: false + +  def filter(%{"type" => "Create", "actor" => actor, "object" => object} = message) do +    with {:ok, %User{} = u} <- User.get_or_fetch_by_ap_id(actor), +         {:contains_links, true} <- {:contains_links, contains_links?(object)}, +         {:old_user, true} <- {:old_user, old_user?(u)} do +      {:ok, message} +    else +      {:contains_links, false} -> +        {:ok, message} + +      {:old_user, false} -> +        {:reject, nil} + +      {:error, _} -> +        {:reject, nil} + +      e -> +        Logger.warn("[MRF anti-link-spam] WTF: unhandled error #{inspect(e)}") +        {:reject, nil} +    end +  end + +  # in all other cases, pass through +  def filter(message), do: {:ok, message} +end diff --git a/test/web/activity_pub/mrf/anti_link_spam_policy_test.exs b/test/web/activity_pub/mrf/anti_link_spam_policy_test.exs new file mode 100644 index 000000000..284c13336 --- /dev/null +++ b/test/web/activity_pub/mrf/anti_link_spam_policy_test.exs @@ -0,0 +1,140 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicyTest do +  use Pleroma.DataCase +  import Pleroma.Factory + +  alias Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicy + +  @linkless_message %{ +    "type" => "Create", +    "object" => %{ +      "content" => "hi world!" +    } +  } + +  @linkful_message %{ +    "type" => "Create", +    "object" => %{ +      "content" => "<a href='https://example.com'>hi world!</a>" +    } +  } + +  @response_message %{ +    "type" => "Create", +    "object" => %{ +      "name" => "yes", +      "type" => "Answer" +    } +  } + +  describe "with new user" do +    test "it allows posts without links" do +      user = insert(:user) + +      assert user.info.note_count == 0 + +      message = +        @linkless_message +        |> Map.put("actor", user.ap_id) + +      {:ok, _message} = AntiLinkSpamPolicy.filter(message) +    end + +    test "it disallows posts with links" do +      user = insert(:user) + +      assert user.info.note_count == 0 + +      message = +        @linkful_message +        |> Map.put("actor", user.ap_id) + +      {:reject, _} = AntiLinkSpamPolicy.filter(message) +    end +  end + +  describe "with old user" do +    test "it allows posts without links" do +      user = insert(:user, info: %{note_count: 1}) + +      assert user.info.note_count == 1 + +      message = +        @linkless_message +        |> Map.put("actor", user.ap_id) + +      {:ok, _message} = AntiLinkSpamPolicy.filter(message) +    end + +    test "it allows posts with links" do +      user = insert(:user, info: %{note_count: 1}) + +      assert user.info.note_count == 1 + +      message = +        @linkful_message +        |> Map.put("actor", user.ap_id) + +      {:ok, _message} = AntiLinkSpamPolicy.filter(message) +    end +  end + +  describe "with followed new user" do +    test "it allows posts without links" do +      user = insert(:user, info: %{follower_count: 1}) + +      assert user.info.follower_count == 1 + +      message = +        @linkless_message +        |> Map.put("actor", user.ap_id) + +      {:ok, _message} = AntiLinkSpamPolicy.filter(message) +    end + +    test "it allows posts with links" do +      user = insert(:user, info: %{follower_count: 1}) + +      assert user.info.follower_count == 1 + +      message = +        @linkful_message +        |> Map.put("actor", user.ap_id) + +      {:ok, _message} = AntiLinkSpamPolicy.filter(message) +    end +  end + +  describe "with unknown actors" do +    test "it rejects posts without links" do +      message = +        @linkless_message +        |> Map.put("actor", "http://invalid.actor") + +      {:reject, _} = AntiLinkSpamPolicy.filter(message) +    end + +    test "it rejects posts with links" do +      message = +        @linkful_message +        |> Map.put("actor", "http://invalid.actor") + +      {:reject, _} = AntiLinkSpamPolicy.filter(message) +    end +  end + +  describe "with contentless-objects" do +    test "it does not reject them or error out" do +      user = insert(:user, info: %{note_count: 1}) + +      message = +        @response_message +        |> Map.put("actor", user.ap_id) + +      {:ok, _message} = AntiLinkSpamPolicy.filter(message) +    end +  end +end  | 
