Skip to main content
Joomla Website Hacked: What To Do, Step by Step
On this page

Joomla Website Hacked: What To Do, Step by Step

09 August 2026

Sooner or later it happens to someone you know, and one day it may happen to you: you open your website and it is not yours anymore. Strange pages appear in Google, visitors get redirected to a gambling site, or your host switches the account off with a short email about "malicious activity". The next few hours decide whether this becomes a bad afternoon or a bad month.

This article explains what to do when your Joomla website is hacked. It covers the first steps for owners and editors, the investigation and clean-up for administrators, and the forensic details for developers. You will learn how to recognise a compromise, how to contain it without destroying the evidence, how to find the entry point and the backdoors, how to decide between cleaning and rebuilding, how to bring the site back safely, and who you have to inform afterwards.

Cleaning a hacked site without finding the entry point is not a repair. It is an invitation to come back.

The goal is simple: give you a calm, ordered plan for a moment when panic is the natural reaction.

1. The Basics

1.1 What "Hacked" Actually Means

A hacked website is a website where someone else can run their code on your hosting account. That is the core of it. Everything you see on the surface - spam pages, redirects, a defaced homepage - is only the visible use of an access that already exists.

This matters for how you react. Removing the spam pages does not remove the access. As long as the attacker keeps a way in, they will simply put the spam back, usually within days.

Most compromises fall into a few groups:

TypeWhat the attacker wantsWhat you notice
SEO spam Backlinks and rankings for their own clients. Hidden pages about pharmacy, loans, or casinos in Google.
Redirects Traffic they can sell. Visitors from Google land somewhere else, but you see a normal site.
Phishing Credentials of other companies' customers. A bank or parcel-service login page in a folder you never made.
Mail abuse A clean server to send spam from. Your domain lands on blocklists; mail stops arriving.
Cryptomining or a botnet Your CPU. The site becomes slow; your host complains about resource use.
Defacement Attention. Your homepage replaced with a message. Rare, and at least honest.
Data theft Your user table, your customer data. Often nothing at all. This is the one that has legal consequences.

Notice the pattern: in most categories, the attacker actively tries to keep the site working. A broken site gets fixed. A working site keeps earning them money quietly for months.

1.2 Signs Your Joomla Site Is Compromised

Some signals are might be obvious, others are easy to explain away. Take all of them seriously:

  • Google Search Console reports "Security issues" or new pages you never created.
  • Search results for your domain show titles in another language or another business.
  • Visitors report redirects, but you cannot reproduce them when you visit directly. Many attacks only trigger for visitors coming from a search engine, or only on mobile.
  • Your browser or antivirus warns about the site.
  • A Super User account exists that you do not recognise, or an existing account suddenly has more rights.
  • PHP files appear in images/, tmp/, or cache/, folders that should never contain code.
  • File modification dates in the Joomla core folders are recent, while you did not update anything.
  • Outgoing mail bounces, or your domain appears on a spam blocklist.
  • Your host suspends the account or sends an abuse notification.
  • Server load or bandwidth jumps without a matching rise in visitors.

A single odd signal can have an innocent cause. Two or three together almost never do.

1.3 The First Rule: Do Not Start Deleting

The natural reaction is to find the strange file and delete it immediately. Resist that. The strange file is also your evidence, and it carries a timestamp that tells you when the break-in happened. Delete it and you lose the thread that leads to the entry point.

Work in this order instead, and finish each phase before you start the next:

  1. Contain - stop the damage from continuing.
  2. Preserve - take a forensic copy before you change anything.
  3. Investigate - find out when and how they got in.
  4. Eradicate - remove the malicious code and close the hole.
  5. Recover - bring the site back and watch it.
  6. Report - inform the people who have a right to know.

The rest of this article follows exactly those six phases.

Back to top

2. The First Hour: Contain the Damage

2.1 Take the Site Offline

As long as the site is reachable, it keeps harming visitors, keeps sending spam, and keeps collecting search-engine penalties. Switch it off.

The best method depends on how bad it is:

MethodWhereWhen to use it
Joomla Site Offline System → Global Configuration → Site Offline Light cases. Joomla still runs, so injected code in Joomla still runs too.
Server-level block .htaccess deny rules or a maintenance page from your host Better. PHP is bypassed for visitors, and the site is genuinely closed.
Move the site out of reach Shell: move the folder aside and set it to 700 Best when you have SSH. Nothing is served, nothing is deleted, everything is preserved.
Suspend the account Hosting control panel Serious cases: phishing pages, active mail abuse, or a demand from your host.

Joomla's own offline mode is the weakest of the three. It stops normal visitors, but a backdoor placed in a PHP file is reached directly by URL and never asks Joomla whether the site is offline. If you suspect a webshell, block at the server level.

If you have shell access, the fourth option is the one I reach for, and the one I used on my own hacked site (section 4.6), because it is the only method that contains the incident and protects the evidence in the same move:

mv public_html public_html.hacked
chmod 700 public_html.hacked
mkdir public_html
printf '<h1>Temporarily unavailable</h1>' > public_html/index.html

The compromised site is now unreachable from the web, including every webshell in it, because the web server user can no longer enter the folder. Nothing has been deleted, so every file and every timestamp survives for phase 2. Visitors get a plain static page that involves no PHP at all, which means no Joomla, no extensions, and nothing left for an attacker to call.

Check afterwards which user your PHP actually runs as. If PHP runs as the same account that owns the files, 700 still permits access; in that case move the folder outside the web root entirely, or take the site down at the hosting level.

2.2 Preserve the Evidence Before You Change Anything

Before you touch a single file, make a complete copy of the compromised state: all files and a full database dump. Label it clearly as infected and store it outside the web root, ideally off the server entirely.

This copy is not a backup you will ever restore. It is your evidence. You need it because:

  • File timestamps and log entries disappear as soon as you start cleaning.
  • You will want to compare the infected files with clean ones to find every injection.
  • If personal data was involved, you may have to explain later what happened and when.
  • If you clean too aggressively and break something, you can still look up how it was.

Copy the files with a method that keeps modification times intact. On the command line, rsync -a or tar preserve them; a drag-and-drop through some FTP clients does not.

2.3 Tell Your Host

Contact your hosting provider early, even if you plan to fix everything yourself. Two reasons: their server logs usually go back further than what you can see in your own account, and a compromise sometimes starts outside your site - in a neighbour account, in a stolen control-panel password, or in a server-level vulnerability. Your host is the only party who can check that.

Ask them specifically for the raw access logs of the last few months, the FTP and SSH login history for your account, and whether they see the same infection pattern on other accounts.

Sometimes the conversation runs the other way, and the first you hear of the hack is an email from someone else. Security organisations scan for defaced and malicious pages and report them to the hosting provider and to the domain registry. For .nl domains, for example, Netcraft reports findings to SIDN, the registry that manages the zone. Your host may forward such a report, or send their own.

Treat those messages as urgent, because they usually carry a deadline and real consequences: hosting providers suspend accounts, and a registry can suspend the domain name itself if nobody responds. Reply straight away to say you are working on it, even before you have fixed anything. A reported site that answers is treated very differently from a reported site that stays silent, and you will need to report back once it is clean (section 9.6).

2.4 Do Not Restore a Backup Yet

Restoring last night's backup feels like the fastest fix, and it is the most common early mistake. Two things usually go wrong:

  • If the site was compromised weeks ago, last night's backup contains the backdoor too. You restore the hack.
  • Even a genuinely clean backup goes straight back to the vulnerable state that let the attacker in. Without the update or the password change, you are reinfected in days.

