Observations about spam signups

I’m observing spam signup behaviors on Synaps Media since a while, and noticed some patterns that I wanted to share.

Spam signup requests (to /members/api/send-magic-link/ endpoint) comes from a wide range of IPs. I checked some of the popular IPs on our platform and noticed that they are mostly coming from some servers or Tor Network. Because Tor Network IPs are mostly the IP addresses of individual people and Tor Network is also used for privacy considerations in some countries, I’m not confident to block whole networks. Some server-side traffic is also can be used by VPNs. So blocking them can also affect legitimate visitors.

E-mail addresses coming with requests seem valid. They don’t bounce. I get some complaints but not that much. Even though most of them don’t convert to a member since they don’t click the link to approve confirmation link, some of them converts. I think it’s because just the fact that some people just click the links in emails without actually understanding what is it about. So members from some countries that you would not expect, can be related with those spam signup requests.

Motivation

There is a pattern that, same IPs make login requests with an email, get “No member exists with this e-mail address” response, then make a signup request for same email. This gives me the idea about the motivation behind these requests: They are using Ghost sites to a way of checking if an email address is actively used. If the owner of the email converts to a member, then login requests sends magic link to login, instead of error message. So the attacker can confirm that that email address owner is actively using that address, -and even more- is eager to click links that they should not. (So great targets for phishing attacks)

Actions

These signup confirmation emails increases the risk to be flagged as spammer. It’s a risk for our domains and mailing services.

If the motivation behind this attack is collecting a list of validated emails for phishing, then I think there is a simple way of preventing this: Using “One time codes” over “Magic Link”. One time codes already replaced magic links for member login flow. I think it’s also needed (maybe even more) for signup flow. Using one time code, will prevent having those fake/accidental members completely, since visitor should complete the flow in the same window they started.

Edit: I just remembered that one-time-code emails still includes magic links and a big “Signup now” button that just calls the magic link. I think still it’s more “discouraging” to click the button than the current version, but even better would be to drop magic link in signup flow completely.

What do you think?

Do you also have observations and ideas about this issue? If you get fake-looking members, can you please share, with the information about your hosting provider or self-hosting?

8 Likes

I can confirm that I am seeing the exact same pattern on Magic Pages. Overall, it does remind me of the SMS gateway spam that happened last year – but this time it seems a lot more sophisticated, in the sense that whoever’s doing this knows a lot more about how Ghost works.

One idea I had was blocking known Tor exit nodes on the /members/api/send-magic-link/ endpoint. However, quite frankly…I am missing the tooling for that to do this in an efficient way that doesn’t impact real people. I might have some more ideas, but want to try them first before putting it out in public.

Another way to mitigate it is @curiositry’s way:

However, if Ghost implemented that upstream, it would just be a matter of time before they hit the new endpoint.

2 Likes

I’m not sure you linked the right post, Jannis? But you mean the one where he actually relocated the endpoint, right?

It’d be interesting if each Ghost site got its own endpoint location (or it were user-configurable). But Portal is still going to need to know where the endpoint is, and so then we’re back to ‘bots can figure it out’ pretty quickly…

3 Likes

Haa! Copied the wrong one. Fixed :smiley:

Yeah, obscurity won’t work much given that the endpoint NEEDS to be known for the frontend.

I like @muratcorlu’s one-time code suggestion.

Thinking about it again…isn’t this a classic captcha problem? Yes, captchas are solvable for bots. But their job is to just make things so expensive for them that it doesn’t pay off anymore.

Just finding the magic link endpoint and filling in the right forms…easy. But having to do proof of work every time? Well…might just not pay off anymore.

Edit: I am stupid. That wouldn’t work on the API route anyway :joy:

1 Like

One-time-code, would fix the “accidental” signups only. If the motivation of this attack is to determine if an email will convert to a member by just sending a signup confirmation email, then with one-time-code, none of them will convert to a member.

Because I see that the same IPs are also trying login flow as well, I think that’s the biggest motivation behind it. And I think using one-time-code instead of magic link, is a way better solution than putting a captcha. (Yes, I hate captchas :blush:)

