How to Patch a Hacked Joomla Site : Incident Response Runbook [2026]
Contain, find the entry point, then decide whether to clean or rebuild. Cleaning without finding the way in means being reinfected.
Your Joomla site is serving spam links, redirecting visitors to somewhere in Eastern Europe, or your host has suspended the account with a one-line abuse notice. The instinct at this point is to start deleting suspicious files, and that instinct will cost you: it destroys the evidence you need to find out how they got in, and cleaning without finding the entry point means being reinfected within days.
This is an ordered incident response procedure for Joomla 5.x sites, though the sequence applies equally to a Joomla 3 or 4 installation you have inherited. Work through it in order. The steps are ordered deliberately — several of them destroy information the later ones need.
Before anything: the two rules
First, contain before you investigate, and investigate before you clean. A site that is actively serving malware to visitors or sending spam is doing ongoing harm and needs to stop now. But stopping it is not the same as cleaning it, and cleaning it is not the same as understanding it.
Second, do not delete anything yet. Every file you remove before taking a forensic copy is a piece of the answer to “how did they get in” that you no longer have. The single most common outcome of a rushed cleanup is a site that is cleaned three times in a fortnight because nobody ever found the hole.
If you clean a compromised site without identifying the entry point, you have not fixed anything. You have removed the attacker’s current foothold from a site that is still vulnerable to whatever let them in the first time. Reinfection typically follows within days, often within hours, because the same automated tooling rescans the same targets.
Phase 1: Contain
Take the site offline at the web server, not in Joomla
Do not use Joomla’s Site Offline setting. That is an application-level switch — it runs after Joomla has bootstrapped, and any injected code in a plugin, a template or a modified core file executes before the offline check. It also does nothing at all for a webshell sitting in /tmp, which is a standalone PHP file that never touches Joomla’s bootstrap.
Instead, block at the web server. In Apache, place this at the top of the document root .htaccess, substituting your own address:
Order deny,allow
Deny from all
Allow from 203.0.113.7
Or rename the document root and point the domain at a static holding page. The goal is that no PHP in the compromised tree executes for anyone but you.
Preserve the evidence before touching anything
Take a complete forensic copy — files and database — and store it somewhere the compromised site cannot reach. This is not your restore point; it is your evidence. Label it clearly so nobody restores from it later by mistake.
tar czf /home/account/forensic-$(date +%F).tar.gz \
--exclude='*.tar.gz' /home/account/public_html
mysqldump -u dbuser -p dbname > /home/account/forensic-$(date +%F).sql
Preserve the logs at the same time, and preserve them first if disk space is tight — access logs rotate, and on a busy shared host the window that contains the initial intrusion may be gone in days.
cp /home/account/logs/*access* /home/account/forensic-logs/
cp /home/account/logs/*error* /home/account/forensic-logs/
Tell the people who need to know
Notify your host — they may have server-side logs and scan results you cannot see, and a host that discovers the compromise on their own after you have hidden it will be much less helpful. If the site handles personal data, start the clock on your notification obligations now rather than at the end of the cleanup; under GDPR the 72-hour window runs from awareness, not from resolution.
Phase 2: Assess the scope
Before hunting for individual files, establish how big the problem is. Three questions, in this order.
Is it only this site? On shared hosting, one account often holds several sites, and cross-site infection through a shared parent directory is routine. Check every document root under the account, not just the one that was reported. If your hosting account holds five Joomla installations, assume all five are in scope until you have checked them.
How long has it been going on? This determines which backups are safe. Find the oldest modified file that does not belong:
find /home/account/public_html -name "*.php" -newermt "2026-06-01" \
-printf "%T+ %p\n" | sort | head -50
What is it doing? Spam link injection, redirect-on-mobile-only, cryptominer, phishing page host, mail relay and defacement all have different urgency and different notification consequences. A phishing page hosted under your domain is a legal and reputational problem of a different order from an SEO spam injection.
Check whether your site is flagged in Google Search Console under Security Issues, and check the mail queue on the server. A full outbound mail queue means the site is being used as a spam relay, which will get the server’s IP blacklisted and makes this considerably more urgent for your host than for you.
Phase 3: Find the entry point
This is the phase people skip, and skipping it is why sites get reinfected. There are only a handful of realistic ways into a Joomla site, and they leave different traces.
| Entry route | Frequency | Where the evidence is |
|---|---|---|
| Unpatched third-party extension | Very common | Access log: POST or crafted GET to a component path, followed by a new file |
| Unpatched Joomla core | Common on old installs | Access log requests to core endpoints; version in System Information |
| Stolen or brute-forced admin credentials | Common | Successful /administrator login from an unfamiliar IP |
| Compromised FTP/SSH/cPanel credentials | Common | FTP transfer logs; no corresponding HTTP request for the new files |
| Neighbouring site on shared hosting | Underrated | Files owned by another account; nothing in your own logs |
| Malicious or backdoored extension | Occasional | Extension install entry in the action log |
The method is to work backwards from a known-bad file. Take the oldest malicious file you found in phase 2, note its exact modification time, then read the access log around that timestamp:
grep "05/Jun/2026:14:2" /home/account/logs/access.log | grep -v "\.css\|\.js\|\.png\|\.jpg"
You are looking for the request immediately before the file appeared. If it is a POST to a component path, that component is your entry point. If it is a successful administrator login, your problem is credentials, not code. If there is no HTTP request at all in that window, the files arrived over FTP or SSH — which means the credential compromise happened on a workstation, and cleaning the site will not be sufficient.
Sometimes the logs have rotated away and the entry point is genuinely unrecoverable. That is not a reason to give up and clean anyway — it is a reason to rebuild rather than clean, and to rotate every credential associated with the site. An unknown entry point plus a cleaned-in-place site is the worst combination available.
Phase 4: Identify persistence
Competent attackers, and most automated toolkits, establish several independent footholds so that removing one does not lock them out. Assume there is more than one. The five sections below are the places to look, and you should work through all of them even after you find something — stopping at the first hit is how the second backdoor survives.
Persistence: injected administrator users
Look for accounts you do not recognise, and specifically for recently created ones with Super User privileges:
SELECT id, name, username, email, registerDate, lastvisitDate
FROM prefix_users ORDER BY registerDate DESC LIMIT 20;
SELECT u.username, u.email, g.title FROM prefix_users u
JOIN prefix_user_usergroup_map m ON u.id = m.user_id
JOIN prefix_usergroups g ON m.group_id = g.id
WHERE g.title LIKE '%Super%';
Watch for the subtler variant: rather than adding an account, the attacker changes the email address on an existing legitimate Super User so they own the password reset route. Check the email address on every privileged account, not just the account list.
Persistence: modified core files
Joomla’s core files are published, so you can compare against a known-good copy. Download the full package matching your exact version, extract it to a temporary directory, and diff:
diff -rq /tmp/joomla-clean /home/account/public_html \
| grep -v "^Only in /home/account/public_html: images"
Every differing core file is suspicious. Expect legitimate differences only in configuration.php, .htaccess, robots.txt, and your own template and media directories.
Persistence: webshells and dropped files
The classic locations are the writable directories: /images, /tmp, /cache, /logs and the media folders. A PHP file in /images is essentially never legitimate:
find /home/account/public_html/images -name "*.php"
find /home/account/public_html/tmp -name "*.php"
grep -rlE "eval\(|base64_decode\(|gzinflate\(|assert\(|preg_replace\(.*/e" \
/home/account/public_html --include="*.php"
That grep produces false positives — some legitimate extensions use base64_decode for benign reasons — so read the hits rather than deleting on sight. What you are looking for is obfuscated code: long base64 strings, character-by-character string construction, or eval applied to something derived from $_POST, $_GET or $_COOKIE.
Persistence: database-resident payloads
This is the one most cleanups miss entirely. Injected JavaScript frequently lives in article content, module content, or the template style parameters — nothing on the filesystem is modified at all, so a file scan comes back clean while the site keeps serving malicious script.
SELECT id, title FROM prefix_content
WHERE introtext LIKE '%<script%' OR fulltext LIKE '%<script%';
SELECT id, title FROM prefix_modules WHERE content LIKE '%<script%';
SELECT template, params FROM prefix_template_styles;
Also check prefix_extensions for enabled plugins you do not recognise, particularly system plugins, which run on every request and are a favourite persistence location.
Persistence: .htaccess injections and scheduled tasks
Check every .htaccess in the tree, not just the root one. Conditional redirects keyed on user agent or referrer are the standard pattern — the site looks fine to you and redirects visitors arriving from search results:
find /home/account/public_html -name ".htaccess" -exec ls -la {} \;
find /home/account/public_html -name ".htaccess" -exec grep -l "RewriteRule" {} \;
Then check crontab -l, any control panel cron entries, and Joomla’s own System → Scheduled Tasks. A cron job that re-downloads a payload every hour will undo your entire cleanup silently.
Phase 5: Clean and restore, or rebuild
Now the decision. Both routes are legitimate; one is right far more often than people expect.
| Clean in place | Rebuild from known-good | |
|---|---|---|
| Time | Hours to days, open-ended | Half a day, predictable |
| Confidence in the result | Never complete | High |
| Requires | Deep familiarity with the site | A pre-compromise backup or the ability to rebuild |
| Right when | Infection is trivial and fully understood; no clean backup exists | Almost always |
| Failure mode | A missed backdoor; reinfection | Losing content created after the last clean backup |
Rebuild usually wins, for a reason that is uncomfortable but true: you cannot prove a cleaned site is clean. You can only say you did not find anything else. A rebuild lets you make a positive statement — every file in this tree came from a source I trust — and that is a categorically stronger position.
The rebuild procedure
Provision a clean directory, ideally on a fresh hosting account or at least a fresh document root. Then:
- Install the current Joomla 5 full package from the project’s downloads page. Not from your backup.
- Reinstall every extension from the vendor, at its current version. Not from your backup. Any extension that is no longer maintained does not come back.
- Reinstall your template from source. If it was custom-built, review the template files by hand before copying them across — templates are a common injection site.
- Import the database, then clean it using the queries in phase 4 before pointing the site at it. Content is what you are recovering; the database is where the injected payloads hide.
- Copy across
/imagesand any user upload directories — after scanning them for PHP files and verifying that every file is the type its extension claims.
If you are cleaning in place instead — because there is no usable backup and the site is too large to rebuild — replace all core files wholesale with a fresh copy of the same version rather than editing individual infected files, then reinstall every extension over the top. Editing out malicious code line by line is how backdoors get missed.
Phase 6: Rotate every credential
Do this after the clean environment exists but before it goes live. Assume everything the site could reach has been captured.
| Credential | Why |
|---|---|
| All Joomla user passwords | Hashes may have been dumped and cracked offline |
| Database user password | Plaintext in configuration.php, which the attacker could read |
| FTP, SFTP and SSH keys | Common entry route and common persistence route |
| Hosting control panel | Often reused, and grants everything else |
| The Joomla site secret | Used for session and token integrity; regenerate it in configuration.php |
| SMTP and API credentials | Stored in configuration and extension settings; used for spam relay |
Force a password reset for all users rather than trusting your own. If the entry point was a workstation compromise — files arriving over FTP with no matching HTTP request — the workstation needs cleaning too, or you will rotate credentials straight back into the attacker’s hands.
Phase 7: Verify before you go live
Do these checks with the site still restricted to your own IP.
Verify the file tree
Run the core diff again against a clean package. It should return only your legitimate differences. Then generate a hash manifest and keep it — this is your baseline for detecting future changes.
find /home/account/public_html -type f -name "*.php" \
-exec sha256sum {} \; | sort -k2 > /home/account/baseline.txt
Verify behaviour, not just files
Request the homepage with a search engine referrer and a mobile user agent, since conditional redirects usually only trigger for those. Compare the rendered HTML against what you expect:
curl -sA "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X)" \
-e "https://www.google.com/" https://example.com/ | grep -i "script src"
Also check the site as Googlebot sees it, in Search Console’s URL Inspection tool. Cloaked spam is designed to be invisible to the site owner.
Apply the hardening that prevents the recurrence
Patch everything to current. Enable multi-factor authentication on every privileged account. Restrict /administrator. Set configuration.php to 444. Move tmp and logs outside the web root. If you know the entry point, address it specifically — and if the culprit was an abandoned extension, replace it rather than reinstalling it.
Phase 8: After it is live again
Request a review in Google Search Console if the site was flagged, and check whether the domain or server IP landed on any mail blacklists — that is a slower problem to unwind than the compromise itself and it is easy to miss until customers report undelivered mail.
Then watch the site properly for a month. Daily file-hash diffs against your new baseline, the Joomla action log reviewed weekly, and external content monitoring on the homepage. Reinfection, when it happens, usually happens quickly, and a diff catches it in hours rather than in the next abuse report.
Finally, write down what happened: the entry point, the timeline, what you changed. It takes twenty minutes and it is the only thing that turns an incident into an improvement.
What not to do
- Do not restore from backup and stop there. That reinstates the vulnerability along with the site. Restore, then patch, with the site offline in between.
- Do not trust a malware scanner’s all-clear. Scanners match known signatures. A hand-placed backdoor in a legitimate-looking file will not match anything.
- Do not leave the old installation in a subdirectory as a “just in case”. It is still executable and still reachable.
- Do not skip the database. A file-only cleanup on a database-resident injection produces a site that scans clean and still attacks visitors.
Frequently asked questions
Can I just restore yesterday’s backup and be done?
Only if you are certain the compromise happened after that backup was taken, and only if you patch the vulnerability before bringing the site back. Compromises are routinely discovered weeks after the initial intrusion, which means yesterday’s backup is usually already infected. Establish the timeline in phase 2 before you choose a restore point.
How do I know which backup is clean?
Work from the timeline. Once you know the earliest modification time of an attacker-placed file, any backup taken before that date is a candidate — but verify rather than assume, by extracting the backup to a scratch directory and running the same core-file diff and webshell greps against it before you restore.
Is a malware scanner enough to clean a Joomla site?
No. Scanners are useful for finding obvious dropped files quickly, and worth running as a first pass. They will not find a backdoor hand-written to look like ordinary extension code, they generally do not scan the database, and they never tell you the entry point — which is the part that determines whether you get reinfected.
My host cleaned the site for me. Am I finished?
Probably not. Host cleanups are usually signature-based file removal aimed at stopping the abuse complaint, which is a different goal from securing your site. Run your own verification in phase 7, check for injected administrator accounts and database payloads yourself, and confirm the entry point was identified — it usually was not.
Do I have to tell anyone the site was hacked?
It depends on what the site holds. If personal data may have been accessed, most data protection regimes including GDPR impose notification duties on a short clock that starts when you become aware, not when you finish cleaning. If the site processes payments, your acquirer’s contract will have its own requirements. Get advice early rather than at the end.
Should I rebuild even for a simple spam-link injection?
Usually yes, and it is often faster than a thorough clean. The exception is a site you know intimately, where you have found the entry point, understood the full extent of the injection, and can diff everything against known-good sources. If any of those three is missing, rebuild.
How long should I monitor after the recovery?
At least a month of daily file-hash diffs, then fold it into normal maintenance. Reinfection from a missed backdoor typically surfaces within the first fortnight, so that is the window where close attention pays for itself.