A backup restore is part of phase 4, after you know when the break-in happened and what caused it. Not before.

2.5 Stop the Site From Sending Mail

If the site is being used to send spam, every extra hour damages your domain reputation and pushes you further onto blocklists. Getting a domain delisted takes far longer than the hack does.

Joomla has a single switch for this: set Send Mail (mailonline) to No under System → Global Configuration → Server → Mail Settings. It is on by default, and turning it off stops Joomla from sending anything at all - including your own contact forms, so this is a deliberate temporary measure.

Be aware of the limit, which is the same limit as the offline mode: this stops Joomla from sending mail. Malware that calls PHP's mail() directly, or talks to an SMTP server itself, does not go through Joomla and ignores this setting completely. If the spam continues after you flip it, the sender is a file, not Joomla, and you need the server-level block from section 2.1 plus your host's help with the mail queue.

2.6 Assume the Neighbours Are Affected Too

If several websites live in the same hosting account, treat all of them as compromised from the start, not just the one where you noticed something. Where all sites run as the same Unix user, code running in one of them can usually read and write the files of the others, so the infection spreads sideways without any second break-in.

That changes your first hour: take stock of every site in the account, check each one for the signals in section 1.2, and plan the clean-up for the whole account at once. Cleaning them one at a time, while the others stay infected, means the first site you finish gets reinfected while you work on the second.

Back to top

3. Lock Out the Attacker

3.1 Rotate Every Credential

Assume every password that touched this site is known to the attacker. Change all of them, and do it from a computer you trust - if your own machine has an information stealer on it, that is where the passwords leaked in the first place.

  • All Joomla Super User and Administrator passwords.
  • The hosting control panel password.
  • FTP, SFTP, and SSH accounts, including any keys. Delete unknown SSH keys from authorized_keys.
  • The database user password in configuration.php.
  • Web Services API tokens, and tokens of any third-party integration.
  • SMTP credentials used for outgoing mail.
  • DNS and CDN accounts. Someone who can change a DNS record does not need your site at all.
  • Backup storage credentials. An attacker who reaches your backups can destroy exactly the clean copy you are about to need.
  • Deployment tokens, Git credentials, and any CI or webhook secret stored on the server.
  • Any account you reused that same password on. This is where a single leak becomes five incidents.

Work out the reach before you start, because the list above is only a default. Anything whose secret was stored on that server is now suspect, and configuration.php is the clearest example: if the attacker could read files, assume they read it, which hands them the database credentials and the site secret in one go. The same applies to a .env file, a stored deployment key, or a password sitting in a config file of another site in the same account.

3.2 Change the Site Secret

Your configuration.php contains a $secret string. Joomla uses it in more places than people realise, and changing it is one of the most effective single actions after a breach.

Two concrete effects, both visible in the core code:

  • Every session ends. Joomla derives the session cookie name from md5($secret . $session_name). Change the secret and every existing session cookie points at a session name that no longer exists, so every logged-in user - including the attacker - is logged out instantly.
  • Every API token dies. The Joomla API token plugins verify tokens with hash_hmac() using the site secret as the key. A new secret makes every issued token fail verification, including tokens the attacker created for themselves.

Generate a new random string of decent length and replace the value. Note what it does not break: multi-factor authentication methods live in #__user_mfa and keep working, and normal passwords are unaffected because they are bcrypt hashes, not secret-derived.

3.3 Clear Sessions and Remember-Me Cookies

If you prefer to be explicit rather than rely on the secret change, empty the session table directly:

TRUNCATE TABLE `#__session`;

Then deal with persistent logins. The System - Remember Me feature stores long-lived login tokens in #__user_keys, and those survive a normal logout by design. After a breach, clear them all:

TRUNCATE TABLE `#__user_keys`;

Everyone has to log in again. That is the point. As a bonus, Joomla deletes a user's rows in #__user_keys automatically when you block that user, so blocking a suspicious account also kills its persistent cookies.

3.4 Review Every Account

Open Users → Manage, sort by Registration Date descending, and look at everything created around and after the suspected break-in. Then check the group assignments, because a quieter attack does not create a new account at all - it adds an existing, forgotten editor account to the Super Users group.

SELECT u.id, u.username, u.email, u.registerDate, u.lastvisitDate, g.title
FROM `#__users` AS u
JOIN `#__user_usergroup_map` AS m ON m.user_id = u.id
JOIN `#__usergroups` AS g ON g.id = m.group_id
ORDER BY u.registerDate DESC;

Block what you do not recognise instead of deleting it right away - a blocked account cannot log in, but you can still see it while investigating. Also look for accounts whose email address was changed to an address at a domain you do not know: that is the classic preparation for a password reset.

Back to top

4. Find Out How They Got In

4.1 The Joomla Action Log

The User Actions Log (com_actionlogs, fed by the System - User Actions Log plugin) is the first place to look. Open Users → User Actions Log. It records logins, extension installs, article and module changes, and configuration saves, with a user name and a timestamp. You can export the whole list to CSV from the toolbar, which is much easier to work through in a spreadsheet.

Two defaults are worth knowing before you rely on this log:

  • IP logging is off by default (ip_logging in the component options). If nobody switched it on, your log tells you who and when, but not from where.
  • API actions are not logged by default (loggable_api). Activity through the Web Services API leaves no trace in this log unless that option was enabled.

Both are switches to turn on today, while you are looking at the screen anyway. They cost nothing and they are exactly what you will wish you had next time.

There is one pattern in this log that people consistently overlook, because it does not look like an attack at all: a successful login by your own account, at a moment when you were not working. Everyone scans the list for unfamiliar user names. Nobody questions their own.

So check your own line. Compare every recent login of every administrator against where those people actually were, and confirm it against something independent - your browser history, your calendar, your phone. A login you cannot account for is one of the strongest signals in this entire article, and it says something specific: the attacker did not need your password. If your password still works and was never reset, they arrived with a stolen session, a leaked token, or a flaw that let them authenticate as you. Changing that password alone will not remove them, which is why section 3.2 and section 3.3 exist.

4.2 The Failed-Login Log

The System - Log plugin listens to the onUserLoginFailure event and writes every failed login attempt to a log file. By default that is error.php in your log folder (administrator/logs/ unless you moved it in the Global Configuration).

Read it for two patterns. Thousands of failures from many addresses is ordinary background noise from bots. A short burst of failures on one existing user name, followed by silence, is the interesting one: silence after failures often means they finally succeeded.

The plugin has a log_username option that adds the attempted user name to each line. Without it you only see that a login failed, not which account was targeted. Switch it on.

4.3 Server Access Logs

The web server log is where the actual break-in is usually visible, because it records requests that never reached the Joomla application layer. Ask your host for the raw logs, then look for:

  • POST requests to files that should never receive a POST, especially anything under images/, tmp/, or media/.
  • Requests to file names you do not recognise, particularly short random names ending in .php.
  • Long, encoded query strings full of % escapes or base64-looking text.
  • Requests to a known vulnerable component URL, for example index.php?option=com_something&task=... for an extension you have installed.
  • The very first request from the IP address that later uploads files. That request is usually the exploit.

A practical trick: once you know the timestamp of the oldest malicious file, grep the access log for that exact minute. The request that created the file is almost always right there.

Expect to find more than one visitor. Once a site is broken into, the address often gets shared, and opportunists arrive to use the webshell somebody else installed. A second IP address appearing hours later, coming in from a social network and going straight to a file it could not possibly have guessed, is a familiar sight. It does not mean two separate break-ins; it means the first one was advertised. It does raise the urgency, because each newcomer does their own damage.

