Ghost subscribe form hit by spam signups, hurting sender reputation

Today my blog started getting hit by a sudden burst of spam email signup attempts:

It looks like potentially someone is trying to validate a stolen or bought list, and then the owners of the email addresses that work are marking the confirmation emails (which they did not ask for) as spam. (At least, I can’t think of any other plausible way this would benefit the attacker.)

If possible, I would like to avoid hiding behind Cloudflare. I’m self-hosting on Fly.io, and using Postmark for transactional email, and Mailgun for newsletters.

I see in my logs POST //members/api/send-magic-link" 201, and my hunch is that someone is hitting the endpoint directly with a script.

I found promising solutions by @HauntedThemes:

SELECT * FROM subscribers WHERE subscribed_url != ‘’;
DELETE * FROM subscribers WHERE subscribed_url = ‘’;

And @dlford:

ALTER TABLE subscribers MODIFY subscribed_url varchar(2000) NOT NULL;

However, the Ghost DB schema has changed since then, and I’m not sure if these sorts of approaches are still recommended.

What mitigation would you recommended?

Thanks!

1 Like

For anyone else with the same problem:

I ended up capitulating and putting my blog behind Cloudflare, but even with bot fight mode activated, it didn’t help. However, a quick look in the actual logs showed that it was a very basic script kiddie attack, and even though the IPs seemed to change (mostly US and UK so far), they hadn’t bothered to spoof the user agent:

"user-agent":"Python/3.12 aiohttp/3.9.5"

So I added a Cloudflare Web Application Firewall (WAF) custom rule to block requests to /api/send-magic-link when the user agent “contains” Python and aiohttp.

I’m still looking for a better solution, that I can implement at the Ghost or Fly.io level, without relying on Cloudflare.

4 Likes

Subsequent attacks have used python-requests user agent rather than aiohttp. I now block all python requests to that endpoint.

Test with:

curl -A "python" -X POST  -H 'Content-Type: application/json' -d '{email: email@example.com, emailType: \'signin\', integrityToken: integrityToken}' "https://yourghosturl.com/members/api/send-magic-link/"
curl -A "something else" -X POST  -H 'Content-Type: application/json' -d '{email: email@example.com, emailType: \'signin\', integrityToken: integrityToken}' "https://yourghosturl.com/members/api/send-magic-link/"

I looked into several other options in addition to Cloudflare. However, I’m using the (unofficial!) Ghost docker image on Alpine Linux on Fly, and:

  • fail2ban has unmet dependencies
  • iptables (not a good solution anyway) is missing the kernel module for the string extension.
  • neither Apache nor Nginx are running in the container

This seems to leave two options:

  1. run a Nginx/Caddy/Traefik/pf reverse proxy as a separate app
  2. hack around and add user-agent blocking to that endpoint in (my fork of) Ghost core

I have already made cursory attempts to use these approaches, and it turned out to be more complicated than I expected. But when I have more time, I will give another go at running my own firewall.

The moral of the story: good managed hosting is expensive for a reason :wink:

3 Likes