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.txt still listed suspicious ?sitemapNNN.xml URLs (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:

  1. Preserve evidence — capture HTML snapshots, note response sizes, record timestamps
  2. Avoid public signals — no social posts or DNS changes that would tip off the attacker before I understood scope
  3. Scope the compromise — determine whether spam was cache-only, filesystem-only, or deeper (it went deeper)
  4. 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.com after 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.

Leave a Reply

Your email address will not be published. Required fields are marked *