Skip to content

Commit cbe8b85

Browse files
committed
Add honeypot field to newsletter form to prevent spam submissions
Fixes #16231 - Added `office_fax` as a honeypot field in the form to trap bots. - Implemented validation to check for any input in the honeypot field, logging a warning if filled. - Updated the newsletter subscription view to handle errors related to the honeypot field. - Added corresponding tests to ensure the honeypot functionality works as intended. - Styled the honeypot field to be hidden from users while remaining accessible to bots.
1 parent 874df8c commit cbe8b85

File tree

5 files changed

+79
-1
lines changed

5 files changed

+79
-1
lines changed

bedrock/newsletter/forms.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
from product_details import product_details
1212

13-
from bedrock.mozorg.forms import EmailInput, PrivacyWidget, strip_parenthetical
13+
from bedrock.mozorg.forms import EmailInput, PrivacyWidget, HoneyPotWidget, strip_parenthetical
1414
from bedrock.newsletter import utils
1515
from lib.l10n_utils.fluent import ftl, ftl_lazy
1616

@@ -186,6 +186,8 @@ class NewsletterFooterForm(forms.Form):
186186
privacy = forms.BooleanField(widget=PrivacyWidget(attrs={"data-testid": "newsletter-privacy-checkbox"}))
187187
source_url = forms.CharField(required=False)
188188
newsletters = forms.MultipleChoiceField(widget=forms.CheckboxSelectMultiple())
189+
# office_fax is a honeypot field
190+
office_fax = forms.CharField(widget=HoneyPotWidget(), required=False, empty_value='')
189191

190192
# has to take a newsletters argument so it can figure
191193
# out which languages to list in the form.
@@ -251,6 +253,16 @@ def clean_source_url(self):
251253

252254
return su
253255

256+
def clean_office_fax(self):
257+
# Check raw data to catch any value, including whitespace-only
258+
office_fax = self.data.get('office_fax', '')
259+
if office_fax:
260+
import logging
261+
logger = logging.getLogger('b.newsletter')
262+
logger.warning(f'Honeypot field filled with value: "{office_fax}"')
263+
raise forms.ValidationError('Invalid submission')
264+
return ''
265+
254266

255267
class EmailForm(forms.Form):
256268
"""

bedrock/newsletter/templates/newsletter/includes/form.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
{% endif %}
1616
<input type="hidden" name="source_url" value="{{ request.build_absolute_uri() }}">
1717

18+
{# Honeypot field #}
19+
{{ form.office_fax|safe }}
20+
1821
{% if include_title and is_multi_newsletter_form %}
1922
<header class="mzp-c-newsletter-header">
2023
<h3 class="mzp-c-newsletter-title">{{ title|d(ftl('multi-newsletter-form-title'), true) }}</h3>

bedrock/newsletter/tests/test_forms.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,3 +233,41 @@ def test_multiple_newsletters(self):
233233
form = NewsletterFooterForm(spacey_newsletters, "en-US", data=data.copy())
234234
self.assertTrue(form.is_valid())
235235
self.assertEqual(form.cleaned_data["newsletters"], newsletters)
236+
237+
def test_honeypot_empty_valid(self):
238+
"""Honeypot field should be valid when empty"""
239+
data = {
240+
"email": "[email protected]",
241+
"lang": "fr",
242+
"privacy": True,
243+
"newsletters": [self.newsletter_name],
244+
"office_fax": "", # honeypot field
245+
}
246+
form = NewsletterFooterForm(self.newsletter_name, locale="en-US", data=data.copy())
247+
self.assertTrue(form.is_valid(), form.errors)
248+
249+
def test_honeypot_filled_invalid(self):
250+
"""Honeypot field should be invalid when filled"""
251+
data = {
252+
"email": "[email protected]",
253+
"lang": "fr",
254+
"privacy": True,
255+
"newsletters": [self.newsletter_name],
256+
"office_fax": "some value", # honeypot field
257+
}
258+
form = NewsletterFooterForm(self.newsletter_name, locale="en-US", data=data.copy())
259+
self.assertFalse(form.is_valid())
260+
self.assertIn("office_fax", form.errors)
261+
262+
def test_honeypot_whitespace_invalid(self):
263+
"""Honeypot field should be invalid when filled with whitespace only"""
264+
data = {
265+
"email": "[email protected]",
266+
"lang": "fr",
267+
"privacy": True,
268+
"newsletters": [self.newsletter_name],
269+
"office_fax": " ", # honeypot field
270+
}
271+
form = NewsletterFooterForm(self.newsletter_name, locale="en-US", data=data.copy())
272+
self.assertFalse(form.is_valid())
273+
self.assertIn("office_fax", form.errors)

bedrock/newsletter/views.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,9 @@ def newsletter_subscribe(request):
262262
errors.append(ftl("newsletter-form-please-enter-a-valid"))
263263
if "privacy" in form.errors:
264264
errors.append(ftl("newsletter-form-you-must-agree-to"))
265+
if "office_fax" in form.errors:
266+
# Honeypot field was filled
267+
errors.append(ftl("newsletter-form-we-are-sorry-but-there"))
265268
for fieldname in ("newsletters", "lang", "country"):
266269
if fieldname in form.errors:
267270
errors.extend(form.errors[fieldname])

media/css/protocol/components/newsletter-form.scss

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,25 @@ $image-path: '/media/protocol/img';
4343
}
4444

4545
/* stylelint-enable declaration-no-important */
46+
47+
// Honeypot field styling - hide from users but keep accessible to bots
48+
.super-priority-field {
49+
position: absolute !important;
50+
left: -9999px !important;
51+
top: -9999px !important;
52+
width: 1px !important;
53+
height: 1px !important;
54+
overflow: hidden !important;
55+
opacity: 0 !important;
56+
pointer-events: none !important;
57+
58+
// Ensure it's not in tab order
59+
input {
60+
tab-index: -1 !important;
61+
}
62+
63+
// Hide from screen readers
64+
label {
65+
display: none !important;
66+
}
67+
}

0 commit comments

Comments
 (0)