If you have root on your own server, check the authentication log as well, to establish whether the attacker ever got beyond the web application:

grep Accepted /var/log/auth.log     # successful SSH and SFTP logins
grep Failed   /var/log/auth.log     # brute-force attempts

Seeing only your own logins here is genuinely good news: it suggests the compromise stayed inside the web account and never reached the server itself. Note the limit, though, and be honest with yourself about it: if the attacker gained write access outside the web root, they could have edited this log. It is evidence, not proof, and on shared hosting you will not have it at all - ask your host instead.

4.4 File Timestamps

Modification times are the cheapest forensic tool you have. On a shell, list everything changed in the last two weeks:

find . -type f -name "*.php" -mtime -14 -printf "%TY-%Tm-%Td %TH:%TM  %p\n" | sort

On a site that was not updated recently, this list should be almost empty. Cluster the results by time: file changes usually arrive in bursts of a few seconds, and the oldest burst marks the break-in.

Be aware that a careful attacker resets timestamps with touch to blend in. If the dates look suspiciously uniform, or a file's modification time is older than its inode change time (ls -lc versus ls -l), treat the timestamps as unreliable and lean on file comparison instead (section 5.1).

4.5 Build a Timeline

Write the findings down in one simple list, sorted by time: the first suspicious request, the first modified file, the new user account, the first spam page indexed. You are looking for the earliest event that has no innocent explanation. That is your break-in moment, and it answers two questions at once: which backup generation is still clean, and which vulnerability was open on that date.

Also check what your Joomla version and extension versions were on that date. Very often the answer is already there: an extension that had a known security release two weeks earlier and was never updated.

Then ask a second question, and ask it about yourself rather than about the software: what did I change shortly before that date and never change back? A surprising share of real incidents begins with a temporary measure taken while troubleshooting something entirely unrelated. Debug mode switched on to chase a mail problem. Error reporting set to maximum. A folder made writable "just for a moment". A test file uploaded to check something. Each is harmless for the ten minutes it was meant to last, and each is a real opening for the months it actually lasts, because the thing that ends the ten minutes is usually an interruption rather than a decision. Walk back through your own notes, your ticket history, and your mail from the weeks before the break-in.

The same reasoning explains how attackers find a site in the first place. Nobody targeted your small Joomla site by name; they searched for what it was leaking. Debug output, verbose error messages, and directory listings all contain distinctive text, and once a search engine indexes a page carrying it, that page becomes findable by anyone who knows what to search for. A referrer of https://www.google.com/ on the very first request from the attacker's IP address is the tell-tale here: they did not guess your domain, they were handed it by a search engine. Search your own domain for the strings your mistakes produce, and you will see roughly what an attacker sees.

4.6 A Worked Example: How This Played Out on My Own Site

The method above is not theory for me. In October 2022 the website of my own Joomla User Group, Arnhem-Nijmegen, was hacked, and my investigation ran exactly along these steps. It is worth walking through, because the site was three months old, ran only a handful of extensions, and was fully up to date. Being patched is necessary, and it was not sufficient.

The First Hour

I found out at a quarter past midnight, from an email. Netcraft, which reports findings to SIDN for the .nl zone, had discovered a defacement file on the domain and asked me to fix it or see the domain taken down. My VPS provider had been told as well, and warned me the whole VPS could be taken offline. That is the notification path from section 2.3, with the deadline attached.

Over FTP I found two files that we had never put there: Ox.php, the defacement, and sec.php, a backdoor with access to the filesystem and the database. I contained the site the way section 2.1 describes, moved it aside with restrictive permissions and served a static maintenance page. Then I archived the files, the database, and the logs and pulled them down to my own machine before touching anything. Then I went to bed, because an analysis needs a clear head and I had safequarded the evidence.

What the Logs Showed

The next day the database gave me the moment. #__action_logs showed a successful login for my own account on 12 October at 15:46. However, I had been nowhere near a computer at that moment; my browser history confirmed it. My password still worked and had never been changed. That is the pattern from section 4.1, and it told me straight away that this was not a guessed password.

With that timestamp I went into the access log. The attacker's IP address first appeared two minutes earlier, on an ordinary article page, with a referrer of https://www.google.com/. Searching Google for that page showed why: it was indexed with Joomla's debug output visible on it. They had not gone looking for my user group. They had gone looking for sites leaking debug data, and Google handed them mine.

The root cause was mine and it was mundane. A JUG meeting had been cancelled, the newsletter about the cancellation failed halfway through with SMTP authentication errors, and I switched debug mode on to investigate. The real fault turned out to be DNS: two mail. records with different IP addresses, one of them wrong. I fixed it, sent the newsletter, posted the cancellation, and forgot to switch debug back off. Exactly the never-reverted temporary change described above.

The log also showed a second, unrelated visitor arriving later that evening from a Facebook link, who used the existing sec.php to read configuration.php and walk off with the database and SMTP credentials. One break-in, two attackers, and the reason section 3.1 insists you assume everything in that file is public.

What Came Out of It

Because core Joomla looked involved, I sent everything to the Joomla Security Strike Team. They confirmed a real issue, and it became CVE-2022-27912, advisory [20221001] Core - Disclosure of critical information in debug mode, affecting Joomla 4.0.0 to 4.2.3 and fixed in Joomla 4.2.4 on 25 October 2022. Only then did I rotate every credential and restore a clean backup. A German colleague whose Joomla 4 site had been hacked months earlier turned out to have had debug mode on too.

The CVE record is assigned by the Joomla project and lists me as the finder. I mention that for one reason only, and it is the point of section 9.5 rather than a credential: I do not hunt for vulnerabilities and I did not find this one on purpose. I got hacked, kept the evidence, wrote down what I saw, and handed it to people who could analyse it. That was enough. If your own investigation ever points at core, your notes are worth more than you think.

Two things are worth carrying forward. That specific hole is closed: in current Joomla the debug AJAX handler checks both the user's authorisation and a valid session token, so it cannot be called the way it was in 2022. But the habit that exposed me is still available to everyone, because the debug plugin's Allowed Groups option ships empty, which means that with debug enabled the output is shown to whoever is looking. If you must debug on a live site, restrict that option to Super Users first, and switch debug off when you are done.

I wrote the full account up at the time in the Joomla Community Magazine, with the log lines and the timings: How my new Joomla 4 website got hacked. The Joomla User Group meeting that set the whole thing off, the one we cancelled because exactly one person besides the presenter had signed up, was rescheduled to 2 November. That one drew a full room. Alongside a Joomla migration talk, there was a short presentation by yours truly called "How the JUG website was hacked".

Back to top

5. Find What They Left Behind

Attackers plant more than one way back in. Finding the first webshell is not the end of the job; finding the third one is. Work through every location below, even after you think you have found it.

5.1 Modified Core Files

Joomla core files are perfectly predictable, which makes them easy to verify: download the exact same Joomla version from the official site, unpack it locally, and compare it with your site.

diff -rq /path/to/clean-joomla-6.1.1 /path/to/site \
  | grep -vE "configuration.php|images/|tmp/|cache/|administrator/logs/"

Every "differ" line for a core file is a file to inspect. Every "Only in /path/to/site" line for a core folder is a file that does not belong to Joomla at all - your prime suspect list.

