AWS WAF challenge: you need the script URL, not just the token
Most anti-bot walls ask one question: where is the sitekey? Turnstile hands you one, DataDome binds its token to the page you were on, Akamai hands you a script block. AWS WAF's Challenge action is different. There is no
Most anti-bot walls ask one question: where is the sitekey? Turnstile hands you one, DataDome binds its token to the page you were on, Akamai hands you a script block. AWS WAF's Challenge action is different. There is no sitekey, and nothing that looks like one.
What you get instead is a challenge script URL. Almost every AWS WAF integration I see fail, fails in one of three places: the field name, where the script URL came from, and the clock.
What the browser was doing
When a page is protected by the WAF challenge action, the browser does four things: loads a small script the WAF injected into the page, runs it, receives an aws-waf-token cookie, and sends that cookie on every subsequent request. If you drop the cookie in without the script context, you are sending a token with no relationship to the session the WAF is scoring.
The two inputs
An AWS WAF task takes exactly two required fields:
{
"type": "awswaf",
"task": {
"websiteURL": "https://example.com/protected",
"WafScriptUrl": "https://example.com/challenge.js?..."
}
}
-
websiteURLis the page you actually want to reach, without credentials in it. -
WafScriptUrlis the full URL of the challenge script as served on that page.
The casing is not cosmetic
It is WafScriptUrl. Not wafScriptUrl, not waf_script_url. Field names in this API are case-sensitive and a wrong case comes back as 400 INVALID_TASK, which reads like a validation bug until you diff your JSON against the reference.
The query string is the whole point
The script URL carries the parameters that identify which distribution and which protected resource you are solving for. A script URL captured from a different host, a different section of the site, or an older deploy can still return a token — the token is just scoped to something that is not your target, so the target rejects it. Copy the full URL including the query string, from the actual blocked page, and do not trim it.
Practical way to find it: open the blocked page, view source, and search for the script tag the WAF injected into the document. It is served from the site's own domain and it is the only script on the page with that shape.
No custom user agent — but proxies are fine
Unlike most task types, awswaf has no useragent request field. If you need a proxy you send proxyType: http with address, port and optional credentials, and then you have one rule: same egress every time. The returned solution.useragent belongs to the session that earned the token. Send the token from a different IP, or with a different UA than the one that came back, and you have rebuilt the mismatch you were trying to avoid.
The timer matters more than the token
Solving is asynchronous, and it is the timing that trips up retry logic:
- create returns
202with a task id, status usuallyqueued - poll
GET /v1/tasks/{id}every 2–3 seconds - the soft processing limit for this type is 180 seconds; the current node timeout is around 100 seconds
- after the soft limit a task can move to
uncertain— not failed, not done
The one retry mistake worth naming: do not recreate a task with a fresh Idempotency-Key because the first one looked slow. The accepted task can still resolve, and you have now paid for two. Keep the key tied to the business request, not to the attempt.
Reading the result
Only read solution when status is completed, and expect the token in either of two places:
-
solution.value— the verification token directly -
solution.cookies["aws-waf-token"]— the cookie form
Then send it as the aws-waf-token cookie against the target domain, together with solution.useragent if a proxy was used. The platform's response is authoritative about which shape you got; do not assume one.
Failure modes, in the order you will meet them
| Symptom | Usually means |
|---|---|
400 INVALID_TASK |
wrong field casing, missing script URL, or a URL with credentials |
503 TYPE_UNAVAILABLE |
the type is not in GET /v1/prices on your account |
429 TASK_LIMIT_EXCEEDED |
concurrency, not balance |
| token accepted, then 403 | script URL from the wrong page, or egress/UA changed mid-session |
TASK_FAILED after ~100s |
script URL reachable by the page but not by the solver |
A short checklist
- Copy the script URL from the blocked page, query string intact.
- Keep
WafScriptUrlcapitalised. - Pin one egress and reuse
solution.useragent. - Poll every 2–3 seconds, keep the idempotency key stable.
- Read
valueorcookies— whichever is present.
I maintain SolveHub, the API in the examples above — it returns tokens and clearance cookies for authorised testing of pages you are allowed to test. The AWS WAF reference, including every field limit, is at tiam.codes/docs?guide=awswaf.
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.