Dev.to Security πŸ” Cybersecurity πŸ‘ 0 πŸ“– 7 min read

How to Investigate WordPress ?p=, ?q=, and ?a= SEO Spam URLs

During a WordPress SEO spam investigation, I found Google requesting URLs like these: https://example.com/?p=23932 https://example.com/?q=98237492 https://example.com/?a=739284729 At first, they look similar: a

During a WordPress SEO spam investigation, I found Google requesting URLs like these:

https://example.com/?p=23932
https://example.com/?q=98237492
https://example.com/?a=739284729

At first, they look similar: a short query parameter followed by a large number.

But they should not all be treated the same way.

The most dangerous mistake would be to create a rule that blocks every single-letter query parameter.

That could break:

  • Real WordPress posts
  • Internal searches
  • Plugin filters
  • Tracking parameters
  • Form submissions
  • Application features

In a larger cleanup involving approximately 242,000 unwanted URL variations, I had to separate legitimate WordPress behaviour from malicious URL patterns.

The full incident is documented in my spam URL removal case study. This article focuses on query-string analysis.

What is a query string?

A query string is the part of a URL following the question mark.

Example:

https://example.com/?p=123

The query string is:

p=123

Multiple parameters can be separated with an ampersand:

https://example.com/?page=2&sort=price

Query strings are normal.

Their existence does not mean a website is hacked.

The issue begins when malware generates large numbers of meaningless or spam-producing variations.

Why ?p= can be legitimate

WordPress uses the p parameter to request a post by its database ID.

For example:

https://example.com/?p=123

If post ID 123 exists, WordPress may redirect the request to its normal permalink:

https://example.com/example-post/

Therefore, this rule would be unsafe:

RewriteCond %{QUERY_STRING} (^|&)p=[0-9]+(&|$) [NC]
RewriteRule ^ - [G,L]

It would block every WordPress post query URL.

Even when the site uses pretty permalinks, old links or external sites may still reference the ?p= version.

How ?p= was abused in the incident

The hacked website produced many URLs in a narrow numerical range:

/?p=23981
/?p=23932
/?p=23919
/?p=23916
/?p=23860
/?p=23795
/?p=23783

Before blocking the range, I checked whether those IDs belonged to real posts.

A database query can help:

SELECT ID, post_title, post_type, post_status
FROM wp_posts
WHERE ID BETWEEN 23000 AND 24999
ORDER BY ID ASC;

If legitimate records exist, a broad range rule may not be appropriate.

You may need to:

  • Exclude known IDs
  • Match another part of the request
  • Handle the response in PHP
  • Use a smaller numerical range
  • Avoid blocking the pattern entirely

?q= may also be legitimate

The q parameter is not WordPress’s default search parameter, but plugins and custom applications may use it.

Examples include:

  • Search plugins
  • Product filters
  • Documentation systems
  • External integrations
  • Analytics tools

Never assume ?q= is malicious simply because it appears in spam examples.

Verify:

  1. Does the legitimate site use it?
  2. What response does a normal ?q= request produce?
  3. Is the suspicious value always numerical?
  4. Does the spam use a consistent length or range?
  5. Do logs show a common user agent or referrer?
  6. Does the pattern appear in Google Search Console?

?a= and other single-letter parameters

Attackers sometimes generate many variants by changing the parameter name:

/?a=839472938
/?b=739284729
/?x=193847293

A tempting firewall rule is:

RewriteCond %{QUERY_STRING} (^|&)[a-z]=[0-9]+(&|$) [NC]
RewriteRule ^ - [G,L]

That is extremely broad.

It may block legitimate features that use parameters like:

?s=wordpress
?p=123
?m=202607

Pattern matching should be evidence-driven.

Collect URLs before blocking them

I combined URLs from several sources:

  • Google Search Console Pages report
  • Search performance exports
  • Server access logs
  • Google search results
  • Malicious sitemap files
  • Security scanner reports

The URLs were saved in a CSV file for analysis.

A simple CSV might look like:

URL
https://example.com/?a=839472938
https://example.com/?q=739284729
https://example.com/?p=23932
https://example.com/real-page/

Filter the domain with Python

import pandas as pd

INPUT_FILE = "urls.csv"
OUTPUT_FILE = "domain-urls.csv"
SITE_URL = "https://example.com"

df = pd.read_csv(INPUT_FILE)

if "URL" not in df.columns:
    raise ValueError("The CSV must contain a column named URL.")

urls = df["URL"].fillna("").astype(str)

domain_urls = df[urls.str.startswith(SITE_URL)].copy()
domain_urls.to_csv(OUTPUT_FILE, index=False)

print(f"Saved {len(domain_urls)} URLs to {OUTPUT_FILE}")

Extract query parameters

Python’s standard library can separate parameter names and values safely:

import pandas as pd
from urllib.parse import urlparse, parse_qs

INPUT_FILE = "domain-urls.csv"
OUTPUT_FILE = "query-parameters.csv"

df = pd.read_csv(INPUT_FILE)

rows = []

