WordPress Hidden Link Spam: How I Discovered My Site Was Compromised
The first time I suspected something was wrong with www.sandeepaade.com, nothing looked wrong at all.
I opened the site in a browser. The resume theme rendered correctly. No pop-ups, no redirects, no defacement. If I had stopped there — as most site owners do — I would have concluded the server was healthy.
That conclusion would have been wrong.
This is Part 1 of a four-part series on a real security incident on my CloudPanel/Ubuntu server. Here I focus on discovery: how invisible SEO spam hid in plain sight, why Varnish still served poisoned HTML after the origin was clean, and why Google’s index told a worse story than my homepage did.
The site looks fine because the compromise is meant to be invisible.
The Illusion of a Healthy Site
WordPress compromises do not always announce themselves. Attackers who inject hidden links for search-engine manipulation have a specific goal: pass relevance signals to third-party domains without alerting the site owner. The visual experience stays unchanged; machines read something else entirely.
On my site, a must-use plugin (Part 2 covers the mechanics) appended hundreds of anchor tags to every page footer. Each link used CSS designed to hide content from human eyes:
<a href="https://example-gambling-site.example/" style="position:absolute;left:-9999px;opacity:0;font-size:1px;">keyword phrase</a>
The left:-9999px technique pushes elements off-screen. Browsers still parse the links. Search crawlers still follow them. Regular visitors never see them unless they view page source or run automated checks.
What Triggered the Investigation
I manage my own infrastructure — CloudPanel on Ubuntu 22.04 on an Azure VM — and I periodically review hosting posture. The investigation did not begin with a user complaint or a Google penalty email. It began with systematic checks: log review, integrity signals, and a decision to fetch the public homepage as raw HTML rather than trusting rendered output.
Discovery moment: A browser shows the DOM after JavaScript, caching, and rendering. A curl fetch shows what the edge and origin actually return. For security work, the raw response is the source of truth.
The First Red Flag: 538 Hidden Links
When I fetched the public homepage over HTTPS, the HTTP status was 200 and the page weighed roughly 97 KB — noticeably larger than a clean WordPress resume site should be. Scanning the HTML for the left:-9999px pattern in anchor tags returned 538 matches.
538
Hidden spam links
Public HTTPS fetch (through Varnish)
0
After origin cleanup
Localhost bypass (no cache)
97 KB → 34 KB
Page size drop
Poisoned cache vs clean origin
The injected links pointed at domains I did not control and had never linked to intentionally. Samples from the cached response included gambling and SEO-spam destinations such as:
betsmove.turkeygiris.pro— anchor text: “betsmove”, “betsmove giriş”, “betsmove güncel”missionnepal.org— anchor text: “celtabet”, “celtabet giriş”yakuzagaragenyc.com— anchor text: “galabet”- Additional Turkish gambling and betting keywords tied to brands like jeetbuzz
None of these topics relate to my resume site. None appeared in the WordPress admin, the post editor, or the theme customizer. A later database scan matched zero campaign rows in wp_options, wp_posts, or wp_postmeta. The spam was injected at the PHP/filesystem layer — not stored as content I could delete from the dashboard.
538 hidden links is not a typo or a stray plugin bug. It is a deliberate SEO spam campaign.
What Google Had Indexed — and Still Shows
The HTML fetch confirmed what crawlers could see on my homepage at that moment. The next question was scarier: what had search engines already stored?
I ran checks any site owner can run without shell access:
site:sandeepaade.com
site:sandeepaade.com betsmove
site:sandeepaade.com jeetbuzz
site:sandeepaade.com celtabet OR galabet
This class of campaign does two things at once: it hides outbound links on every page footer and it often publishes junk URLs under your domain — German classified ads, gambling landing pages, app-download spam — pushed through fake sitemaps and rewrite rules. The goal is SEO poisoning: borrow your domain’s trust, rank someone else’s keywords, disappear when you notice.
When I checked again on 2026-05-27, five days after quarantine-based cleanup, the picture was mixed:
Live site: The public homepage returned ~34 KB of clean HTML. Zero left:-9999px spam anchors. Zero betsmove, celtabet, galabet, or jeetbuzz strings. Sample URLs that search engines still listed — pages titled jeetbuzz.cc, Großer Esstisch für 8 Person, and gta+game+download … Fast withdrawal — now returned HTTP 404. The origin was clean; those paths were gone.
Search index: Not clean. A site:sandeepaade.com review still surfaced dozens of indexed URLs that have nothing to do with my resume — antique lamps, e-bikes, Turkish evening dresses, Bengali “message app” pages, and gambling-adjacent titles sitting beside legitimate pages like /experience, /key-projects, and /educational-journey.
site: search still poisoned — Cleanup on the server does not instantly erase Google’s memory. Stale SERP entries can linger for weeks or months after the HTML is fixed.
Practical follow-ups I am acting on:
- Google Search Console — confirm coverage, see which spam URLs Google still knows about, request re-indexing of clean pages after remediation
- Fake sitemaps — my
robots.txtstill listed suspicious?sitemapNNN.xmlURLs (not normal WordPress sitemap paths); another index-poisoning artifact worth auditing - Re-crawl — after confirming the origin is clean, use GSC URL Inspection on the homepage and key pages, submit an updated sitemap, and purge CDN/Varnish/Cloudflare caches so the next Googlebot fetch sees the same clean HTML I see from
curl
The hidden links on the homepage were the smoking gun in real time. The search index is the slow-burn damage — proof the compromise had been working long enough to leave footprints in public search results, even after I stripped the malware from disk.
Varnish Cache vs Clean Origin
At this point I had a puzzle: the public fetch showed spam, but interactive browsing looked fine. Two mechanisms explained the gap.
Off-screen injection does not change the visible page. Hidden-link attacks alter what machines read, not what you see.
Cache vs origin: When I fetched the homepage from the origin directly (bypassing Varnish on localhost), the hidden-link count dropped to zero. The live origin was already clean — or had been cleaned in an earlier partial step — while the public URL still served a stale cached copy with all 538 links.
| Fetch target | Hidden spam anchors | Page size |
|---|---|---|
| Public HTTPS (through Varnish) | 538 | ~97 KB |
| Origin localhost (bypass cache) | 0 | ~34 KB |
| Public HTTPS (after Varnish ban) | 0 | ~34 KB |
After a Varnish ban for www.sandeepaade.com, the public URL matched the clean origin. That sequence taught me a lesson I return to in Part 3: a clean origin does not automatically mean a clean public site. Edge caches can continue serving compromised HTML until explicitly purged.
Public Checks Anyone Can Run
You do not need shell access for a first-pass hidden-link check.
Fetch raw HTML:
curl -sL "https://www.example.com/" -o homepage.html
grep -c 'left:-9999px' homepage.html
A count greater than zero on a site that does not intentionally use off-screen elements warrants investigation. WordPress core CSS legitimately contains left:-9999px in minified stylesheets, but those are <style> blocks — not hundreds of <a href="..."> tags in the body footer.
Compare origin vs public URL if you have server access. A mismatch strongly suggests cache poisoning or stale cache — visitors and crawlers may still see old content even when the filesystem is clean.
Search for campaign strings — later filesystem analysis found markers tied to the root-seo.com campaign: rs_links, .rs_links.json, and related files. At discovery time, the HTML alone was enough to confirm SEO spam injection.
Run site:yourdomain.com after any suspected SEO spam. A clean homepage can coexist with a poisoned index for months.
What Looked Normal vs What Mattered
| Check | What I saw | Verdict |
|---|---|---|
| Browser visit | Normal resume site | Misleading |
| WordPress admin | No spam in posts/pages | Misleading — DB was clean |
| Public HTML fetch | 538 hidden <a> tags |
Definitive spam signal |
| Origin fetch (localhost) | 0 hidden links | Origin clean; cache stale |
site:sandeepaade.com (post-cleanup) |
Dozens of junk URLs listed; live fetches 404 | Stale index — work not finished |
| Legitimate pages in index | /experience, /key-projects, etc. |
Good — but mixed with spam |
Search engines and SEO audit bots behave more like curl than like a casual browser session. That is exactly what the attackers optimize for.
What I Did Immediately After Discovery
I did not panic-delete files. Destructive cleanup without evidence collection makes forensics harder and can trigger persistence mechanisms to restore payloads. My immediate actions were:
- Preserve evidence — capture HTML snapshots, note response sizes, record timestamps
- Avoid public signals — no social posts or DNS changes that would tip off the attacker before I understood scope
- Scope the compromise — determine whether spam was cache-only, filesystem-only, or deeper (it went deeper)
- Plan quarantine-based remediation — move, do not delete; backup first
Part 2 explains what deeper investigation revealed. Part 3 walks through remediation.
Takeaways for Site Owners
If you manage WordPress on a VPS or panel-based host, treat “the site loads fine” as necessary but not sufficient:
- Fetch your homepage as raw HTML periodically
- Compare cache vs origin if you run Varnish, nginx cache, or Cloudflare
- Run
site:yourdomain.comafter any suspected SEO spam — stale indexed junk can outlive a clean homepage - Investigate size anomalies — a homepage doubling in byte size without content changes is a signal
- Do not assume the database is the source of truth — filesystem-only persistence is common in modern SEO spam kits
Hidden-link injection will not encrypt your files or flash a skull on the homepage. It will quietly trade your domain’s reputation for someone else’s ranking benefit until you look in the right place.
I looked. I found 538 reasons to start an incident response — and dozens more still sitting in Google’s index.
Next in this series
Part 2 — What the Attackers Left Behind
Coming soon — filesystem persistence, must-use plugin mechanics, and the root-seo.com campaign artifacts.