S
Home Application Security ยท Updated 2026-07-14

HTTP User Agents

The User-Agent request header is a free-text string a client sends to identify itself โ€” nominally the browser, operating system, and rendering engine making the request. It is one of the oldest and most widely inspected signals in web security tooling, and also one of the weakest: the client fully controls its content, and nothing about HTTP requires it to be truthful. A WAF that trusts the User-Agent at face value is trusting the attacker to accurately label their own attack traffic.

This article defines the recommended WAF action โ€” Allow, Delay, Challenge, or Block โ€” for the categories of User-Agent a WAF will see in practice, and the reasoning behind each. It assumes a baseline WAF is deployed; see Web Application Firewall for foundational concepts and Advanced Web Application Firewall for the fingerprinting techniques (JA3/TLS, HTTP/2, behavioural) referenced throughout as cross-checks.


What a User-Agent Is

A typical browser User-Agent looks like:

Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36

The format is a historical accretion rather than a clean specification โ€” every major browser claims to be Mozilla/5.0 and lists the engines of its ancestors and competitors for legacy compatibility. Non-browser clients follow their own convention, typically product/version:

python-requests/2.31.0
curl/8.4.0
PostmanRuntime/7.36.0
Googlebot/2.1 (+http://www.google.com/bot.html)
com.example.myapp/2.1.0 (iOS 17.4; iPhone15,3)

Why It Cannot Be Trusted Alone

  • It is entirely client-supplied. Any HTTP client โ€” curl, a Python script, a browser devtools override, a mobile proxy tool โ€” can set this header to any value.
  • Spoofing tooling is trivial and ubiquitous. Setting a Chrome on Windows User-Agent from a curl one-liner takes a single flag: curl -A "Mozilla/5.0 ...".
  • Legitimate clients change it too. Browser vendors rotate version numbers constantly, and privacy-focused browsers/extensions deliberately randomise or genericise the string.
  • It does not prove the claimed software actually produced the request. A UA string claiming Chrome tells you nothing about whether the TLS handshake, HTTP/2 frame ordering, or JavaScript execution environment actually matches real Chrome โ€” see the fingerprinting layers in Advanced Web Application Firewall.

The User-Agent is useful as a routing and prioritisation signal, and as one input into a broader risk score โ€” never as a standalone authorisation decision, and never as a substitute for verifying identity (reverse DNS/ASN for bots, TLS/HTTP fingerprint for browsers, authentication for users).


The Four WAF Actions

Action Mechanism Use When
Allow Request proceeds normally, no added friction The UA is expected, low-risk, and ideally corroborated by a second signal (reverse DNS, IP allowlist, valid API auth)
Delay Artificial latency, reduced rate limit, or connection throttling โ€” the request still completes The traffic is plausibly legitimate but low-priority, or the goal is to raise the cost of automation without blocking outright
Challenge An interactive or silent verification step (JS challenge, CAPTCHA, proof-of-work, step-up auth) must pass before the request proceeds The UA is ambiguous โ€” could be human or automated โ€” the endpoint is sensitive enough to warrant friction but not an outright block, and a browser or human is actually present to respond (see the M2M callout below โ€” this action is not viable for webhook/API traffic)
Block Request is rejected (HTTP 403 or connection reset), typically logged and alerted The UA is known-malicious, claims an identity it demonstrably isn't, or matches a pattern with no legitimate use case on this application

Challenge and Delay exist because Block is a blunt instrument: false positives on Block affect real users and real revenue, while Delay and Challenge let low-confidence signals accumulate more evidence (does it solve the JS challenge? does it back off when throttled?) before an irreversible decision is made.

Callout โ€” Challenge assumes a human, or at least a browser, is present. M2M (machine-to-machine) traffic โ€” API integrations and webhook deliveries in particular โ€” is automated non-human traffic by design, but that does not make Challenge the right control for it. A webhook sender (Stripe, GitHub, Twilio, etc.) or a server-side API client has no browser engine to execute a JS challenge and no human to solve a CAPTCHA. Routing that traffic through Challenge does not slow an attacker down โ€” it simply fails the legitimate integration outright, and most webhook senders will retry a fixed number of times with backoff before marking the delivery permanently failed and disabling it, which looks identical to an outage from the sender's side.

For known M2M endpoints (webhook receivers, first- and third-party API integrations), replace Challenge with:
- Strong authentication instead of interactive proof: HMAC payload signature verification (e.g. Stripe-Signature, GitHub's X-Hub-Signature-256), mTLS, or OAuth client-credentials/API keys โ€” see API Security. A verified signature is a stronger identity signal than anything a JS challenge produces.
- Allow once authentication passes, Block once it fails โ€” Challenge is not a valid middle state for these endpoints.
- Delay (rate limiting) is still appropriate and does not break M2M clients the way Challenge does, since a well-behaved client backs off and retries rather than failing to respond at all.
- Explicitly exempt known webhook paths and confirmed partner source IPs/ASNs from any cloud WAF or bot-management product's default Challenge behaviour โ€” the default is tuned for browser traffic and will otherwise silently break integrations.


Category Examples Recommended Action Why
Missing / empty User-Agent No header sent, or an empty string Challenge Virtually all real browsers and well-behaved HTTP libraries send a User-Agent by default; its total absence is unusual enough to warrant a challenge rather than an outright block, since some legitimate minimal IoT/embedded clients omit it
Current-version mainstream browser Chrome, Firefox, Safari, Edge โ€” versions released in the last 12 months Allow The expected majority of real traffic; cross-check against TLS/JA3 fingerprint (see Advanced WAF) to catch UA spoofing rather than trusting the string alone
Outdated / EOL browser version Internet Explorer 11, Chrome/Firefox versions several years old Delay Legitimate legacy users still exist (regulated/kiosk environments), but old engines carry known client-side vulnerabilities and outdated UAs are commonly reused by bot frameworks precisely because they look "boring"; delay while logging for a deprecation/upgrade campaign
Headless browser signature HeadlessChrome, Headless tokens, PhantomJS remnants Challenge Confirmed headless automation; frameworks that forget to mask the token are typically less sophisticated, and a JS/behavioural challenge (canvas, WebGL, mouse movement โ€” see Advanced WAF) will catch most of them without blocking every automated QA/monitoring tool outright
Generic scripting HTTP client python-requests, curl, Go-http-client, axios, node-fetch, Java/1.8 Context-dependent โ€” Allow on authenticated API/webhook endpoints with valid credentials; Delay/Block (never Challenge) on public login, checkout, and form-submission pages These are legitimate for machine-to-machine API and webhook traffic (see API Security and the M2M callout above) but have no business hitting a browser-rendered login form; on M2M endpoints, a failed Challenge just breaks the integration rather than deterring an attacker, so the correct control there is signature/API-key verification, not an interactive challenge
Verified good bot โ€” claimed UA string reads Googlebot, Bingbot, AhrefsBot, etc. Challenge (verify), then Allow The UA claim alone is free to fake โ€” malicious scanners routinely impersonate Googlebot to bypass naive allowlists. Verify via reverse DNS lookup that resolves to the claimed provider's domain and forward-confirms, or match against the provider's published IP ranges, before granting Allow
Verified good bot โ€” IP/DNS confirmed Reverse-DNS-confirmed Googlebot/Bingbot from Google/Microsoft's published ranges Allow Identity is corroborated by infrastructure the operator doesn't control, not just a header; blocking confirmed search crawlers damages SEO for no security benefit
AI / LLM training and retrieval crawlers GPTBot, ClaudeBot, Google-Extended, CCBot, Bytespider, PerplexityBot Delay (rate-limit) or Block, per robots.txt policy Not inherently malicious, but a business/content-licensing decision rather than a security one; apply consistent rate limiting regardless of the decision to prevent crawl volume from degrading origin performance
Social/link-preview unfurl bots facebookexternalhit, Twitterbot, Slackbot-LinkExpanding, Discordbot Allow (public content only) Needed for link previews to render correctly when content is shared; restrict to public, non-authenticated routes โ€” never allow these to reach pages requiring a session
Monitoring / uptime services Pingdom, UptimeRobot, StatusCake, internal health-check agents Allow, scoped to health-check endpoints, via IP allowlist Expected, low-volume, business-critical traffic; corroborate with source IP rather than trusting the UA string in isolation
Known attack/scanning tools sqlmap, Nikto, Nmap Scripting Engine, Hydra, w3af, Metasploit default strings Block + alert No legitimate production traffic presents these signatures; unauthorised presence is itself a high-confidence indicator of active reconnaissance or attack, and should feed directly into incident response
Authorised pentest / scanner tooling Burp Suite, OWASP ZAP, Acunetix, Nessus Block by default; temporary Allow via a scoped, time-boxed IP allowlist during an authorised engagement Same signatures as the row above from the WAF's perspective โ€” the only safe differentiator is a pre-arranged, time-limited exception tied to a specific test window and source IP, not the UA string
Mobile app โ€” current version com.example.myapp/2.1.0 (iOS 17.4; ...) matching the latest released build Allow Expected first-party traffic; validate against a maintained list of current app versions rather than pattern-matching loosely
Mobile app โ€” deprecated version Same app UA pattern, but a version below the enforced minimum Challenge or Block (force upgrade) Old app builds may lack security patches or use deprecated, less-secure API contracts; blocking with a clear "please update" response is preferable to silently allowing a known-vulnerable client to keep authenticating
UA/fingerprint mismatch UA claims a mainstream browser, but TLS JA3 hash or HTTP/2 frame ordering does not match that browser's known fingerprint Block This is the highest-confidence spoofing signal available โ€” the UA string is actively lying about the client, which has no legitimate justification (see Advanced WAF for fingerprinting mechanics)
Excessively long, malformed, or payload-bearing UA UA string containing SQL/script fragments, thousands of characters, control characters Block Any well-formed client, hostile or not, sends a compact identification string; an oversized or payload-laden UA is either an injection attempt targeting log processors/admin dashboards that render the UA unsanitised, or a buffer/parser-abuse probe

Implementation Notes

Corroborate, Don't Trust in Isolation

Every row above pairs the User-Agent with a second signal โ€” reverse DNS, IP range, TLS fingerprint, authentication state, or app version registry. Treat the UA as the first filter that determines which corroboration check runs, not the final verdict.

# ModSecurity: challenge requests claiming to be Googlebot that don't
# resolve via reverse DNS to a googlebot.com / google.com host
SecRule REQUEST_HEADERS:User-Agent "@contains Googlebot" \
    "id:2001,phase:1,pass,nolog,\
     setvar:tx.claimed_bot=googlebot"

# Follow-up (application/edge layer): perform reverse DNS + forward-confirm
# on tx.claimed_bot before granting Allow; otherwise route to Challenge/Block.
# Example rate-based rule: delay (throttle) generic scripting clients
# on public-facing routes rather than blocking outright
- Name: ThrottleGenericClients
  Statement:
    AndStatement:
      Statements:
        - ByteMatchStatement:
            FieldToMatch: { SingleHeader: { Name: "user-agent" } }
            SearchString: "python-requests"
            PositionalConstraint: CONTAINS
        - NotStatement:
            Statement:
              ByteMatchStatement:
                FieldToMatch: { UriPath: {} }
                SearchString: "/api/"
                PositionalConstraint: STARTS_WITH
  Action:
    Challenge: {}

Log the Raw String, Always

Regardless of the action taken, log the full raw User-Agent alongside the request. It is frequently the first artefact reviewed during incident response to cluster related requests, identify a specific attack tool version, or confirm whether a false positive affected a legitimate but unusual client.

Re-tune After Every Bot-Landscape Shift

User-Agent-based rules decay faster than most WAF rules โ€” new AI crawlers, new headless framework defaults, and new legitimate SDKs appear continuously. Review and retune UA-based rules on the same cadence as CRS rule updates (see Web Application Firewall), not as a one-time configuration.

Never Let UA Filtering Replace Authentication or Input Validation

A User-Agent rule is a triage and cost-raising control, not an access control. An attacker who solves a challenge or waits out a delay still needs to fail on authentication, authorisation, and input validation to be stopped โ€” UA handling reduces noise and raises the cost of automation, it does not substitute for those controls.


Hardening Checklist

Control Priority Notes
No security decision relies on User-Agent alone Critical Always pair with IP/DNS verification, TLS/HTTP fingerprint, or auth state
M2M endpoints (webhooks, API integrations) excluded from Challenge actions Critical Interactive/JS challenges cannot be completed by non-browser clients; use signed payloads, mTLS, or API keys instead
Claimed search/crawler bots verified via reverse DNS before Allow High Prevents Googlebot/Bingbot impersonation bypassing allowlists
UA vs TLS/HTTP fingerprint mismatch blocked High Highest-confidence spoofing signal; requires JA3/HTTP fingerprinting capability
Known attack tool signatures blocked and alerted High sqlmap, Nikto, Hydra, Metasploit, etc.
Pentest tool exceptions scoped to time-boxed, IP-restricted windows High Never a standing allowlist
Deprecated mobile app versions challenged or blocked Medium Forces upgrade off known-vulnerable client builds
AI/LLM crawler policy defined and enforced via robots.txt + rate limiting Medium Business decision, but must be consistently enforced regardless of outcome
Oversized / malformed / payload-bearing UA strings blocked Medium Guards against log/dashboard injection and parser abuse
Raw User-Agent logged on every request regardless of action taken Medium Primary artefact for incident triage and false-positive review
UA-based rules reviewed on a recurring cadence Medium Bot/crawler landscape shifts faster than most WAF rule categories

References

The Security Architecture Site โ€” for internal reference use. Back to contents