1 Like

Great to see this thread, with everyone’s experience and ideas collated.

I collected the patterns I’ve seen, possible motivations, and mitigations I’ve used into a post on my blog, What’s the end game for Ghost newsletter sign-up spam?, and I agree with @muratcorlu’s observation that it could be preparation-for-phishing (I hadn’t noticed separate login attempts that precede the signups, but that makes sense).

A few thoughts:

  • My opinion of captchas: :face_vomiting:
  • Re: blocking server traffic: services like these attract the worst and the best people, and as a tech blogger, I think probably some of my smartest readers would be bycatch. It’s like Facebook deciding that Linux was “malware”. Yes, probably most malware is created using Linux. But also, probably most of Facebook is created using Linux?
  • One time codes seem like it would make Ghost less attractive of a target, but I think that it might reduce completions of the signup flow, compared to a button you can click
  • If it’s phishing preparation, witholding the information that no user with that email exists might help, but this would also impact real users who typed their email wrong, so it probably isn’t a good idea
  • Changing the endpoint has worked, but also been quite a pain. Most of the hassle has been specific to being the only person running forked code, and wouldn’t necessarily apply if endpoint-changing was built into Ghost.
    • Any firewall rules around that endpoint must be updated.
    • It will tend to quietly break integrations that hit it directly
    • If the portal and embedded signup code are changed, they won’t be delivered by a CDN, and they won’t be cached from the last time someone read a Ghost blog, and the portal.min.js is like 2MB, so this is not a trivial waste

In the short term, I think my top ideas are:

  • adding magic link endpoint customization/randomization (it might not last forever but I think it will help)
  • add an option for manually approving signups
  • or putting an email validation API step before the first email gets sent
  • integrating something like Anubis/BotStopper into Ghost, so the whole site is behind a proof-of-work captcha
5 Likes

At this point, it really feels like something Ghost needs to address at a platform level, because this problem is not sustainable for site owners to fight individually.

Whether this activity is coming from Ghost competitors, third parties abusing Ghost endpoints, or targeted attacks against specific sites is still unclear—but what is clear is that the effort required to track and mitigate it is extremely time-consuming. We’ve traced and reported multiple server clusters, had some providers confirm shutdowns, only to see the same behavior resume the same day from entirely different regions (e.g., Germany → dozens of US servers → China).

Using fingerprinting and session analysis, we can see this is coordinated automation operating at scale—rotating infrastructure, macOS-based servers, VPNs, and Tor exit nodes. Blocking individual ASNs or regions simply causes the traffic to reappear elsewhere almost immediately. Even when mitigations work temporarily, the attackers adapt faster than site owners reasonably can.

The larger issue is that Ghost’s public membership endpoints make this kind of abuse disproportionately easy, while protections (CAPTCHA, Turnstile, rate gating, verification controls) are left to custom, external, or brittle workarounds. That puts the burden entirely on individual users, while sender reputation, CRM data, and domain trust are at risk.

Regardless of who is behind this, the reality is that it affects all Ghost users, and it’s probably time for Ghost to treat automated signup abuse as a core platform concern, not an edge case.

We’ve managed to track and contain parts of it, but doing this manually is not scalable—and most site owners won’t even realize what’s happening until their sender reputation is already damaged.

Our complaint is documented in a separate thread, but it’s the same underlying issue. Addressing this has been time-consuming and repetitive, and we’ve had to switch to invite-only multiple times as a result.
https://forum.ghost.org/t/email-form-abuse-on-ghost-no-captcha/61687

5 Likes

There is an upcoming improvement on Ghost that will -probably- release this week with v6.17: https://github.com/TryGhost/Ghost/pull/26015

As far as I understand, Ghost team also thinks that this attack is used for collecting/validating an email list for a phishing attack. With this improvement, now login flow will not give any sign about if that email address is already a member or not.

I think, this will not stop the spam immediately, but if our assumption is correct, attacker will eventually leave Ghost sites alone.

