Contact form antispam

pull/12/head
Noah 2022-09-26 19:12:24 -07:00
parent 8085e092bc
commit c97cc28b13
7 changed files with 84 additions and 7 deletions

View File

@ -47,13 +47,21 @@ const (
ChangeEmailRedisKey = "change-email/%s"
SignupTokenExpires = 24 * time.Hour // used for all tokens so far
// Rate limit
// Rate limits
RateLimitRedisKey = "rate-limit/%s/%s" // namespace, id
LoginRateLimitWindow = 1 * time.Hour
LoginRateLimit = 10 // 10 failed login attempts = locked for full hour
LoginRateLimitCooldownAt = 3 // 3 failed attempts = start throttling
LoginRateLimitCooldown = 30 * time.Second
// Contact form rate limits for logged-out users to curb spam robots:
// - One message can be submitted every 2 minutes
// - If they post 10 minutes in an hour they are paused for one hour.
ContactRateLimitWindow = 1 * time.Hour
ContactRateLimit = 10
ContactRateLimitCooldownAt = 1
ContactRateLimitCooldown = 2 * time.Minute
// How frequently to refresh LastLoginAt since sessions are long-lived.
LastLoginAtCooldown = 8 * time.Hour
)

View File

@ -15,11 +15,12 @@ var Current = DefaultVariable()
// Variable configuration attributes (loaded from settings.json).
type Variable struct {
BaseURL string
AdminEmail string
Mail Mail
Redis Redis
Database Database
BaseURL string
AdminEmail string
Mail Mail
Redis Redis
Database Database
UseXForwardedFor bool
}
// DefaultVariable returns the default settings.json data.

View File

@ -11,6 +11,7 @@ import (
"code.nonshy.com/nonshy/website/pkg/mail"
"code.nonshy.com/nonshy/website/pkg/markdown"
"code.nonshy.com/nonshy/website/pkg/models"
"code.nonshy.com/nonshy/website/pkg/ratelimit"
"code.nonshy.com/nonshy/website/pkg/session"
"code.nonshy.com/nonshy/website/pkg/templates"
)
@ -26,6 +27,8 @@ func Contact() http.HandlerFunc {
title = "Contact Us"
message = r.FormValue("message")
replyTo = r.FormValue("email")
trap1 = r.FormValue("url") != "https://"
trap2 = r.FormValue("comment") != ""
tableID int
tableName string
tableLabel string // front-end user feedback about selected report item
@ -85,6 +88,32 @@ func Contact() http.HandlerFunc {
replyTo = currentUser.Email
}
// Rate limit submissions, especially for logged-out users.
if currentUser == nil {
limiter := &ratelimit.Limiter{
Namespace: "contact",
ID: session.RemoteAddr(r),
Limit: config.ContactRateLimit,
Window: config.ContactRateLimitWindow,
CooldownAt: config.ContactRateLimitCooldownAt,
Cooldown: config.ContactRateLimitCooldown,
}
if err := limiter.Ping(); err != nil {
session.FlashError(w, r, err.Error())
templates.Redirect(w, r.URL.Path)
return
}
}
// If they have tripped the spam bot trap fields, don't save their message.
if trap1 || trap2 {
log.Error("Contact form: bot has tripped the trap fields, do not save message")
session.Flash(w, r, success)
templates.Redirect(w, r.URL.Path)
return
}
// Store feedback in the database.
fb := &models.Feedback{
Intent: intent,

View File

@ -56,7 +56,7 @@ func (l *Limiter) Ping() error {
}
// Are we throttled?
if data.Pings >= l.CooldownAt {
if l.CooldownAt > 0 && data.Pings > l.CooldownAt {
data.NotBefore = now.Add(l.Cooldown)
if err := redis.Set(key, data, l.Window); err != nil {
return fmt.Errorf("Couldn't set Redis key for rate limiter: %s", err)

View File

@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"net/http"
"strings"
"time"
"code.nonshy.com/nonshy/website/pkg/config"
@ -112,6 +113,19 @@ func Get(r *http.Request) *Session {
return nil
}
// RemoteAddr returns the user's remote IP address. If UseXForwardedFor is enabled in settings.json,
// the HTTP header X-Forwarded-For may be returned here or otherwise the request RemoteAddr is returned.
func RemoteAddr(r *http.Request) string {
if config.Current.UseXForwardedFor {
xff := r.Header.Get("X-Forwarded-For")
if len(xff) > 0 {
return strings.SplitN(xff, ",", 1)[0]
}
}
return strings.SplitN(r.RemoteAddr, ":", 1)[0]
}
// ReadFlashes returns and clears the Flashes and Errors for this session.
func (s *Session) ReadFlashes(w http.ResponseWriter) (flashes, errors []string) {
flashes = s.Flashes

View File

@ -67,4 +67,9 @@
/* Collapsible cards for mobile (e.g. filter cards) */
.card.nonshy-collapsible-mobile {
cursor: pointer;
}
/* Hide an element */
.nonshy-hidden {
display: none;
}

View File

@ -89,6 +89,26 @@
</div>
{{end}}
<!-- "Trap" fields for dumb automated spammer bots -->
<div class="field block nonshy-hidden">
<label class="label" for="url">Website URL</label>
<input type="text" class="input"
name="url" id="url"
value="https://">
<p class="help">
Do not touch this field.
</p>
</div>
<div class="field block nonshy-hidden">
<label class="label" for="url">Comment</label>
<input type="text" class="input"
name="comment" id="comment"
value="">
<p class="help">
Do not touch this field.
</p>
</div>
<div class="field has-text-centered">
<button type="submit" class="button is-success">
Send Message