What is AWS WAF Challenge and how to recognise it
AWS WAF Challenge is a silent JavaScript verification step. When the request lacks an acceptable token, the Challenge action can return HTTP 202 with x-amzn-waf-action: challenge. An HTML response includes the challenge script when the request asks for HTML.
The separate CAPTCHA action can return HTTP 405 with x-amzn-waf-action: captcha and a puzzle. The status and action header help distinguish these flows; a page merely being protected by AWS WAF is not enough to choose the right solver.
AWS documents the distinction in CAPTCHA and Challenge action behavior. A token can carry different completion state for these actions, so obtaining a Challenge token does not certify that a CAPTCHA puzzle was completed.
Which page URL and challenge.js URL to send
Use the URL of the protected site as websiteURL. Inspect its challenge page or Network requests and copy the complete challenge.js URL as scriptUrl.
The loader can be hosted on the protected site or on an AWS WAF SDK host. Its identifiers belong to that integration. Copy the actual URL, including any query string, instead of building one from a different site's example.
scriptUrl must use HTTPS, its path must end in /challenge.js, and it must have no URL fragment. The optional title is the challenged page's document title. A site key, script array or captured page HTML is not required by this task.
The AWS Challenge API reference contains the exact input and output example.
Where the cookie goes
Set solution.token as the value of the aws-waf-token cookie on the protected site. Do not send the entire solution object as a cookie, and do not assign the cookie to an unrelated SDK-script host.
When the solution includes userAgent, use that value for the replay request. Keep the cookie with the client session that will access the protected application.
This type returns the cookie value in token; it does not promise a full cookies jar. If the application needs additional session cookies, retain those from your own application session. The cookie is not a login session either: the protected request keeps its own authentication, and other WAF rules can still refuse a route.
Common mistakes
Do not select Challenge for a CAPTCHA puzzle simply because both products use AWS WAF tokens. The task deliberately has its own Challenge name so the two integrations cannot be confused.
A guessed script URL, a non-HTTPS loader, a URL fragment or a script whose path does not end in /challenge.js does not describe the supported loader. Copy the challenge page's actual request.
Also distinguish WAF verification from application authentication. If the protected request still fails, inspect the application response rather than assuming that any failure means the cookie was not generated.
Solve it with SurSolver
Create an AwsChallengeCookie task with websiteURL and the actual scriptUrl, optionally adding the document title. Poll getTaskResult until the task is ready.
Apply the returned token as the aws-waf-token cookie and make the protected request with your client. The AWS Challenge API reference shows the full payload. AWS WAF CAPTCHA is a different verification flow and is not selected by this task name.
The Playwright script below creates the task, adds the solved token to its browser context as the aws-waf-token cookie with the returned user agent, reloads the protected page and waits for an element only the served page contains. The AWS Challenge Playwright guide walks through the same flow step by step.
// aws-challenge-playwright.js - solve AWS Challenge and open the protected page with the
// aws-waf-token cookie.
// Install: npm install playwright && npx playwright install chromium
// Run: node aws-challenge-playwright.js
const { chromium } = require('playwright');
const { setTimeout: sleep } = require('node:timers/promises');
// Your SurSolver API key.
const CLIENT_KEY = 'YOUR_API_KEY';
// The protected page that answers with the challenge.
const TARGET_URL = 'https://example.com/page';
// Optional, read from the page when empty: the challenge.js request of the challenged page
// (DevTools, Network).
const SCRIPT_URL = '';
// A selector that matches only after the site accepted the result.
const SUCCESS_SELECTOR = '.PASTE_ACCEPTED_SELECTOR';
const API = 'https://api.sursolver.com';
const POLL_ATTEMPTS = 60;
// API errors are HTTP 200 with errorId 1; anything else is a transport failure.
async function call(path, body) {
const res = await fetch(`${API}${path}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
if (data.errorId !== 0) throw new Error(`${data.errorCode}: ${data.errorDescription}`);
return data;
}
async function solve(task) {
const created = await call('/createTask', { clientKey: CLIENT_KEY, task });
for (let attempt = 0; attempt < POLL_ATTEMPTS; attempt += 1) {
const result = await call('/getTaskResult', {
clientKey: CLIENT_KEY,
taskId: created.taskId,
});
if (result.status === 'ready') return result.solution;
await sleep(3000);
}
throw new Error(`task ${created.taskId} was not ready in ${POLL_ATTEMPTS} polls`);
}
// Optional task fields are sent only when they have a value.
function withoutEmpty(fields) {
return Object.fromEntries(
Object.entries(fields).filter(([, value]) => value !== undefined && value !== ''),
);
}
// A value the page did not show must be pasted into its constant at the top.
function need(name, value) {
if (value === undefined || value === null || value === '') {
throw new Error(`Could not read ${name} from the page; fill in the ${name} constant.`);
}
return value;
}
// Opens the page once and reads what the solve needs from it.
async function readPage(browser) {
const page = await browser.newPage();
const requests = [];
page.on('request', (request) => requests.push(request));
try {
await page.goto(TARGET_URL, {
waitUntil: 'domcontentloaded',
timeout: 45000,
});
// Requests made after load count too; wait for them, but never hang on a busy page.
await page.waitForLoadState('networkidle', { timeout: 5000 })
.catch(() => undefined);
const urls = (matches) =>
[...new Set(requests.filter(matches).map((request) => request.url()))];
return {
pageUrl: page.url(),
scriptUrl: urls((request) => /\/challenge\.js(\?|$)/.test(request.url()))[0],
};
} finally {
await page.close();
}
}
async function main() {
const browser = await chromium.launch();
try {
const found = await readPage(browser);
const solution = await solve({
type: 'AwsChallengeCookie',
websiteURL: found.pageUrl,
scriptUrl: need('scriptUrl', SCRIPT_URL || found.scriptUrl),
});
// Replay with the user agent the solve presented.
const context = await browser.newContext(
solution.userAgent ? { userAgent: solution.userAgent } : {},
);
await context.addCookies([
{
name: 'aws-waf-token',
value: solution.token,
domain: new URL(found.pageUrl).hostname,
path: '/',
secure: true,
},
]);
const page = await context.newPage();
await page.goto(found.pageUrl, { waitUntil: 'domcontentloaded' });
await page.locator(SUCCESS_SELECTOR).first().waitFor({ timeout: 15000 });
console.log('The site served the protected page.');
} finally {
await browser.close();
}
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});