Joomla can also repair itself. In System → Update → Joomla, when no update is available, Joomla offers Reinstall Joomla core files, which writes all core files again over the existing installation. Understand the limit: it overwrites core files, it does not delete files that Joomla never shipped. It fixes an injected index.php; it does nothing about a webshell called media/vendor/x9k2.php.

5.2 Uploaded Webshells

A webshell is a PHP file that gives the attacker a command interface or a file uploader through the browser. It usually sits somewhere that accepts uploads. Search for executable code in folders that should only hold data:

find images/ tmp/ cache/ media/ administrator/logs/ \
     -type f \( -name "*.php" -o -name "*.phtml" -o -name "*.php[57]" \)

A legitimate Joomla site has very few PHP files in those places, and the ones that do exist are empty index.html-style guards. Anything else deserves a close look.

Then grep the whole site for the functions malware relies on:

grep -rEl "eval\s*\(|base64_decode|gzinflate|str_rot13|assert\s*\(|preg_replace\s*\(.*/e|\\\$_(POST|GET|REQUEST|COOKIE)\s*\[[^]]*\]\s*\(" \
  --include="*.php" .

This produces false positives - legitimate libraries use base64_decode for perfectly good reasons - so read the results, do not delete them blindly. What you are looking for is code that is deliberately unreadable: one enormous line, nested encoding, variable names like $_0x4a2, or a long base64 blob passed straight into eval().

Do not forget files that hide by their name. A file called .well-known/x.php, images/logo.png.php, or simply wp-config.php on a Joomla site is not a mistake.

Both searches above key on the file extension, and that is their weak spot. Read section 5.8 before you decide the folder is clean, because the payload you are looking for may not be called .php at all.

5.3 Injected Content in the Database

Cleaning the files and forgetting the database is a very common half-repair. Joomla stores plenty of content that is rendered as HTML, and injected scripts hide there comfortably:

SELECT id, title FROM `#__content`
 WHERE introtext LIKE '%<script%' OR fulltext LIKE '%<script%'
    OR introtext LIKE '%<iframe%' OR fulltext LIKE '%<iframe%';

SELECT id, title, module FROM `#__modules`
 WHERE content LIKE '%<script%' OR content LIKE '%<iframe%';

Check these places in particular:

  • Custom modules (mod_custom): the classic hiding place, because the module can be published on every page and nobody reads its content.
  • Articles: hidden <div style="display:none"> blocks full of links, usually in older archived articles.
  • Template style parameters in #__template_styles: many templates offer a "custom code" or "analytics code" field that is injected into every page head.
  • Category and menu item descriptions, which nobody ever reviews.

Related setting worth checking: System → Global Configuration → Text Filters. Super Users default to No Filtering, which is exactly why content saved through a hijacked Super User account can contain raw <script> tags in the first place.

5.4 Rogue Extensions and Plugins

A serious backdoor does not sit in a random file. It registers itself as a Joomla plugin, so Joomla loads it on every single request, and it survives a core reinstall.

Go to System → Manage → Extensions and sort by type and by name. Anything you cannot account for is suspect. Then check the database directly, because a well-made backdoor plugin hides from the extension manager by not being listed there properly:

SELECT extension_id, name, type, element, folder, enabled
FROM `#__extensions`
WHERE type = 'plugin'
ORDER BY folder, element;

Compare that list against the folders actually present under plugins/. A folder with no database row and a database row with no folder are both worth investigating. Use System → Manage → Discover carefully here: it finds unregistered extensions on disk, but its purpose is to install them, so use it to look, not to click.

5.5 Scheduled Tasks and Cron