About Captcha, I have some concerns:

  • Captcha implementation can break -almost- all custom signup implementations. To not release a very annoying breaking change, a very careful implementation is needed.
  • Captchas reduce the UX, adds a privacy concern (3rd party captchas are mostly well known trackers). Maybe Turnstile in “hidden mode” can be a good option. It seems, it doesn’t track users.

Hopefully, new implementation will mitigate the issue without needing the captcha at all.

4 Likes

No one talks about this, but how do spammers detect blog addresses? I have the impression that it started when Ghost Explore appeared. In any case, for me, it coincided with the moment I started receiving spam.

I’m not saying this to criticize Ghost Explore, but isn’t there a way to protect this directory a little better?

2 Likes

I have a reply on this topic on another thread:

2 Likes

(Thanks to mutacorla for pointing my post to this). I read the thread, and I was hopeful that the mention of the 6.17 might have fixed it, but I am up to 6.19.1, and I am getting an accelerating number of these fake signups.

Dropping this here to keep track of any progress.

edited to add: Bloody hell, I just checked my mailgun logs, and it is getting slayed with signup requests that aren’t being converted, clearly this is a new attack vector. This is a csv dump of the failed email log from just yesterday (somehow, I am not surprised by all the attempts from the yahoo account)

This site (sweatyspice.com) has about 2K legitimate subscribers, but this is making me worried that my domain(s) might be flagged for spam.

2 Likes

Agreed. This really needs to be addressed at the platform level.

I’ve been tackling it using my reverse proxy (caddy) to limit traffic based on request byte length, user-agent mismatch, and a few other tricks. I’ve also setup fail2ban to tail the logs from caddy to ban repeat offenders that hit the link too hard.

And while it’s been largely successful, it’s still occurring.
It’s like trying to catch water with a sieve.

If the ghost devs are reading this, here’s an easy fix you could implement in a future build (hopefully the next one!)

Randomised Magic Link Endpoint

On boot it would be fairly trivial for the Ghost startup routine to generate a 10-character random string that gets stored in the DB and bind it to the magic link function.

This way when the site and admin interface stands up it can grab the value directly from the DB to assign it to the subscribe and sign in buttons to fire the request, and to the admin backplane so the request can be passed to the mailgun integration without having an exposed guessable link.

Something along the lines of:

/members/api/send-magic-link/cZFHs0rDEv

As a fallback there should be an environment variable that can be set in config.production.json (or in .env / docker if you go that route) that can override the randomised endpoint function if the user requires other api level integrations which need to be static.

set__magic-link__URL: <user configured endpoint>

To further this, it would be even better if the old link (/members/api/send-magic-link/) would send a rarely used 4xx response like 418 ‘I’m a teapot’ so we could easily catch the attempts in logs to outright block or ban these bots.

While we’re at it, adding link rotation with an environment user set duration would be also ideal for good measure.

Happy to help discuss, build, test, this solution!

1 Like

Current attackers already know how Ghost works. send-magic-link endpoint requires an integration token that UI fetch from another endpoint. This is also done by attacker. So, even if you change the URL, portal.js need to know this URL as well to be able to make request for legit users. Then, attacker will just update their code to first check that URL, then make the request to it. It’ll add a little more effort to attacker, but will add to many complexity to the Ghost codebase.

I think the right solution should be a method that will prevent these attacks from achieving their objectives (collecting potential victim email addresses for phishing attacks). The Ghost team has already done some work on this, but unfortunately the current solution is still not sufficient.

2 Likes

Sure, that’s how it works right now, and if they are grabbing the integration token from another API endpoint, then add the random database-stored value to both the API that provides the integration token and the magic link endpoint.

As for portal.js needing to know which endpoint to hit, that’s literally why I said store the value in the database. This way, the value is injected into the frontend config at runtime.

By pulling the string from the DB, the server can dynamically update the portal.js configuration or the global ghost object with the rotated value on boot or set timescale. A bot would have to actually ‘load’ and parse the site like a real browser to find the current valid endpoint, rather than just blasting a known-to-everyone hardcoded URL.

