Repository: freedomofpress/securedrop

Issue:
Add honeypot field to SI message form to block automated submissions
## Description

The Source Interface has limited protections in place to block bots. One simple bot-blocking measure would be to add a hidden form field on the message submission form, and reject submissions that include a value for the field.

## User Research Evidence

Spam reports from instance maintainers.

## User Stories

As a journalist, I want to save time by not having to review spam messages.



Discussion:
For reference, MediaWiki does this with https://gerrit.wikimedia.org/g/mediawiki/core/+/4afd0f7c3e017b84adef7938a31bc40a69304e62/includes/EditPage.php#2981

The intention is to block generic form-filling spambots. Anything designed to spam SecureDrops will eventually adapt.
>The Source Interface has limited protections in place to block bots. One simple bot-blocking measure would be to add a hidden form field on the message submission form, and reject submissions that include a value for the field.

How much effective will this be? I am wondering if someone is targeting SecureDrop instances, that is much targeted than random spambots. 
@kushaldas I don't think we'll know until we try mitigations like this, 1:1. That said—I do think this one back(ish)-end mitigation, should go in before any others—so that its effectiveness on its own, can be measured against other bot mitigations.

While I'm not a fan of the "bounce" remediation prior to other UX measures going in before it, entirely separate from that opinion I would like to see _different_  spam mitigations go out in different releases... as the source of the spam is not known at this time.  We'll never know, unless 1:1 they're deployed with methodical research endeavored following each release, to learn more. 
@kushaldas I don't expect that it will help with much tbh, but dealing with low-hanging fruit first (see the rest of https://github.com/freedomofpress/securedrop/milestone/77). Targeted spam is gonna require more work, probably the best mitigations there would require the use of SecureDrop Workstation.
@legoktm in https://github.com/freedomofpress/securedrop/issues/6295#issuecomment-1045303533:

> For reference, MediaWiki does this with https://gerrit.wikimedia.org/g/mediawiki/core/+/4afd0f7c3e017b84adef7938a31bc40a69304e62/includes/EditPage.php#2981
> 
> The intention is to block generic form-filling spambots. Anything designed to spam SecureDrops will eventually adapt.

Seconding this example's use of `display: none`, which will [correctly hide the honeypot field both visually and in the accessibility tree (but of course leave it reachable in the DOM)](https://www.scottohara.me/blog/2017/04/14/inclusively-hidden.html).

Resolving patch:
diff --git a/securedrop/source_app/forms.py b/securedrop/source_app/forms.py
--- a/securedrop/source_app/forms.py
+++ b/securedrop/source_app/forms.py
@@ -1,7 +1,8 @@
 import wtforms
+from flask import abort
 from flask_babel import lazy_gettext as gettext
 from flask_wtf import FlaskForm
-from wtforms import FileField, PasswordField, TextAreaField
+from wtforms import FileField, PasswordField, StringField, TextAreaField
 from wtforms.validators import InputRequired, Regexp, Length, ValidationError
 
 from models import Submission, InstanceConfig
@@ -26,6 +27,7 @@ class SubmissionForm(FlaskForm):
     msg = TextAreaField("msg", render_kw={"placeholder": gettext("Write a message."),
                                           "aria-label": gettext("Write a message.")})
     fh = FileField("fh", render_kw={"aria-label": gettext("Select a file to upload.")})
+    antispam = StringField(id="text", name="text")
 
     def validate_msg(self, field: wtforms.Field) -> None:
         if len(field.data) > Submission.MAX_MESSAGE_LEN:
@@ -38,3 +40,8 @@ def validate_msg(self, field: wtforms.Field) -> None:
                     )
                 )
             raise ValidationError(message)
+
+    def validate_antispam(self, field: wtforms.Field) -> None:
+        """If the antispam field has any contents, abort with a 403"""
+        if field.data:
+            abort(403)


Test patch:
diff --git a/securedrop/tests/test_source.py b/securedrop/tests/test_source.py
--- a/securedrop/tests/test_source.py
+++ b/securedrop/tests/test_source.py
@@ -445,6 +445,20 @@ def test_submit_both(source_app):
         assert "Thanks! We received your message and document" in text
 
 
+def test_submit_antispam(source_app):
+    """
+    Test the antispam check.
+    """
+    with source_app.test_client() as app:
+        new_codename(app, session)
+        _dummy_submission(app)
+        resp = app.post(
+            url_for('main.submit'),
+            data=dict(msg="Test", fh=(StringIO(''), ''), text="blah"),
+            follow_redirects=True)
+        assert resp.status_code == 403
+
+
 def test_delete_all_successfully_deletes_replies(source_app, app_storage):
     with source_app.app_context():
         journalist, _ = utils.db_helper.init_journalist()