A Practical Guide for Administrators
Overview
Dynamic Fingerprinting, introduced in UAP 8.4, gives administrators the ability to define custom fingerprint algorithms tailored to their specific applications and traffic patterns. Unlike the platform's legacy static fingerprinting — which applied a fixed algorithm to generate the hash for all the traffic — Dynamic Fingerprinting uses a Lua-based scripting framework embedded in the Defender (Mitigator) to compute multiple, configurable fingerprints at request time.
The result is a composite fingerprint made up of up to five independently computed hashes, each produced by a distinct algorithm. Every hash encodes the algorithm ID in its upper bits, enabling full version tracking and independent updates without impacting other algorithms or downstream detection logic.
Why Dynamic Fingerprint?
Traditional static fingerprints cast a wide net. They group requests by broad attributes, making it difficult to distinguish legitimate user behavior from automation when attackers rotate cookies, mimic header structures, or vary query parameters.
Dynamic Fingerprinting solves this by letting you define what makes a client identity meaningful for your application — whether that's cookie structure, header order, session tokens, TLS handshake attributes, or request body schema . And it works on the fly, you can create and enable a new fingerprint hash instantly — no Defender restarts, no waiting for configuration updates to propagate.
With this you can
Group requests with fewer attributes — Fingerprint requests by finer-grained signals like cookie structure or session tokens, rather than broad request characteristics.
Spot automation that evades IP-based detection — Bots rotating IPs still tend to have consistent structural patterns in headers, cookies, or payloads that expose them.
Understand what goes into your fingerprint — Your fingerprint is composed of the attributes that matter to your application, and you know exactly what goes into it.
Default Algorithms (Out-of-Box)
Cequence ships four template algorithms that serve as starting points:
Cookie FP — Fingerprints the structure of cookie headers by extracting and hashing the ordered list of cookie keys. Particularly useful for stateful web and mobile applications.
Header FP — Captures the order and composition of HTTP headers, including custom application-specific headers that legitimate clients consistently send.
Query Param FP — Fingerprints the structure of query parameters only, not their values. Useful for identifying clients by how they construct requests, regardless of what values they pass.
Query Param Full FP — Fingerprints both the structure and values of query parameters, ideal for APIs where the universe of legitimate query values is well-bounded.
Building a Custom Algorithm
Custom fingerprint algorithms are written in Lua directly from the UAP dashboard and forwarded to the Defender automatically. Your script returns a string — the framework computes a CRC32 hash of that string and incorporates it into the composite fingerprint. What you put into that string is entirely up to you.
Up to 5 algorithms can be active at any time, each contributing one hash to the composite fingerprint. You can have up to 31 algorithms stored in the system — both Cequence-provided system algorithms and your own custom ones.
Your Lua script can access:
- Headers — get all header keys, look up specific header values, or check if a header exists
- Cookies — get all cookie keys, look up specific cookie values, or check if a cookie exists
- Query Parameters — get all query param keys, look up specific values, or check if a param exists
- Misc — hostname, normalized path, raw path, client IP
For example, you might return the ordered list of cookie keys to fingerprint cookie structure, or concatenate specific header values that only legitimate clients send, or extract JSON body keys to detect schema anomalies.
A few things to remember when writing the Script to Create a custom Hash:
Your script must include a RunFPAlgorithm function with the following signature.
function RunFPAlgorithm(txn)
This function receives the transaction object txn and must return two string values. The first string is your computed fingerprint value, which the platform converts to a hash. The second string is a log message for debugging purposes.
If your script fails to define RunFPAlgorithm, the algorithm returns a null hash value (00000000).
Optional initialization
To improve performance, define an Init function that executes once when the algorithm loads. Use Init to precompute static data that the RunFPAlgorithm function can reuse during request processing.
function Init() -- precompute static values here end
Access request data
Within RunFPAlgorithm, call Cequence functions through the cequence namespace to access request data.
| Name | Data type | Description |
|---|---|---|
cequence.getHostName(txn) |
string | Transaction host header value. |
cequence.getPath(txn) |
string | Normalized transaction path. |
cequence.getPathRaw(txn) |
string | Transaction path without normalization. |
cequence.getClientIP(txn) |
string | Computed client IP. |
cequence.getHeaderValue(txn, header_name) |
vector of strings | Header values. |
cequence.headerExists(txn, header_name) |
boolean | True if the header exists. |
cequence.getHeaderKeys(txn) |
vector of strings | All header names. |
cequence.getHeaders(txn) |
map | All headers as name-value pairs. Header names are case-insensitive. |
cequence.getQueryParamValue(txn, param_name) |
vector of strings | Query parameter values. |
cequence.queryParamExists(txn, param_name) |
boolean | True if the query parameter exists. |
cequence.getQueryParamKeys(txn) |
vector of strings | All query parameter names. |
cequence.getQueryParams(txn) |
map | All query parameters as name-value pairs. Parameter names are case-sensitive. |
cequence.getCookieValue(txn, cookie_name) |
vector of strings | Cookie values. |
cequence.cookieExists(txn, cookie_name) |
boolean | True if the cookie exists. |
cequence.getCookieKeys(txn) |
vector of strings | All cookie names. |
cequence.getCookies(txn) |
map | All cookies as name-value pairs. Cookie names are case-sensitive. |
cequence.getBody(txn) |
string | Request body. |
cequence.getBodySize(txn) |
number | Size of the request body. |
Examples —
Fingerprinting by Country Code
In this example, a different hash is produced for each unique value found in the x-countrycode header. Requests from different countries will produce distinct fingerprints, allowing you to build mitigation policies that target specific origin patterns.
function RunFPAlgorithm(txn) local log = ""
-- Check if x-countrycode header exists if not cequence.headerExists(txn, "x-countrycode") then return "NO_COUNTRY_CODE", "x-countrycode header not found" end
-- Get x-countrycode header value(s) local country_code_values = cequence.getHeaderValue(txn, "x-countrycode")
if not country_code_values or #country_code_values == 0 then return "EMPTY_COUNTRY_CODE", "x-countrycode header has no value" end
-- Use the first value local country_code = country_code_values[1]
if country_code == "" then return "EMPTY_COUNTRY_CODE", "x-countrycode value is empty" end
-- Normalize to uppercase (CN, US, CA format) country_code = string.upper(country_code)
log = "Country code fingerprint: " .. country_code
-- Return country code as fingerprint string -- Each country code will hash to different CRC32 return country_code, logend
Fingerprinting on sessionID
Here we are creating a hash on the sessionID, the sessionID is present in the cookie under the field called JSESSIONID
function RunFPAlgorithm(txn) local log = "" -- Check if JSESSIONID cookie exists if not cequence.cookieExists(txn, "JSESSIONID") then return "NO_SESSION", log end -- Extract JSESSIONID value local session_values = cequence.getCookieValue(txn, "JSESSIONID") if not session_values or #session_values == 0 then return "EMPTY_SESSION", log end local session_id = session_values[1] if session_id == "" or session_id == nil then return "NULL_SESSION", log end -- Create fingerprint string from session ID local fingerprint_string = "session:" .. session_id log = "Session ID FP: Found JSESSIONID=" .. session_id return fingerprint_string, logend
A fingerprint algorithm that checks three session sources in priority order and creates a hash from whichever is found first:
function RunFPAlgorithm(txn) local log = "" local session_id = nil local source = nil -- Priority 1: Check JSESSIONID cookie if cequence.cookieExists(txn, "JSESSIONID") then local session_values = cequence.getCookieValue(txn, "JSESSIONID") if session_values and #session_values > 0 and session_values[1] ~= "" then session_id = session_values[1] source = "JSESSIONID_COOKIE" log = "Session ID FP (Priority 1 - JSESSIONID Cookie): " .. session_id return "session:" .. session_id, log end end -- Priority 2: Check x-sessionid header if cequence.headerExists(txn, "x-sessionid") then local header_values = cequence.getHeaderValue(txn, "x-sessionid") if header_values and #header_values > 0 and header_values[1] ~= "" then session_id = header_values[1] source = "X_SESSIONID_HEADER" log = "Session ID FP (Priority 2 - x-sessionid Header): " .. session_id return "session:" .. session_id, log end end -- Priority 3: Check x-sessionq cookie if cequence.cookieExists(txn, "x-sessionq") then local session_values = cequence.getCookieValue(txn, "x-sessionq") if session_values and #session_values > 0 and session_values[1] ~= "" then session_id = session_values[1] source = "X_SESSIONQ_COOKIE" log = "Session ID FP (Priority 3 - x-sessionq Cookie): " .. session_id return "session:" .. session_id, log end end -- No session ID found in any source log = "Session ID FP: No session identifier found in any source" return "NO_SESSION", logend
Once you have written your script, use the Validate Expression option at the bottom of the editor to verify your script is valid before saving.
Using Dynamic Fingerprints in Rules and Policies
Similar to traditional source fingerprint, Dynamic Fingerprints can be reference them in threat protection rules, build aggregators around them, and use them as criteria in mitigation policies.
In Rules and Aggregators
When writing a rule or building an aggregator, you can reference the composite Dynamic Fingerprint or target a specific algorithm's hash individually.
Example — Matching on a Dynamic Fingerprint in a Rule
// Exact match on full Dynamic Fingerprint
input.fingerprint_v2.equals("15a3b2c8f-27d4e1a9b-39C4E7F2-400000000-500000000")Each segment in the fingerprint corresponds to one algorithm's hash. You can match on the full composite value to catch a known bad client identity exactly as it appears in traffic.
Example Aggregators
Use DYNAMICFINGERPRINT to reference the full composite fingerprint across all active algorithms — useful when you want to track or count unique client identities as a whole.
Use DYNAMICFINGERPRINT_ALGO_1, DYNAMICFINGERPRINT_ALGO_2, etc. to reference the hash from a specific algorithm — useful when one algorithm is purpose-built for a particular signal and you want to isolate its output.
-
IP_DYNAMICFINGERPRINT— count how many distinct Dynamic Fingerprints are seen per IP. A spike here is a strong signal of automation rotating application-layer attributes. -
IP_DYNAMICFINGERPRINT__USERAGENT— track unique User Agents seen per IP + Dynamic Fingerprint combination. -
SESSIONID__DYNAMICFINGERPRINT— detect how many distinct Dynamic Fingerprints are associated with a single session, which can indicate session sharing or token replay. -
ORGANIZATION_DYNAMICFINGERPRINT— group and count Dynamic Fingerprints by organization or ASN to identify coordinated bot campaigns from a single network.
In Mitigation Policies
You can create a mitigation policy that uses DYNAMICFINGERPRINT or a specific algorithm hash as the action field or policy criteria — blocking, rate limiting, or challenging traffic that matches the fingerprint pattern tied to bad behavior.
Look up Traffic in Detection Page by Dynamic Fingerprint
Just as you can filter and group transactions by the traditional source fingerprint in the detection page, you can do the same with Dynamic Fingerprints.
If you have a Dynamic Fingerprint value of interest — whether spotted in a rule match, a mitigation hit, or while reviewing a suspicious transaction — you can use it as a pivot directly in the detection page. Filter or group by DYNAMICFINGERPRINT and UAP will load all transactions, IPs, sessions, and other associated data that share that fingerprint value, giving you a full picture of the traffic behind it.
Common Pitfalls
Request body hashing is not supported in this release. Support for body-based fingerprinting is planned for a future release. When it becomes available, avoid hashing entire bodies without excluding dynamic fields — if your body payload includes timestamps, nonces, request IDs, or other values that change with every request, your fingerprint will be unique per transaction and effectively useless for grouping or detection. Extract only the structural or stable elements, such as JSON key names, rather than raw values that vary.
Why Use Dynamic Fingerprinting (FPv2) - 3 Key Reasons with Use Cases
1. Multi-Dimensional Traffic Analysis— Capture Different Behavioral Aspects Simultaneously
The Problem with Legacy Fingerprinting (static CRC32) only capture one dimension of a request . A single fingerprint can't sometimes distinguish between a legitimate user with the same device but using a different browser
FPv2 Solves this by -
Running up to 5 algorithms concurrently, each fingerprinting a different behavioral aspect:
Default FPv2 Format:
1B65537F-133F8F08-19F33542-39CD1374-00000000
↓ ↓ ↓ ↓
Query Header Cookie Header (unused)
Param Standard Keys All
Keys
Each captures a DIFFERENT pattern:
- Query Params: What parameters are being sent?
- Header Order: Which headers and in what order?
- Cookie Structure: What cookies are present?
- Header All: All headers in sequence?
Even if the attacker rotates User-Agent headers every request, the same combo of Query Param FP + Cookie FP identifies the attack pattern
2. Specific Detection Without Separate Builds — Tailor Algorithms to Business Logic
The Problem with Legacy Fingerprinting
Legacy fingerprints are the same across all applications. You can't fingerprint:
- Checkout app specific login parameter names: `?custid=123&pin=4567`
- Gift card enumeration pattern: `/gift-card/{card_number}/balance`
With Fingerprint v2 each application can have their own set of algorithms targeting their specific APIs:
Global Algorithms
- Algorithm 1: Header Standard
Checkout APP:
+ Algorithm 2: Authentication Flow Pattern
+ Algorithm 3: Custom Login Parameter Fingerprint
Giftcard APP:
+ Algorithm 4: Inventory Query Pattern
+ Algorithm 5: Gift Card Enumeration Pattern
With this you have Application specific detection
3. Precise Fingerprint Clustering for Better Bot Identification
The Problem with Legacy Fingerprinting is that they are too broad or too narrow**:
- Too broad: A static CRC32 of header order catches millions of users with the same browser (false positives)
- Too narrow: A single-dimension fingerprint misses sophisticated bots that rotate just one component
With Fingerprint V2 By combining multiple fingerprints, each transaction has a composite signature that's both granular and accurate:
Legacy (1 fingerprint)
- Fingerprint:
3328072988 - Match count: 500,000 users Too many false positives
FPv2 (5 fingerprints)
- FPv2 ID:
1A2B3C4D-5E6F7G8H-9I0J1K2L-3M4N5O6P-7Q8R9S0T - Match count: 127 IPs + 43 fingerprints + specific body pattern
- Intersection of all 5 → ~12 matching transactions High confidence same attacker
Attacker A — Sophisticated ATO
Uses residential proxies (different IP each request, all look normal) and spoofs headers perfectly — but always uses the same body extraction pattern (form data order) and the same query param keys.
// ATO Detection: Same body + query pattern even if IP/headers differ
input.get_aggregate_count("UNIQUE_BODY_PATTERNS", 3600, "DYNAMICFINGERPRINT_BODY") == 1
&& input.get_aggregate_count("UNIQUE_QUERY_PATTERNS", 3600, "DYNAMICFINGERPRINT_QUERY") == 1
&& input.get_unique_value_count("DYNAMICFINGERPRINT_BODY__IP") > 5 // Different IPs, same bodyAttacker B — Credential Stuffing
Uses rotating bot fingerprints (different headers, different query params) — but always from the same datacenter ASN and always has the same cookie structure.l
// Credential Stuffing: Same ASN + cookie even if headers differ
input.get_is_found_in_proxy_feeds() == true
&& input.get_aggregate_count("UNIQUE_COOKIE_PATTERNS", 3600, "DYNAMICFINGERPRINT_COOKIE") == 1
&& input.get_aggregate_count("UNIQUE_HEADERS", 3600, "DYNAMICFINGERPRINT_HEADERS") > 10 // Different headers, same cookiesOutcome: Different attack types → different mitigations. ATO = account lockdown. Credential stuffing = rate limit.