You’re arguing that ‘attackers will just update their code,’ but you’re missing the bot economics. These aren’t targeted hacks. They are mass-scale, low-effort scripts hitting a known, hardcoded path. Forcing a bot to parse a dynamic endpoint and a fresh integration token for every single site it hits raises the cost of the attack from free to rather expensive.

If the choice is complexity in the codebase vs. free signups are a nightmare because of a few dumb scripts, the codebase will lose every time.

Security that penalises the owner more than the attacker is an utter failure.

Another way that this could be mostly solved is to add a feature in the Ghost Admin Panel where the user can drop the URL for a solid blocklist that blocks requests from known bad actors.

We don’t need a perfect solution that stops state-level actors. We need a smart solution that makes mass-automated spamming far too annoying to be worthwhile to the attacker.

I agree with you, except the idea of changing url will affect the attacker economy. Delivering the dynamic url to client to allow use by portal.js, without breaking any 3rd party Ghost themes, without breaking full-site cache we love to use, without affecting visitor experience and making considerably hard for attacker to read, is the real challenge here.

But if you think that’s easy, please make a PR on GitHub Repo, or at least a GitHub issue that explains the exact technical solution that you think it will work. I would like to see your solution in detail, in GitHub.

1 Like

I’ve had a couple of these lately and they ended up going to someones inbox who then reported the signup as spam on my domain which was disheartening since i keep a pretty clean mail server record.

Is anyone else hosting behind cloudflare and if so have any of WAF page rules triggering the human verification check mark through cloudflare helped?

1 Like

IIRC (there’s a thread around here somewhere…), most of this traffic is coming from tor nodes, and you can block those (they’re a “country”) in cloudflare.

3 Likes

Correct. Blocking country T1 is what blocked the initial wave on Magic Pages quite successfully:

The same spam has then shifted to a russian data center provider known for spam proxies, which was then blocked through the respective ASNs.

So, it’s a long game of whack-a-mole. But arguably, the ASNs I blocked should have been blocked anyway. No actual legitimate traffic other than spam coming from them.

3 Likes

Nice, I missed that in the rules. added it now. Great catch

I think perhaps at a certain point, from an application perspective there can only be so much done and you have to rely on other layers of prevention and protection.

on top of some of the cloudflare rules, im also doing some limitations with using bad ip reports in abuseipdb, I have a script which is pulling IPs from that site and adding them to block lists in my firewall.

I’m seeing a new-to-me pattern, despite having a custom magic link endpoint. Before, it’s been signups that never confirm.

Now, I’m seeing a handful sign-ups in the past few days that are from custom domains of actual companies. I’m noticing a suspicious pattern: they’re all custom domains (that aren’t in the tech sector), and they log-in 2–3 times within a minute, rather than just once when they confirm.

@muratcorlu I think you mentioned seeing this pattern before. Were these blocked by blocking T1 in Cloudflare, or not? It seems like they must be running a headless browser, and in control of the domains the emails are from.

Addresses are firstname.lastname@domain.com, or what looks like [initial(s)][lastname]@domain.com, and similar.

They all come in on the homepage, direct. (It’s tricky, because I just got a big spike in signups, that came in on the http version of the homepage, that I assumed were spam, but turned out to be real.) I see visits in my stats from the same countries as these addresses, that enter on the homepage and click the subscribe button three times. One of them then navigated around. Both instances are Chrome 142 on Linux, 1920x1080. I partially anonymize IP addresses in my stats, but I see reports of cryptojackers or spammers using the same subnet, in both cases, and in at least one there’s a known exit node. None of this means that they’re for sure spam, but it seems increasingly likely.

I see domains like a-zcorp.com, worldwide.com, trinity-pm.com, and e-arc.com. Locations, in Ghost, are listed as Nevada, Germany (2), Denmark, etc. Doing quick searches for “domain.com spam”, I noticed that e-arc.com was recently hit by a ransomware attack. Other businesses look either legitimate, or scummy, but like they are appearantly actual businesses? And some of the names used match Linkedin profiles for people at the businesses. So maybe they are real sign-ups, or maybe they are real email addresses that have been compromised?

Any insight into motivation and migitation strategies for this particular type of sign-up would be much appreciated!