Strange admin accounts, spam pages in the search results, files you didn’t put there — if that’s your morning, take a breath. A hacked site is recoverable, but the order of operations matters: contain, preserve, investigate, clean, then reopen. Skipping ahead to “just delete the weird files” is how sites get re-hacked the same week. You’ll need SSH access, drush, a couple of quiet hours, and a backup you’ve actually tested.
Take the site offline politely
First, stop the bleeding. Maintenance mode keeps visitors out of the application:
drush state:set system.maintenance_mode 1
drush cache:rebuild
But understand its limits — maintenance mode is Drupal being polite, and
malware doesn’t knock. If the compromise is in the code itself, return a
real 503 at the web-server level (your host’s dashboard, or a temporary
.htaccess rule) so PHP stops executing for the public entirely.
Preserve the evidence before you clean
The instinct is to scrub. Resist it for ten minutes. Copy the files, the database, and every log you can reach to somewhere off the server:
tar -czf ~/incident-$(date +%Y-%m-%d)-files.tar.gz /path/to/site
drush sql:dump --gzip --result-file=~/incident-$(date +%Y-%m-%d).sql
This copy is your map of what happened — and if the site handles payments or personal data, it may be evidence you’re obligated to keep. Never restore from it; it’s contaminated by definition.
Find the way in
Cleaning without knowing the entry point is bailing a canoe without finding
the leak. Check the usual doors: an unpatched security advisory
(cross-check your versions against
drupal.org/security — this is why
timely updates matter),
a stolen password, or a writable spot in sites/default/files executing
PHP. Look for accounts you didn’t make, especially privileged ones:
drush user:information --uid=1
drush sql:query "SELECT u.name, u.mail FROM users_field_data u"
Then read the access logs around each suspicious file’s modification time.
POST requests to odd paths just before a file appeared is usually your
answer.
Compare the code against known-good
If the site is in Git, this is one command:
git status
Anything modified or untracked is either yours or theirs — sort every file into one pile or the other, no exceptions. For core, contrib, and vendor code, don’t clean by hand: delete those directories and let composer rebuild them from upstream, which guarantees byte-for-byte clean copies. Custom code and the files directory you review line by line, searching for the classics:
grep -rn "eval(base64_decode" web/sites/default/files web/modules/custom
Rotate every credential
Assume they read everything. Change all admin passwords, the database
password, $settings['hash_salt'] in settings.php (this invalidates
one-time login links they may have minted), SSH and SFTP credentials, and
any API keys the site holds. Do this after the code is clean — rotating
secrets a backdoor can still read is just telling them the new ones.
Patch the hole and restore clean code
Now close the door you found in step three: apply the security update,
remove the abandoned module, or fix the file permissions. Deploy the
cleaned code, run drush updatedb -y and drush cache:rebuild, verify the
site works while still behind the 503, then reopen:
drush state:set system.maintenance_mode 0
drush cache:rebuild
Watch the logs for their return
You’re not done at reopening — you’re done two weeks later. Attackers
retry the same door, so tail the access logs for the paths and IPs from
your investigation, watch for new accounts and file changes, and request
recrawls in Search Console if spam pages got indexed. Success looks like
this: two quiet weeks of logs, git status still clean, no accounts you
didn’t create. Write up what happened and how they got in while it’s
fresh — that document is the difference between an incident and a lesson.