for url in df["URL"].fillna("").astype(str):
    parsed = urlparse(url)
    parameters = parse_qs(parsed.query, keep_blank_values=True)

    for name, values in parameters.items():
        for value in values:
            rows.append(
                {
                    "url": url,
                    "path": parsed.path,
                    "parameter": name,
                    "value": value,
                    "value_length": len(value),
                    "numeric": value.isdigit(),
                }
            )

result = pd.DataFrame(rows)
result.to_csv(OUTPUT_FILE, index=False)

print(f"Saved {len(result)} parameter rows to {OUTPUT_FILE}")

This makes it easier to answer questions such as:

  • Which parameter appears most often?
  • Are the values always numbers?
  • What is the usual value length?
  • Are the values concentrated in one range?
  • Do different parameters generate the same content?

Count the patterns

import pandas as pd

df = pd.read_csv("query-parameters.csv")

summary = (
    df.groupby(["parameter", "numeric"])
      .size()
      .reset_index(name="count")
      .sort_values("count", ascending=False)
)

print(summary.to_string(index=False))

Example output:

parameter  numeric  count
a          True     8421
q          True     6138
p          True     1820
s          False      42

This does not prove that a, q, or p are malicious.

It identifies where to investigate.

Review the server response

Test several examples:

curl -I "https://example.com/?a=839472938"
curl -I "https://example.com/?q=739284729"
curl -I "https://example.com/?p=23932"

Check whether they return:

  • 200
  • 301
  • 302
  • 404
  • 410
  • 500

A hacked URL returning 200 is especially important because Google may treat it as a real page.

Also inspect the body:

curl -s "https://example.com/?a=839472938" | head -n 40

Compare normal and suspicious requests.

Check access logs

Useful questions include:

  • Is Googlebot requesting the URLs?
  • Are other bots requesting the same patterns?
  • How often are they requested?
  • Do they reach index.php?
  • Are they producing expensive application responses?
  • Did the requests continue after malware removal?

Example Apache log searches:

grep -E '\?(a|q|p)=[0-9]+' access.log | head

Count matching requests:

grep -Ec '\?(a|q|p)=[0-9]+' access.log

View the most common URLs:

grep -E '\?(a|q|p)=[0-9]+' access.log \
  | awk '{print $7}' \
  | sort \
  | uniq -c \
  | sort -nr \
  | head -n 30

Log formats differ, so the URL may not always be in field seven.

Build the narrowest possible rule

Suppose the investigation confirms:

  • a and q are not used legitimately
  • Their spam values always contain at least eight digits
  • Requests should never return content again

A narrow Apache rule could be:

<IfModule mod_rewrite.c>
RewriteEngine On

RewriteCond %{QUERY_STRING} (^|&)(?:a|q)=[0-9]{8,}(&|$) [NC]
RewriteRule ^ - [G,L]

</IfModule>

This returns 410 Gone.

It does not block:

?s=wordpress
?p=123
?q=shoes
?a=42

Whether those exclusions are correct depends on the actual website.

Handle a confirmed ?p= range separately

If a numerical range has been verified as malicious and contains no legitimate WordPress records:

# Example range only: p=23000 through p=24999
RewriteCond %{QUERY_STRING} (^|&)p=2[3-4][0-9]{3}(&|$) [NC]
RewriteRule ^ - [G,L]

This is safer than blocking every post ID, but it still needs testing.

Test both malicious and legitimate cases

Spam tests:

curl -I "https://example.com/?a=12345678"
curl -I "https://example.com/?q=987654321"
curl -I "https://example.com/?p=23932"

Legitimate tests:

curl -I "https://example.com/?s=security"
curl -I "https://example.com/?p=123"
curl -I "https://example.com/"
curl -I "https://example.com/wp-login.php"

A successful spam response should look like:

HTTP/2 410

The legitimate URLs should keep their expected behaviour.

Why server-level handling matters

If WordPress handles every request, even a missing page can require:

  • PHP startup
  • WordPress bootstrap
  • Plugin loading
  • Database queries
  • Theme rendering

A narrow Apache rule can stop confirmed spam requests earlier.

That reduces unnecessary application work while also providing a clear status to search engines.

Do not treat query patterns as malware signatures

A URL such as:

/?q=12345678

is not automatically proof of malware.

Context matters.

The evidence should include some combination of:

  • Unwanted content
  • Unexpected HTTP 200 responses
  • Search Console impressions
  • Large-scale repeated patterns
  • Malicious files or database content
  • Access-log activity
  • Unknown sitemap entries

The rule should be the result of the investigation, not the beginning of it.

Final checklist

Before blocking query-string spam:

  • Export representative URLs
  • Group them by parameter and structure
  • Confirm whether the parameter is used legitimately
  • Check database IDs for ?p=
  • Test current HTTP responses
  • Inspect server logs
  • Create the narrowest rule possible
  • Test legitimate pages after deployment
  • Monitor 410 responses
  • Keep the patterns out of the canonical sitemap

The larger lesson is simple:

Do not block thousands of URLs individually when a small number of verified patterns explains them.

The full cleanupβ€”including malware removal, Google Search Console requests, 410 handling, and sitemap recoveryβ€”is available in my complete WordPress SEO spam case study.

πŸ“° Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes β€” full credit and traffic to the original publisher.