SEO Spam Injection With Cloaking: How Your Site Serves Porn to Google and Nothing to You
Baljeet Dogra
You open Google. You search for your own brand. And there, under your domain name, is a page advertising leaked adult content. You click it. Your normal homepage loads. You check your CMS, files, and database. Nothing. The page does not exist. Google says it does. Google is not wrong. This is SEO spam injection with cloaking — and increasingly, the spam is not even coming from your server.
Part 1: Why cloaking exists
Cloaking means serving different content to different visitors based on who they appear to be. It is not new — it has been a black-hat SEO technique since the late nineties. What has changed is what it is used for.
An attacker who has injected spam into your site has a problem. They want two things that are in tension:
- Google must index the spam. Without indexing, there is no traffic, and without traffic there is no money.
- You must not notice. The moment you notice, you clean up, and the revenue stops.
Cloaking resolves the tension. The injected code inspects each incoming request and makes a routing decision:
| Request signature | What gets served |
|---|---|
User-agent contains Googlebot, bingbot, etc. | Full spam page, keyword-stuffed, internally linked to more spam |
Referer header is a search engine | 302 redirect to the monetised destination |
| Normal browser, direct navigation, no referer | Your real site, or a clean 404 |
| Request from a known security-scanner IP range | Your real site |
| Repeat visitor (cookie set) | Your real site |
The site owner types the URL into their browser. Direct navigation, no search referer. They get bucket three. They see nothing wrong and conclude Google has made a mistake.
More sophisticated kits add IP-based verification: they do a reverse DNS lookup on the requesting IP and only serve spam if it genuinely resolves into Google's netblocks. This defeats the naive detection method of simply spoofing a Googlebot user-agent with curl, because the kit knows a request from a residential UK broadband IP claiming to be Googlebot is a liar. If you spoof the user-agent and see nothing, that is not an all-clear.
What the spam is actually selling
Understanding the monetisation tells you how urgently to treat the incident, and it varies enormously.
Backlink farming and parasite SEO
The lowest tier. Your domain's authority is borrowed to rank spam pages that link out to a client site. Annoying, damaging to your rankings, not dangerous to your visitors.
Affiliate funnels
Adult tube sites, gambling, pharmaceuticals, replica goods. The spam page redirects to an affiliate link and the attacker earns per signup. Moderately harmful — your visitors land somewhere seedy.
Direct carrier billing (DCB) fraud
The serious tier, and increasingly the dominant one. The visitor is routed through a chain of redirects to a mobile subscription page. On some networks, particularly in markets with lax DCB controls, a single tap subscribes the user to a premium service charged directly to their phone bill. The victim finds out when the bill arrives. The attacker earns a commission per conversion.
You can often identify this from the URL parameters alone. Query strings containing transaction_id, session_id, operator, gateway, msisdn, or error messages like request_not_from_operator_gateway_ip are carrier-billing infrastructure. If you see those on your domain, you are not dealing with an SEO nuisance. You are the front door of a fraud operation.
Malware and phishing distribution
The most dangerous, thankfully the least common in this particular pattern, because malware gets a site blocklisted quickly and blocklisted sites stop earning.
The traffic distribution system
Mature spam operations do not link directly from bait page to money page. They run a TDS — traffic distribution system — an intermediate layer that fingerprints each visitor and routes them accordingly.
You will spot a TDS in your logs as paths like /go/, /out/, /forwarder/, /redir/, /click/, usually carrying a session identifier and a destination parameter. The TDS decides, per visitor, whether to send them to the carrier-billing page, an affiliate offer, or a harmless decoy — based on their country, carrier, device, and whether they look like a researcher.
This is why the same spam URL can behave completely differently for you and for a visitor in Jakarta. It is also why "I clicked it and nothing happened" proves nothing at all.
Part 2: The five architectures
"SEO spam injection" describes a symptom, not a mechanism. Five distinct mechanisms produce near-identical symptoms, and they have almost nothing in common in terms of remediation. Misidentifying which one you have is the single most common reason cleanups fail.
Architecture 1: Server-side file injection
The classic. Malicious PHP (or similar) is written to your document root. This is the WordPress, Joomla, Magento case.
Typical hiding places:wp-content/mu-plugins/— must-use plugins load automatically and do not appear in the plugin admin list- Appended to
index.php,wp-config.php, or a theme'sfunctions.php - Standalone
.phpfiles insidewp-content/uploads/, which should never contain executable code - A nested
.htaccesswith rewrite rules conditional onHTTP_USER_AGENT
The code is usually obfuscated with base64_decode, gzinflate, eval, str_rot13, or long hex-escaped strings.
Entry vector: vulnerable or nulled plugin, outdated core, weak admin password, or a compromised neighbouring account on shared hosting.
Architecture 2: Database injection
No new files. Spam content is written into your database — post tables, options tables, widget content — and rendered by your own legitimate templates. File integrity checks pass perfectly. The site is serving spam using entirely genuine code.
Detection: search the database for the spam keywords directly, and audit for post types or taxonomy terms you did not create.
Architecture 3: Edge and configuration injection
The modern variant, and the one that catches out people on Jamstack and static hosting who assume "no server, no server-side vulnerability."
There is no PHP on a Next.js site deployed to Netlify or Vercel. There is still plenty of request-time logic, and all of it can cloak:
middleware.ts— Next.js middleware executes on every request at the edge. It can read the user-agent and rewrite to an external host. Ten lines, no trace in your page components.next.config.jsrewrites —async rewrites()supports ahasarray matching on headers. A rewrite conditional onuser-agentmatchingGooglebot, with an externaldestination, is a complete cloaking implementation in configuration alone._redirects/netlify.toml— a proxy rule such as/* https://attacker.example/:splat 200serves remote content under your domain with a 200 status. Not a redirect; a proxy. Google sees your domain serving the content.- Edge Functions / Middleware — arbitrary code at the CDN layer, invisible to any scan of your repository.
Entry vector: compromised Git account, a stolen CI/CD token allowing direct API deploys, a malicious npm dependency modifying config at build time, or a compromised team member's session.
The crucial investigative advantage here: platform deploys are immutable and logged. Your deploy history will tell you exactly which commit produced the running build and who triggered it. If a deploy exists that does not correspond to any commit in your repository, it came in through the API with a stolen token.
Architecture 4: Reverse proxy interception
Something sits in front of your origin and rewrites responses. Your origin is pristine. Your files are perfect. Every check you run against the server passes.
- A malicious Cloudflare Worker or Page Rule, if the attacker has your CDN account
- A compromised nginx or Apache reverse proxy in front of the application
- A rogue service worker (client-side only, but produces convincing symptoms)
Tell: the origin serves clean content when accessed directly by IP or by its platform-assigned hostname, but the public hostname serves spam.
Architecture 5: Hostname takeover — no compromise at all
This is the one that almost no article about SEO spam covers, and in 2026 it is common enough that it should be your first hypothesis on any modern hosting platform.
The mechanism:
- Your domain's apex A record points at a shared platform load balancer — Netlify, Vercel, GitHub Pages, Heroku, S3, Azure. That IP serves thousands of sites, and it routes by the
Hostheader. - The hostname is not claimed by your project on that platform — perhaps you only ever added
www.example.comand never the bare apex, or a subdomain's CNAME outlived the service it pointed to. - Anyone with an account on the same platform adds
example.comto their project. - The platform now routes requests for your hostname to their content.
Nothing was hacked. Your repository is untouched. Your credentials are safe. Your DNS is exactly as you configured it. And your domain is serving spam.
The signature that identifies this instantly: the spam exists only on http://.
This follows from certificate issuance. The attacker cannot obtain a valid TLS certificate for a domain they do not control, because domain validation would fail. So the takeover works over plain HTTP only. Your legitimate site continues to serve perfectly over HTTPS. If every spam URL Google has indexed carries an http:// prefix and not one carries https://, stop investigating your codebase — you have a hostname takeover.
This is the dangling-DNS class of vulnerability, and it applies to any CNAME pointing at a service you no longer use. Decommissioned subdomains are the classic case: old-app.example.com still CNAMEs to a Heroku app you deleted three years ago, someone else claims that app name, and they own a hostname on your domain.
Part 3: Detection
Work in this order. Each step eliminates architectures.
Step 1: Enumerate what Google actually has
Search operators first:
site:example.com
site:example.com onlyfans
site:example.com -inurl:https
Then Search Console, with a critical caveat: a URL-prefix property only covers that exact prefix. If your property is https://example.com/ and the spam is on http://, your Search Console will be completely clean while your domain burns. Add a Domain property (verified by DNS TXT), which covers every protocol and subdomain.
In the Domain property, check Performance → Pages for URLs you do not recognise, and Performance → Countries. Spam traffic clustering in markets where you have no audience — Indonesia, Egypt, Brazil, Thailand are typical for carrier-billing fraud — confirms both the diagnosis and the target market.
Then use URL Inspection → Test live URL → View crawled page on a specific spam URL. This is the single most valuable diagnostic available, because it fetches from Google's own infrastructure. It defeats IP-verification cloaking that would defeat your curl. If Google's live test returns spam and your browser returns a 404, cloaking is confirmed beyond argument.
Step 2: Compare responses across request signatures
# As a normal browser
curl -sS -A "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36" \
-o browser.html -w "%{http_code}\n" https://example.com/suspect-path
# As Googlebot
curl -sS -A "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)" \
-o googlebot.html -w "%{http_code}\n" https://example.com/suspect-path
# With a search referer
curl -sS -A "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X)" \
-H "Referer: https://www.google.com/" -L \
-o referred.html -w "%{http_code} %{url_effective}\n" https://example.com/suspect-path
diff browser.html googlebot.html
Any difference is cloaking. Test mobile user-agents specifically — carrier-billing funnels frequently serve spam only to mobile devices, since the monetisation requires a phone.
Bear in mind you are testing from one IP in one country. A negative result is weak evidence. Cross-check with a VPN or an external fetcher.
Step 3: Establish protocol asymmetry
curl -sSI http://example.com/suspect-path
curl -sSI https://example.com/suspect-path
Spam on 80, clean on 443, is the hostname-takeover signature. Confirm by inspecting the certificate:
openssl s_client -connect example.com:443 -servername example.com </dev/null 2>&1 \
| grep -E "subject=|issuer=|Verify return code"
Step 4: Establish who is answering
dig example.com A +short
dig example.com NS +short
dig example.com TXT +short
dig www.example.com CNAME +short
dig randomstring9x.example.com +short # any answer indicates wildcard DNS
whois example.com | grep -iE "updated|status"
You are checking three things. Do the A records point where you expect? Are the nameservers yours? Are there TXT records — verification challenges especially — that you did not create? A platform verification TXT record you did not add means someone passed an ownership check against your DNS, which escalates the incident considerably.
Then examine the response headers for platform fingerprints. On a takeover, the headers will show the platform serving the content while the content itself is not yours.
Step 5: Bypass DNS entirely
Request the origin directly by its platform-assigned hostname or by IP with an explicit Host header:
curl -sS -A "Mozilla/5.0 (compatible; Googlebot/2.1)" https://your-project.netlify.app/suspect-path
curl -sS --resolve example.com:443:ORIGIN_IP https://example.com/suspect-path
Spam present at the origin means it is in your deployment (architectures 1, 2 or 3). Origin clean but public hostname dirty means the problem is between DNS and origin (architectures 4 or 5).
This single test cleaves the problem space in half and should be done early.
Step 6: If it is in your deployment, find it
# Recently modified files
find . -type f \( -name "*.php" -o -name "*.js" -o -name "*.ts" \) -newermt "45 days ago" -ls
# Cloaking logic in config
grep -rniE "googlebot|bingbot|user-?agent|HTTP_REFERER|rewrites|:splat|edge_function" \
next.config.* middleware.* netlify.toml vercel.json public/_redirects .htaccess 2>/dev/null
# Obfuscation
grep -rniE "eval\(|base64_decode|gzinflate|str_rot13|\\\\x[0-9a-f]{2}" --include="*.php" .
# Against a known-good baseline
git status --porcelain
git log --oneline --since="60 days ago" --stat
wp core verify-checksums # WordPress
Also inspect your sitemaps. Attackers publish their own — often at unusual filenames — and submit them to get thousands of URLs indexed within days. And audit Search Console users and permissions: adding themselves as a verified owner is standard practice, and it lets them keep resubmitting spam after you have cleaned up.
Part 4: Remediation
If it is a hostname takeover
The fix is claiming the hostname, not cleaning any code.
- Add the exact hostname — apex,
www, and every subdomain with DNS pointing at the platform — to your project. Leave no hostname pointing at a shared platform edge unclaimed. That is the entire vulnerability. - If the platform refuses because another account holds it, open a support ticket and prove ownership. Most platforms use a TXT-record challenge for this. Frame it as an abuse case rather than a routine domain move, and attach evidence — indexed spam URLs, Search Console screenshots, the carrier-billing parameters if present. Live fraud gets triaged faster than domain admin.
- Force HTTPS and enable HSTS. This eliminates the HTTP surface that made the takeover indexable at all.
- Add a CAA record to restrict which certificate authorities may issue for your domain.
- Audit every DNS record for dangling CNAMEs. Every one is a future incident.
If it is a code or database compromise
Restore from a known-good backup predating the compromise, then patch. Do not clean file-by-file. These kits install multiple backdoors — a modified core file, an innocuous-looking uploads directory script, a database-stored payload, an extra admin user — and missing one means reinfection within days.
Then, in order:
- Rotate every credential: hosting panel, CMS admins, SFTP/SSH keys, database, API and CI tokens, registrar, CDN
- Enable MFA everywhere, particularly registrar and DNS provider
- Delete unrecognised admin users and API tokens
- Update core, plugins, themes, dependencies
- Remove any nulled or pirated software — it is the single most common entry vector, and it is designed to be
- Enable registrar transfer lock
Getting Google to forget
Once the site is genuinely clean:
- Serve
410 Gonefor the spam paths, not404. Google processes 410 as definitive and drops those URLs substantially faster. - Use the Removals tool in Search Console for immediate suppression while recrawling catches up. Removals last roughly six months — long enough to bridge the gap, not a substitute for the 410.
- Request a review under Security Issues or Manual Actions. Describe what happened, what you found, and what you fixed. Vague submissions get rejected; specific ones get approved.
- Fix internal linking — spam kits sprinkle links to their pages across your legitimate content.
- Wait. Recrawl is not instant. Adult-content classification and SafeSearch flags can lag several weeks behind the technical fix. Ranking recovery, in a bad case, takes months.
If carrier billing is involved
Do not treat this as purely a Google problem. The transaction_id values in those URLs are traceable. Identify the DCB aggregator from the destination page and report the campaign to their fraud team. They can terminate the payout account, which kills the operator's incentive far more effectively than de-indexing does. In the UK, the Phone-paid Services Authority regulates this space; equivalent regulators exist in most markets. Consumers are being defrauded under your brand, and that is a reporting obligation as much as a reputational one.
Part 5: Prevention
Claim every hostname
On any shared-edge platform, an unclaimed hostname pointing at that platform is an open door. Audit quarterly.
Kill dangling DNS
When you decommission a service, remove the DNS record in the same change. A CNAME outliving its target is a takeover waiting to happen.
Monitor as a bot, not as yourself
Every check you run as yourself is the check the attacker designed around. Schedule a job that fetches key URLs with a Googlebot user-agent and a search referer, and diffs the response against a baseline. This is the highest-value monitoring you can build for this threat class, and it is perhaps thirty lines of code.
Set up Search Console alerts on a Domain property
Not URL-prefix. Domain. And actually read the emails.
Watch for index bloat
A sudden jump in indexed pages is the earliest reliable signal. Alert on it.
Add a Content Security Policy
Use frame-ancestors and form-action directives to limit what injected content can do.
Lock down deployment
Rotate CI tokens on a schedule. Require MFA on the Git host and the deploy platform. Enable branch protection and required reviews. Audit collaborators quarterly — the ex-contractor with lingering repository access is a genuine risk, not a hypothetical one.
Pin dependencies and audit the supply chain
Build-time compromise is real, and it produces exactly these symptoms with a completely clean repository.
The one thing worth remembering
The instinct when you first see spam in your search results is to look at your website. That instinct is right about a third of the time.
The rest of the time the answer is somewhere in the path between a DNS query and your server — a hostname you never claimed, a CNAME you forgot to delete, a proxy rule in a config file, a token in a CI system. The content on your server is fine. It was never the point.
So before you spend a weekend grepping for base64_decode, spend five minutes on two questions: is the spam on HTTP only? and does it appear when I request the origin directly? Those two answers tell you which of the five architectures you are dealing with, and therefore whether you are cleaning code, restoring a backup, or filing a support ticket.
Diagnosis before remediation. Especially when the attack was built specifically to make diagnosis hard.
Dealing with SEO spam or a suspected takeover?
I help teams diagnose cloaking incidents, reclaim hostnames, and harden DNS and deploy pipelines so the same attack path does not reopen.