Persistence does not need a web request. Check both layers:

  • Joomla Task Scheduler (System → Scheduled Tasks, table #__scheduler_tasks): look for tasks you did not create, especially ones that run very frequently.
  • Server cron jobs: run crontab -l for your account and check the control panel's cron page. A cron line that downloads and executes a remote file re-infects the site minutes after every clean-up, which is the classic explanation for "I cleaned it and it came back overnight".

5.6 Configuration and Entry-Point Files

A few files are loaded on every request, which makes them attractive targets:

FileWhat to look for
index.php (site and administrator) Code before or after the original content, or a single added include.
configuration.php Extra code outside the class, changed log_path or tmp_path, a changed $secret.
.htaccess Conditional redirects based on HTTP_REFERER or HTTP_USER_AGENT, or an ErrorDocument pointing at a PHP file.
.user.ini / php.ini An auto_prepend_file directive. This loads the attacker's code before every PHP script, without touching a single Joomla file.
Template index.php Injected script tags in the template output. Check the html/ override folder too.

The auto_prepend_file trick deserves special attention because it is invisible to a file comparison of Joomla itself. Everything in the Joomla folder can be perfectly clean while every request still runs foreign code.

5.7 The Redirect That Only Fires For Strangers

If visitors report redirects but you never see one, the malware is checking who you are before it acts. It commonly triggers only when the referrer is a search engine, only on mobile user agents, only once per IP address (stored in a cookie), and never when a logged-in Joomla session cookie is present.

Test it the way the attacker expects you not to: use a private browsing window, a mobile device on mobile data, and a request with a spoofed referrer.

curl -sI -A "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X)" \
     -e "https://www.google.com/" https://example.test/ | head -20

A Location: header pointing somewhere unexpected confirms it in one command.

5.8 The Payload That Is Not a PHP File

Every search in section 5.2 looks for a PHP file extension. That is precisely the assumption these attacks are built to walk past, and often not out of cleverness but out of necessity: the flaw the attacker is using may only allow them to write one harmless file type in the first place.

In July 2026 I helped a client whose old Joomla 3 site had been compromised through CVE-2026-49049, an access-control flaw in the Helix3 plugin from JoomShaper, published on 29 June 2026 and rated 7.5. Its description explains the whole attack in one line: the exposed Ajax handler task lets unauthenticated attackers delete arbitrary files, write arbitrary JSON files, and update template parameters.

Being able to write only JSON sounds like a limited problem. It is not. The bots wrote complete PHP web shells into .json files, which on its own achieves nothing, because a visitor cannot rename a file on your server and Apache serves .json as plain text. The bots also tried to place .htaccess files containing this:

<FilesMatch "\.json$">
  SetHandler application/x-httpd-php
</FilesMatch>

That small block is the whole attack. The attacker cannot rename the file, so instead they change the rule that decides what counts as PHP. SetHandler tells Apache which handler processes a matching request, and Joomla's shipped htaccess.txt says nothing about handlers at all. Once that block lands, requesting images/data.json stops being a download and starts being a web shell, running as your PHP user with access to your files and your database.

That is the general lesson, and it is worth more than the specific CVE: a write primitive restricted to a harmless file type is not harmless if the attacker can also change what that file type means. When you read an advisory that says "only allows writing JSON" or "only allows writing text files", do not treat it as low risk on the strength of the extension alone.

Why It Failed, and Why That Is Not Reassuring

On that client's server nothing ever executed. The site runs on nginx, and nginx does not read .htaccess files at all. The rootkits sat on disk as inert text while the bots kept trying. The attackers had prepared for Apache and landed on something else.

The useful lesson is not "nginx is safe". It is that the payload only runs if a handler is actually registered under the name the attacker guessed. application/x-httpd-php is the classic mod_php name. A modern PHP-FPM setup usually routes PHP by proxy instead, with a handler such as proxy:unix:/run/php-fpm.sock|fcgi://localhost, and cPanel with EasyApache uses versioned names such as application/x-httpd-ea-php81. Guess wrong and nothing happens. Guess right and you have remote code execution, which is why bots drop several variants in one visit. Whether your server survived this is a configuration detail, not a security control you chose.

A Second Route: Files the Web Server Parses Itself

Later that same month I cleaned up other client sites broken into through a different flaw, and the attackers used a different file type to reach the same goal. That is what makes the pattern worth learning rather than the individual trick.

The way in was CVE-2026-21627 in the Tassos Framework plugin (plg_system_nrframework), rated 9.5 critical on CVSS 4.0. Section 8.1 of the Focus On article on security hardening covers how that plugin came to still be installed, because that part is a lesson of its own. Here the interesting part is what the bots uploaded: a large number of .shtml files, plus a handful of .txt files.

.shtml is the classic extension for Server Side Includes. When Apache is configured to parse that extension through mod_include, and the directory carries Options +Includes, the server reads the file before sending it and acts on directives written as HTML comments:

<!--#include virtual="/some/other/file" -->
<!--#exec cmd="..." -->

The second one runs a shell command. No PHP is involved at any point, which is why a PHP-only malware scan sees nothing. On those sites the bots used their .shtml files to try to write .php backdoors into the web root, which is the same two-step shape as the JSON case: get any file in, then use a server feature to make that file do something.

It failed again, and again because of nginx, but for a different reason worth knowing. nginx does have an SSI module, but it is not enabled in most builds, and more importantly it has no exec cmd directive at all. It can include and echo; it cannot run commands. Apache's mod_include can, unless the directory is set to IncludesNOEXEC.

Joomla does not help you here either. The shipped htaccess.txt sets Options +FollowSymlinks and Options -Indexes, and says nothing about Includes. If your host enables SSI globally, and some shared hosts still do, every folder on your site inherits it. Adding Options -Includes to the upload folders costs nothing on a normal Joomla site, because Joomla never uses SSI.

What This Changes in Your Investigation

First, search by content instead of by extension. Any file in a data folder that contains a PHP open tag or an SSI directive is a finding, whatever it is called:

grep -rlE "<\?php|<!--#(exec|include)" \
  images/ tmp/ cache/ media/ administrator/logs/

The same payload turns up as .json, .shtml, .txt, .ico, .css, and .log. Malware scanners that only open PHP files miss all of them, and so does a hosting control panel that reports "no PHP files in images".

Second, treat every .htaccess file on the site as evidence. A clean Joomla 6 core ships exactly two: the one in the web root, renamed from htaccess.txt, and libraries/.htaccess, which denies all access to that folder. Anything else was put there by you, your host, an extension, or your attacker:

find . -name ".htaccess" -type f

grep -rl --include=".htaccess" \
  -E "SetHandler|AddHandler|AddType|php_value|php_flag|auto_prepend_file" .

Read every hit. A rewritten handler, an AddType application/x-httpd-php .json line, or an auto_prepend_file pointing at an image is a backdoor that survives deleting the web shell, because it re-arms the next file the attacker uploads. This is also why section 13.5 lists .htaccess next to plugins and cron jobs: it is a place code can hide without containing any code.

Section 8.1 covers the repair, and it is not the obvious one. Putting your own .htaccess in the upload folder is not enough when the attacker can write files into that same folder.

Back to top

6. Clean or Rebuild?

Now you decide how to get to a trustworthy site. There are three routes, and the honest ranking is not the one most people hope for.

6.1 Restore a Clean Backup - Fastest

If your timeline (section 4.5) says the break-in was on the 14th and you have a full backup from the 10th, restoring that backup is the fastest and most reliable route. You get a known state, with no guessing about what you missed.

The cost is the content created between the 10th and today, which you have to re-enter by hand. That is a real cost, but usually a smaller one than an incomplete clean-up.

Never restore the backup on top of the infected files. Delete everything in the web root first, then restore. Otherwise the webshell that is not part of your backup simply stays where it is.

6.2 Rebuild on Fresh Files - Most Reliable

When no backup is old enough, rebuild the file side from scratch and keep only the database and your own content. It takes a few hours and it leaves nothing behind, because you never copy an unknown file forward. Section 7 describes the procedure step by step.

This is the route I recommend for any site where the timeline is unclear, and the only sensible route when the compromise is older than every backup you own.

6.3 Clean In Place - Last Resort

Removing malicious code from the existing installation is the most attractive option and the one that fails most often. It only works if you can enumerate every change the attacker made, and you can never be quite sure that you did.

Reserve it for small, obvious, freshly discovered infections where the timeline is short and complete. Even then, follow it with a full comparison against clean sources.

6.4 The Database Is the Hard Part

Whichever route you take, the database usually cannot be replaced, because it holds the content you want to keep. It has to be reviewed rather than rebuilt: the injected content of section 5.3, the accounts of section 3.4, the extension rows of section 5.4, and the scheduled tasks of section 5.5.

One structural check is easy to forget. Go to System → Manage → Database, where Joomla verifies that your table structure matches the schema of your installed version, and fix anything it reports. It will not detect malicious rows, but it does tell you whether the structure itself was tampered with or left half-updated.

Back to top

7. Rebuild Step by Step

This is the procedure for a full rebuild on fresh files. Work on a staging location if you can, so the live account stays untouched until you are ready.

  1. Keep three things from the old site, and nothing else: a database dump, the images/ folder (files only, no PHP), and a note of your settings from configuration.php. Anything else you copy forward is a risk you accept without knowing it.
  2. Empty the web root completely. Not "delete the suspicious files" - delete everything. This step is what makes the whole approach reliable.
  3. Install the latest Joomla from the official download, into the empty web root, using a new database user with a new password.
  4. Import the reviewed database dump into the new installation, after you have removed the injected content and the rogue accounts.
  5. Reinstall your extensions from their original sources. Download every one fresh from the developer. Never reuse the extension files from the hacked site, and take this moment to skip the extensions you did not really need.
  6. Reinstall the template. Fresh download, then re-apply your own overrides from a source you trust - your Git repository, your local copy, or by re-creating them.
  7. Copy the images back, but filter them: only real media extensions, and no PHP whatsoever.
    rsync -av --include="*/" \
          --include="*.jpg" --include="*.jpeg" --include="*.png" \
          --include="*.gif" --include="*.webp" --include="*.avif" \
          --include="*.pdf" --exclude="*" \
          old-site/images/ new-site/images/
    Be careful with SVG files: they are XML and can contain scripts. If you need them, review them first.
  8. Set a new site secret in configuration.php (a fresh install already generates one) and configure the site again from the Global Configuration.
  9. Reset every password and require a password change for all users. In Users → Manage you can select all accounts and set Require Password Reset in one batch action.
  10. Enforce multi-factor authentication for every group that can reach the backend, in Users → Manage → Options. If a stolen password caused this incident, this is the setting that prevents the repeat.
  11. Update everything and check the versions once more. Rebuilding on an outdated extension reproduces the original hole exactly.
  12. Rotate the credentials a second time. This one surprises people, so it is worth being explicit: the passwords you changed in section 3.1 were changed while the backdoor was still running. Anything the attacker could read at that moment - the new database password in configuration.php, a new SMTP password, a token you pasted into a form - they may already have. Only now, on an environment you built yourself from clean parts, is a rotation actually final. Change them again.
Back to top

8. Bring the Site Back Online

8.1 Block PHP Where It Does Not Belong

Almost every webshell in section 5.2 depends on one assumption: that a PHP file dropped into an upload folder will actually be executed when someone requests it. You can take that assumption away, and it is the single most valuable thing you can add while rebuilding.

Joomla does not do this for you. The shipped htaccess.txt blocks directory listings, sets nosniff, and stops scripts inside uploaded SVG files, but it contains no rule against running PHP in images/. You add that yourself, in an .htaccess file inside the upload folder:

<FilesMatch "\.(?i:php|phtml|php[0-9]|phar|inc)$">
  Require all denied
</FilesMatch>

On nginx the equivalent lives in the server block, because nginx does not read .htaccess files:

location ^~ /images/ {
    location ~ \.php$ { return 403; }
}

Apply it to every folder that receives uploads and never needs to run code: images/, your media folders, and any upload target of a form extension. Test afterwards that your site still works and that a test file is genuinely refused, then delete the test file.

This does not make uploads safe on its own - the attacker can still store the file - but it turns a working backdoor into an inert file sitting on disk, which is a very large difference.

Put the Rule Where the Attacker Cannot Reach It

There is a hole in the advice above, and it is better to say it out loud than to discover it twice. The rule sits in an .htaccess file, inside a folder the attacker can already write to. They can overwrite your file, or drop their own in a subfolder, which Apache reads last and therefore lets win. Section 5.8 shows the version used in the wild, where the attacker's block re-enables PHP for .json files.

The repair is to move the decision out of the document root, into the server configuration, where nothing an upload form touches can override it. In an Apache virtual host:

<Directory /var/www/example.test/images>
    AllowOverride None
    Options -Includes -ExecCGI
    <FilesMatch "\.(?i:php|phtml|php[0-9]|phar|inc|shtml)$">
        Require all denied
    </FilesMatch>
</Directory>

Options -Includes switches off Server Side Includes, which is the second route described in section 5.8: an .shtml file that Apache parses and acts on without PHP being involved. Joomla never uses SSI, so you lose nothing by turning it off.

AllowOverride None is the line that does the real work, and it is worth knowing why. SetHandler, AddHandler, and AddType all belong to Apache's FileInfo override class, so an .htaccess file can only change which handler runs a file when AllowOverride includes FileInfo. Shared hosting almost always ships AllowOverride All, which includes it. Switch it off for the folders that exist only to hold uploads, and the attacker's block becomes a comment. Because the setting is inherited, it covers the subfolders as well.

Note that AllowOverride None also disables your own .htaccess in that folder, which is why the FilesMatch rule moves into the same <Directory> block. Keep the folder-level .htaccess in place anyway as a second layer, for the day the site moves to a host that has not applied this.

If you cannot edit the server configuration yourself, ask your host for it. It is a small change on their side, and it is the difference between a defence you control and a defence your attacker can edit.

8.2 Test Before You Flip the Switch

Go through a short checklist before the site is public again:

  • The homepage and the main templates render correctly, logged out.
  • Test with a search-engine referrer and a mobile user agent (section 5.7). No unexpected redirect.
  • View the page source of a few pages and search for script and iframe tags you do not recognise.
  • Forms work, and outgoing mail arrives. Check the mail queue too: a queue full of messages you did not send means something is still sending.
  • Scan the site with an external malware scanner, and let a second one confirm it. Treat both as detection tools, not as proof: signature scanners routinely miss custom or freshly modified malware, so a clean report is encouraging, not conclusive.
  • No unknown Super Users, no unknown plugins, no unknown scheduled tasks.
  • A test PHP file in images/ returns 403 and does not execute (section 8.1).

8.3 Reopen and Watch

Take the site out of offline mode and then keep watching for at least a month. Reinfection almost always happens quickly, because the attacker has automated scripts that check their old victims.

Set up the watch so it does not depend on you remembering:

  • Enable the User Actions Log, now with ip_logging and API logging switched on.
  • Enable the System - Log plugin with log_username, and read error.php weekly.
  • Enable the Task - Update Notification plugin with the Task Scheduler, so you hear about updates by email.
  • Connect Google Search Console and turn on its email alerts.
  • Add an external uptime and content monitor that alerts you when the homepage changes unexpectedly.
  • Take a fresh backup now - this one is your new clean reference point - and keep multiple generations from here on.

8.4 If It Comes Back

Reinfection within days means one of three things, in order of likelihood: you missed a backdoor, the original vulnerability is still open, or the compromise is not in your site at all but in the server, the control panel, or the computer you administer the site from.

Do not simply repeat the same clean-up. Go back to the timeline, and this time include the layers outside Joomla: scan the administrator's own workstation for information stealers, check for unknown SSH keys, and ask your host to look at the account from their side.

Back to top

9. Tell the People Who Need to Know

9.1 Users and Customers

If accounts existed on the site, tell the account holders. Say what happened, what data was involved, what you have done, and what you advise them to do. Which in practice means: change this password, and change it anywhere else you reused it.

Plain, early, factual communication costs you far less reputation than a discovered cover-up. People forgive an incident. They do not forgive being kept in the dark about their own data.

If personal data may have been accessed, this is not only a technical incident. Under the GDPR, a personal data breach must be reported to your supervisory authority without undue delay and, where feasible, within 72 hours of becoming aware of it. When the breach is likely to result in a high risk to the people involved, you also have to inform them directly.

In the Netherlands the supervisory authority is the Autoriteit Persoonsgegevens; every EU country has an equivalent. Note the trigger: the clock starts when you become aware of the breach, not when you finish the investigation. If you are unsure whether personal data was reachable, get advice quickly rather than waiting for certainty.

Practical consequence for the phases above: your evidence copy from section 2.2 and your timeline from section 4.5 are exactly the documentation this process asks for. That is a second good reason not to skip them.

Depending on the country and the seriousness, filing a police report may also be an option or an expectation. It rarely leads to anyone being caught, but it creates an official record, and insurers and business customers sometimes require one. Ask your supervisory authority or your legal advisor what applies to your case.

9.3 Payment Data

If the site handled card data in any form, contact your payment provider immediately and follow their instructions. Do not investigate this part alone; the contractual obligations here are strict and specific.

9.4 Search Engines

Once the site is genuinely clean, ask for a review. In Google Search Console, open the Security Issues report and request a review; do this only after the clean-up, because a failed review costs you more waiting time. Check the Pages report for indexed spam URLs and let them return 404 or 410 so they drop out of the index.

Recovery of rankings takes weeks, sometimes months. That delay, not the clean-up itself, is usually the most expensive part of a hack.

9.5 The Joomla Security Strike Team

There is one more report to consider, and it is the one many people never think of. If your investigation suggests the attacker used Joomla core rather than an extension, a weak password, or your own custom code, report it to the Joomla Security Strike Team (JSST). They are the volunteers who handle vulnerability reports for the CMS, and they are the reason core security releases exist.

Report it through the official channel described on Joomla's security vulnerability reporting page, by email or through the JSST contact form, rather than in a forum thread, a chat channel, or a social post. An unpatched core issue discussed in public is a gift to every other attacker, and it puts every Joomla site at risk until a release is out. The team commits to acknowledging a report within 24 hours, responding further within 7 days, and resolving within 21 days or explaining why not.

What makes a report useful is exactly the material you already collected: the timeline from section 4.5, the relevant access log lines showing the requests that worked, the list of files that changed, and the versions you were running. Offer them access to the preserved copy if they ask. You do not have to prove the vulnerability yourself or name it correctly - describe what you observed and let them analyse it.

This is worth doing even though it costs you time in the middle of a bad week. If core really was the way in, you are almost certainly not the only site affected, and your evidence may be what turns an unexplained pattern of hacked sites into a patch that protects everyone else. That is not a hypothetical: the material I handed the JSST after my own site was hacked (section 4.6) became CVE-2022-27912 and a core security release three weeks later.

9.6 Close the Loop

Finally, go back to everyone who contacted you about the incident and tell them it is resolved: your hosting provider, the security organisation or registry that reported it (section 2.3), and anyone who filed an abuse complaint. Say what you found, what you did, and that the site has been rebuilt or cleaned.

Do not skip this because the hard work is over. A pending abuse report does not close by itself, and an unanswered one can still cost you the account or the domain weeks after the site is perfectly clean. It is also the cheapest reputation you will ever buy: hosts and registries deal mostly with people who ignore them, and they remember the ones who reported back.

Back to top

10. Under the Hood (Developer View)

10.1 Where an Attacker Hides in the Database

TableWhat to check after a breach
#__users New accounts, changed email addresses, block flags flipped, unusual registerDate values.
#__user_usergroup_map Existing accounts silently added to the Super Users group.
#__user_keys Remember-me tokens. Truncate them all.
#__user_profiles API tokens live here under the joomlatoken key prefix. Remove tokens you did not issue.
#__user_mfa MFA methods. An attacker who enrolled their own device keeps access after a password reset.
#__session Active sessions. Truncate.
#__extensions Rogue plugin rows, and enabled flags changed on security-relevant plugins.
#__scheduler_tasks Tasks nobody created.
#__modules Custom modules containing scripts, published on all pages.
#__template_styles Custom-code parameters injected into every page.
#__action_logs Your evidence. Export it before anything prunes it.
#__update_sites A changed update URL turns your update system into a delivery channel.

That last row is worth a second look. If #__update_sites points somewhere that is not the official Joomla or developer URL, every future "update" you install comes from the attacker.

10.2 A Practical Triage Toolkit

Four commands that answer most first questions:

# 1. What changed recently?
find . -type f -newermt "2026-07-01" ! -path "./cache/*" ! -path "./tmp/*"

# 2. PHP where PHP does not belong
find images tmp cache media -type f -name "*.ph*"

# 3. Obfuscation markers
grep -rn --include="*.php" -E "eval\s*\(|base64_decode|gzinflate|\\\$GLOBALS\[" . | head -50

# 4. Files that are writable by everyone
find . -type f -perm -o+w

A compromised site does not only receive traffic, it also makes connections: sending spam, fetching extra payloads, reporting to a command-and-control host, or mining. If you have shell access while the incident is live, look at what the server is doing right now:

ss -plant          # listening sockets and active connections
ps auxf            # process tree, including anything spawned by PHP
lsof -nP           # open files and sockets per process
mailq              # queued mail waiting to go out

Collect the output before you kill anything. A running process is evidence that disappears the moment you stop it, and the connection it holds open often identifies the infrastructure behind the attack faster than any file on disk.

If the site is in version control, you have a far better tool than any of these:

git status --porcelain
git diff --stat

Every unexpected line is a modified or added file, with no guessing involved. This is the strongest practical argument for keeping even small Joomla sites in Git: after an incident, "what changed" becomes a one-second question instead of a two-hour investigation.

10.3 Why the Site Secret Matters, Precisely

Joomla builds the session cookie name with md5($secret . $session_name), where $session_name falls back to the application class name. The API token plugins verify tokens with hash_hmac($algorithm, $rawToken, $siteSecret). So a single change to $secret ends all sessions and invalidates all API tokens at once, which is exactly what you want in the first minutes of an incident.

It is worth being precise about the limits, so nobody relies on it too much: passwords are bcrypt hashes and are unaffected, enrolled MFA methods in #__user_mfa keep working, and remember-me tokens in #__user_keys are stored per user agent and need their own clean-up.

10.4 Verifying a Cleaned Site

Do not trust your own eyes on this. Build a checksum list from a clean Joomla of the same version and compare:

cd /path/to/clean-joomla && find . -type f -name "*.php" \
  -exec sha256sum {} \; | sort -k2 > /tmp/clean.sha256

cd /path/to/site && find . -type f -name "*.php" \
  -exec sha256sum {} \; | sort -k2 > /tmp/site.sha256

diff /tmp/clean.sha256 /tmp/site.sha256

Everything that differs is either a legitimate customisation you can name, or something to explain. "I do not know what that file is" is not a passing result.

Two limits are worth knowing if you turn this into ongoing monitoring. A hash list only catches changed files, and most malware simply adds new ones, so compare the file lists as well as the hashes. And keep the reference data somewhere the web account cannot write - another server, or storage your PHP user has no access to. A checksum file that sits next to the site is a checksum file the attacker can regenerate.

Back to top

11. The Web Services API After a Breach

The Joomla API is easy to forget during a clean-up, and it is a complete second front door into the same site.

  • Revoke every token. Changing $secret does this in one move, because tokens are HMAC-verified against it. Alternatively, reset the token per user on their profile.
  • Check who may use tokens. The User - Joomla API Token plugin has an allowedUserGroups parameter. If it was widened beyond Super Users, verify whether you widened it.
  • Check the authentication plugins. If API Authentication - Basic is enabled but you only ever used tokens, ask yourself who enabled it.
  • Enable API logging in the User Actions Log options (loggable_api), so the next incident leaves a trail here too.
  • Disable what you do not use. If the site has no headless consumer, switch off the API plugins entirely.

Testing that the old token is really dead takes one command:

curl -i -H "X-Joomla-Token: <old-token>" \
     https://example.test/api/index.php/v1/users

A 401 is the answer you want.

Back to top

12. SEO and Metadata

A hack is a search-engine problem as much as a technical one, and the damage often outlives the infection by months.

What typically happens: the attacker publishes hundreds of spam pages that get indexed under your domain, injects hidden links into existing articles, and serves different content to the Googlebot than to you. Google detects it, flags the site in Search Console, and may show a warning in the results. Rankings that took years to build drop within days.

The recovery order matters:

  1. Clean the site properly first. A review request on a site that is still infected fails and costs you another waiting period.
  2. Make the spam URLs return 404 or, better, 410 Gone. Do not redirect them to your homepage; that just tells Google the spam page moved.
  3. Request a review under Security Issues in Search Console.
  4. Check whether your domain landed on any mail or malware blocklists, and follow each delisting procedure.
  5. Watch the indexed page count for a few weeks until the spam URLs are gone.

There is a preventive angle here as well. Search Console alerts are free and often reach you before your visitors do. A site that nobody monitors can serve spam for months before anyone notices, and the longer that lasts, the longer the recovery takes.

Back to top

13. Common Mistakes and Pitfalls

13.1 Deleting the Malware Immediately

Symptom: you find a strange PHP file, delete it in relief, and now have no idea when it arrived or what created it.

Fix: copy the whole site and database first (section 2.2). The timestamps on those files are the thread that leads to the entry point. Investigate, then delete.

13.2 Restoring a Backup and Calling It Done

Symptom: the site is restored, looks fine, and is reinfected within a week.

Fix: a restore rolls back the symptoms and the fix at the same time - you are back on the same vulnerable version. Restore, then immediately update everything and change all credentials before the site goes public again.

13.3 Restoring On Top of the Infected Files

Symptom: the backup is genuinely clean, the restore succeeds, and the backdoor is still there.

Fix: a restore only overwrites the files that are in the archive. A webshell that was never in your backup survives untouched. Empty the web root before restoring.

13.4 Cleaning the Files, Ignoring the Database

Symptom: all files verify against a clean Joomla, but visitors still get redirected.

Fix: check the database (section 5.3). A published custom module with one <script> tag reproduces the whole symptom with a perfectly clean file system.

13.5 Missing the Second Backdoor

Symptom: the site is cleaned carefully and is back within days.

Fix: assume there is always more than one. Check plugins, scheduled tasks, server cron, .htaccess, and auto_prepend_file before you declare victory, not after.

13.6 Reusing the Old Extension Files

Symptom: during a rebuild, the extensions are copied over from the old site "to save time".

Fix: download every extension fresh from the developer. Copying extension folders forward is copying the unknown forward, and it defeats the entire point of a rebuild.

13.7 Assuming the Site Was the Way In

Symptom: Joomla is fully patched, everything is clean, and files keep changing.

Fix: widen the search. Stolen FTP credentials from an infected workstation, a compromised control panel, a leaked SSH key, or a neighbour account on a badly isolated shared server all produce exactly this picture. Involve your host.

13.8 Not Telling Anyone

Symptom: user data was in the database, and the incident is quietly cleaned up and never mentioned.

Fix: if personal data was reachable, notification is a legal obligation with a 72-hour clock, not a matter of preference (section 9.2). Handle it in parallel with the technical work.

Back to top

14. Best Practices

If you remember only a few things from this article, remember these:

  • Contain first, investigate second, clean third. Never reverse that order.
  • Copy the infected site and database before you change anything. It is your evidence and your safety net.
  • Find the entry point. A clean-up without it is a delay, not a repair. Ask what you switched on while troubleshooting and never switched off.
  • Check your own account's login times, not just the list of unfamiliar users. A login you cannot account for means they did not need your password.
  • Change every credential, and change the $secret in configuration.php to end all sessions and API tokens at once.
  • Empty the web root before restoring or rebuilding. Never clean around unknown files.
  • Reinstall extensions and templates from their original sources, never from the hacked site.
  • Review the database as carefully as the files. Custom modules and template parameters hide injections well.
  • Look for the second and third backdoor: plugins, scheduled tasks, server cron, .htaccess, auto_prepend_file.
  • Block PHP execution in upload folders, and block it from the server configuration with AllowOverride None, not only from an .htaccess file the attacker can rewrite. Joomla does not do this for you, and it disables most webshells.
  • Block PHP execution in upload folders. Joomla does not do this for you, and it disables most webshells.
  • Enforce MFA and update everything before the site goes public again.
  • Rotate the credentials a second time once the clean site exists. The first round happened while the backdoor was watching.
  • Watch the site closely for a month, and take a fresh backup as your new clean reference.
  • Report a personal data breach within 72 hours. Tell your users plainly.
  • If Joomla core was the way in, report it to the JSST instead of posting it publicly.
  • Close the loop with your host and whoever reported the hack. An open abuse report can still cost you the account.
  • Afterwards, fix the cause: the missing update, the weak password, the extension nobody maintains.
Back to top

15. Quick Reference

PHASE 1  CONTAIN
         block at server level (not only Joomla offline mode)
         with SSH: mv public_html public_html.hacked ; chmod 700
                   + static index.html  (contains AND preserves)
         Send Mail (mailonline) = No, if the site is sending spam
         several sites in one account? treat them all as infected
         notify your host, ask for raw access logs
         reported by Netcraft/SIDN/your host? reply at once, there
         is a deadline and the domain itself can be suspended

PHASE 2  PRESERVE
         full file copy + database dump, timestamps intact
         store off-server, label as INFECTED, never restore it

PHASE 3  INVESTIGATE
         Users > User Actions Log        (export CSV; ip_logging is OFF by default)
         check YOUR OWN login times, not just unknown accounts
         administrator/logs/error.php    (System - Log, failed logins)
         server access logs              (POST to images/ tmp/ media/)
         grep Accepted /var/log/auth.log (did they reach the server?)
         find . -name "*.php" -mtime -14
         grep -rl "<?php" images/ tmp/ cache/ media/   (payload may be .json)
         find . -name ".htaccess"        (clean Joomla 6 core ships exactly 2)
         build a timeline → earliest unexplained event = break-in
         ask: what did I switch on and never switch off?
         expect more than one attacker once the URL is shared

PHASE 4  ERADICATE
         credentials: Joomla, hosting, FTP/SSH, database, SMTP, API,
                      DNS/CDN, backup storage, deployment tokens
         configuration.php readable? assume every secret in it leaked
         configuration.php: new $secret  → kills sessions + API tokens
         TRUNCATE #__session ; TRUNCATE #__user_keys
         users: check #__user_usergroup_map for new Super Users
         files: diff against a clean Joomla of the same version
                System > Update > Joomla > Reinstall Joomla core files
                (overwrites core files, does NOT delete foreign files)
         db:    #__content #__modules #__template_styles  (script/iframe)
                #__extensions #__scheduler_tasks #__update_sites
         other: .htaccess, .user.ini auto_prepend_file, crontab -l

PHASE 5  RECOVER
         empty the web root, fresh Joomla, fresh extensions
         images back with an extension allowlist, no PHP
         deny PHP in upload folders (.htaccess / nginx location)
         AllowOverride None on upload dirs, in the vhost, not .htaccess
                (stops the attacker re-enabling PHP with SetHandler)
         Require Password Reset for all users; enforce MFA
         update everything; test with mobile UA + search referrer
         rotate every credential AGAIN on the clean environment

PHASE 6  REPORT
         users: what happened, what data, what to do
         GDPR: supervisory authority within 72 hours
         payment provider if card data was involved
         Joomla core the way in? report to the JSST, not in public
         close the loop: host, registry, whoever reported it
         Search Console > Security Issues > request review
         spam URLs return 404/410, never redirect to home

AFTER    ip_logging ON, loggable_api ON, log_username ON
         Task - Update Notification + Task Scheduler
         fresh backup = new clean reference, keep generations
Back to top

16. Summary

A hacked Joomla site is a bad day, not a lost site, provided you work in the right order:

  • Contain before you clean. Block at the server level, and copy the infected state before you change a byte.
  • Find the entry point. The action log, the failed-login log, the server access log, and file timestamps together give you a timeline, and the timeline tells you which backup is still clean.
  • Lock the attacker out. Rotate every credential, change the site secret, and empty #__session and #__user_keys.
  • Look for everything they left. Modified core files, webshells in upload folders, injected articles and custom modules, rogue plugins, scheduled tasks, .htaccess redirects, and auto_prepend_file.
  • Prefer a rebuild over a clean-up when the timeline is unclear. Empty web root, fresh Joomla, fresh extensions, reviewed database.
  • Come back carefully. Update everything, enforce MFA, test as a stranger would, and watch the logs for a month.
  • Report what the law and decency require. Users, the supervisory authority within 72 hours if personal data was involved, the JSST if Joomla core was the way in, and a review request to the search engines once you are genuinely clean. Then close the loop with your host and whoever reported it.

The most useful thing to take away is that almost none of this is exotic work. It is patient, ordered checking: one list of accounts, one list of files, one list of database tables, and the discipline not to skip the investigation because the clean-up feels more productive.

If you are in the middle of an incident right now and the timeline will not come together (the logs are gone, the backups are all too recent, or the site keeps coming back) that is the point where a second pair of experienced eyes usually saves more time than it costs. Recovering a compromised Joomla site methodically, and finding the door that everyone else walked past, is exactly the kind of work a Joomla specialist does often enough to recognise the patterns quickly.

Back to top
Joomla Website Hacked: What To Do, Step by Step
Peter Martin
Peter Martin
Joomla Specialist

Peter is a Joomla specialist and a Linux admin for fast, secure and scalable websites.