{"id":27130,"date":"2026-11-04T20:09:28","date_gmt":"2026-11-04T20:09:28","guid":{"rendered":"https:\/\/capskip.com\/?p=27130"},"modified":"2026-11-04T20:09:28","modified_gmt":"2026-11-04T20:09:28","slug":"aws-waf-widget-nodejs","status":"publish","type":"post","link":"https:\/\/capskip.com\/zh\/aws-waf-widget-nodejs\/","title":{"rendered":"\u5982\u4f55\u7528 Node.js \u8bc6\u522b AWS WAF \u5c0f\u7ec4\u4ef6\u9a8c\u8bc1\u7801"},"content":{"rendered":"<p>To solve an AWS WAF CAPTCHA in Node.js on a widget page, you read the widget&#8217;s apiKey and the jsapi.js address from the page, send them to the <a href=\"https:\/\/capskip.com\/aws-waf-captcha-solver\/\">CapSkip AWS WAF solver<\/a> with amazonWaf, trade the voucher pair it returns for a token, and send that token with your next request. The widget is the second shape AWS WAF uses. A form or login page draws the puzzle itself, so there is no HTTP 405 page and no gokuProps to read. This guide does the whole loop with the CapSkip Node.js SDK and the fetch that ships with Node.js. The 405 page has its own <a href=\"https:\/\/capskip.com\/aws-waf-captcha-python\/\">Python guide<\/a>.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">What you need<\/h2>\n<ul>\n<li>CapSkip 3.0.0 or later running on a Windows PC. That release added AWS WAF solving. The <a href=\"https:\/\/capskip.com\/setup-guide\/\">setup guide<\/a> covers the first start.<\/li>\n<li>Node.js 18 or later, which is what the package asks for, and the capskip package at version 1.4.0 or later. Earlier versions have no amazonWaf method. The code below ran on Node.js 24.22.0 with capskip 1.4.0. No other Node.js version was tried here.<\/li>\n<li>A PowerShell prompt. The commands use PowerShell syntax.<\/li>\n<li>A page that you own or may automate, with an AWS WAF CAPTCHA widget on it, and a browser with developer tools to look at it once.<\/li>\n<li>An address for the solver. In Local mode CapSkip answers on 127.0.0.1 for that device only. In Server mode it listens on your network address or public IP, so a script on another PC, a VPS or a hosted platform can reach it over the API. Both are set under <a href=\"https:\/\/capskip.com\/setup-guide\/#connection-settings\">connection settings<\/a>.<\/li>\n<\/ul>\n<p>How this was tested: the code ran against three mock servers that I wrote, not against a real AWS WAF site. One copies the AWS WAF checks listed in the CapSkip <a href=\"https:\/\/capskip.com\/api-docs\/\">API docs<\/a>, one plays a site with a widget on its contact page and one plays AWS&#8217;s token host. The SDK was the real capskip 1.4.0. This code did not run against the real CapSkip app, and no live AWS WAF site was involved, so no number below is a solve time, and I cannot tell you that a given live site accepts the token this flow produces.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 1: set up the project and find the widget values<\/h2>\n<p>Create a folder, initialise a project and install capskip:<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"bash\" class=\"EnlighterJSRAW\">npm init -y\nnpm install capskip<\/pre>\n<\/div>\n<p>A widget page loads a script called jsapi.js and calls AwsWafCaptcha.renderCaptcha with an options object. According to the <a href=\"https:\/\/docs.aws.amazon.com\/waf\/latest\/developerguide\/waf-js-captcha-api.html\" rel=\"noopener nofollow\" target=\"_blank\">AWS WAF documentation<\/a>, the apiKey in that object is an encrypted key that lists the client domains allowed to use the widget, so it belongs to one site. CapSkip needs two things from the page: that apiKey, which it calls the sitekey, and the full address of jsapi.js. A widget page has no gokuProps, so you send no iv and no context. Sending the jsapi.js address without them is what marks the request as a widget solve.<\/p>\n<p>The first block reads both from the page&#8217;s HTML. Some sites keep the apiKey in a separate JavaScript bundle instead. When that happens the function returns null, and the fix is to search the scripts the page loads for apiKey.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"js\" class=\"EnlighterJSRAW\">'use strict';\nconst { CapSkip, ApiException } = require('capskip');\n\nconst PAGE_URL = 'https:\/\/www.example.com\/contact'; \/\/ the page that draws the widget\n\/\/ The folder that the page's challenge.js loads from (see the Network tab).\nconst TOKEN_BASE = 'https:\/\/3f2a9c7d41b0.edge.sdk.awswaf.com\/3f2a9c7d41b0\/8e51d0c7a2f4\/';\n\nasync function readWidget() {\n  const res = await fetch(PAGE_URL, { headers: { Accept: 'text\/html' } });\n  const html = await res.text();\n  const src = \/&lt;script[^&gt;]+src=&quot;([^&quot;]*\\\/jsapi\\.js)&quot;\/i.exec(html);\n  const key = \/apiKey\\s*:\\s*[&quot;']([^&quot;']+)[&quot;']\/.exec(html);\n  if (!src || !key) return null; \/\/ not in the HTML: look in the page's scripts\n  return { sitekey: key[1], jsapiScript: new URL(src[1], PAGE_URL).href };\n}<\/pre>\n<\/div>\n<p>The script address on a page can be relative or start with two slashes, so the function turns it into a full address with the page URL. TOKEN_BASE near the top of the block is a constant you fill in by hand, and the next step explains why.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 2: find the token host once<\/h2>\n<p>The voucher pair is traded for a token on AWS&#8217;s own host. On a widget page that host is not in the HTML you fetched. Open the page in a browser, open the Network tab, reload, and look for the request to challenge.js that jsapi.js makes for you. Its folder is the one to copy, ending with a slash. The CapSkip docs say to replace challenge.js with voucher, which is what the code in the next step does with TOKEN_BASE.<\/p>\n<p>This is one lookup per site, so keep it in your configuration. I cannot say how long it stays the same on a real site. If the exchange in the next step starts failing, check the Network tab again before you suspect anything else.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 3: solve it and trade the voucher<\/h2>\n<p>The SDK method is amazonWaf. Only the sitekey and the page URL are required, and the options carry the jsapi.js address. The script address must be an https address on an awswaf.com host, and the SDK refuses anything else before a request is sent. The answer is an object with captcha_voucher and existing_token, plus the raw answer under code and the task number under captchaId. The method waits with the longer 300 second timeout that reCAPTCHA uses, because CapSkip solves the puzzle in a real browser. I did not time it.<\/p>\n<p>The exchange is a POST to the voucher endpoint with the pair as a JSON body. The CapSkip docs send it as text\/plain with an Origin header that matches the site. The CapSkip app&#8217;s own code also sends a Referer, so the code sends both. Node.js&#8217;s fetch lets you set Origin, which a browser would not. The SDK documentation says the pair is short-lived, so exchange it at once and never store it.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"js\" class=\"EnlighterJSRAW\">\/\/ Local mode. In Server mode, use your server's address instead.\nconst solver = new CapSkip({ host: '127.0.0.1', port: 8080 });\n\nasync function getToken(widget) {\n  \/\/ The answer is a voucher pair, not a token.\n  const result = await solver.amazonWaf(widget.sitekey, PAGE_URL, {\n    jsapiScript: widget.jsapiScript,\n  });\n  const reply = await fetch(new URL('voucher', TOKEN_BASE), {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'text\/plain;charset=UTF-8',\n      Origin: new URL(PAGE_URL).origin,\n      Referer: new URL(PAGE_URL).origin + '\/',\n    },\n    body: JSON.stringify({\n      captcha_voucher: result.captcha_voucher,\n      existing_token: result.existing_token,\n    }),\n  });\n  if (!reply.ok) throw new Error(`voucher exchange failed: HTTP ${reply.status}`);\n  return (await reply.json()).token; \/\/ the aws-waf-token value\n}<\/pre>\n<\/div>\n<p>To send the solve through a proxy, add a proxy option to the call: an object with type set to HTTP and uri set to user:pass@1.2.3.4:8080. In the mock run that arrived at in.php as a proxy and a proxytype. One address is fine for testing, but sites behind AWS WAF often pair the CAPTCHA with rate limits, so a steady stream of solves from one address draws harder treatment. The <a href=\"https:\/\/capskip.com\/api-docs\/\">API docs<\/a> suggest a proxy list in the CapSkip settings, or a per-call proxy when the token must come from a particular network. CapSkip&#8217;s own solver sends its page loads, the puzzle and the voucher exchange through one proxy, so AWS sees one address. If you pass a proxy to the solve, send your own requests through the same one. I could not test that part.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 4: send the token the way the page does<\/h2>\n<p>On a widget page the token is the same one the page&#8217;s own onSuccess callback receives. Pages differ in how they pass it on. Many send it as the aws-waf-token cookie and others as an x-aws-waf-token header, so submit the form once by hand with the Network tab open and copy what the page&#8217;s request carries. The function below sends the cookie and shows the header as the alternative. The form fields are placeholders for yours.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"js\" class=\"EnlighterJSRAW\">function submitForm(token) {\n  return fetch(PAGE_URL, {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application\/x-www-form-urlencoded',\n      Cookie: `aws-waf-token=${token}`, \/\/ or 'x-aws-waf-token': token\n    },\n    body: new URLSearchParams({ email: 'me@example.com', message: 'Hello' }),\n  });\n}<\/pre>\n<\/div>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 5: the full working example<\/h2>\n<p>The last block turns the pieces into a full AWS WAF CAPTCHA in Node.js solver, with the retry and the entry point. Save all four code blocks in one file, one after another, and run it with node. A failed puzzle raises ApiException, and the loop tries again, which draws a new puzzle each time. Any other error is thrown, so a wrong host or a refused exchange is not retried silently.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"js\" class=\"EnlighterJSRAW\">async function main() {\n  const widget = await readWidget();\n  if (!widget) throw new Error('no jsapi.js script or apiKey in the page HTML');\n  for (let attempt = 1; attempt &lt;= 3; attempt++) {\n    try {\n      const token = await getToken(widget); \/\/ exchange at once, never store it\n      const res = await submitForm(token);\n      console.log(res.status, await res.text());\n      return;\n    } catch (err) {\n      if (!(err instanceof ApiException)) throw err;\n      console.log(`attempt ${attempt}: ${err.message}`); \/\/ each retry draws a new puzzle\n    }\n  }\n  throw new Error('AWS WAF CAPTCHA not solved in 3 attempts');\n}\n\nmain().catch((err) =&gt; {\n  console.error(`${err.constructor.name}: ${err.message}`);\n  process.exitCode = 1;\n});<\/pre>\n<\/div>\n<p>In the mock run the script behaved like this:<\/p>\n<table>\n<thead>\n<tr>\n<th>Mock setting<\/th>\n<th>What the script did<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Every puzzle answered<\/td>\n<td>One task, one voucher exchange, then status 200 and the text Message sent<\/td>\n<\/tr>\n<tr>\n<td>First two puzzles refused<\/td>\n<td>Two attempt lines, then status 200 on the third task<\/td>\n<\/tr>\n<tr>\n<td>First three puzzles refused<\/td>\n<td>Three attempt lines, then the error line and exit code 1<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>The token works for as long as the site&#8217;s immunity time. AWS sets 300 seconds by default and lets the site owner choose from 60 seconds to three days, as the <a href=\"https:\/\/docs.aws.amazon.com\/waf\/latest\/developerguide\/waf-tokens-immunity-times.html\" rel=\"noopener nofollow\" target=\"_blank\">AWS WAF documentation<\/a> explains. If you send many requests, keep the token and reuse it until it expires instead of solving once per request.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Common errors<\/h2>\n<table>\n<thead>\n<tr>\n<th>Error<\/th>\n<th>Cause<\/th>\n<th>Fix<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>no jsapi.js script or apiKey in the page HTML<\/td>\n<td>The page builds the widget from a bundle, so the HTML does not hold them<\/td>\n<td>Search the loaded scripts for apiKey and jsapi.js, then pass the values in by hand<\/td>\n<\/tr>\n<tr>\n<td>ValidationException<\/td>\n<td>iv without context, or a script address that is not a full https address on an awswaf.com host<\/td>\n<td>Send no iv or context for a widget, and copy the jsapi.js address exactly as the page has it<\/td>\n<\/tr>\n<tr>\n<td>ApiException with ERROR_CAPTCHA_UNSOLVABLE<\/td>\n<td>The puzzle could not be answered<\/td>\n<td>Submit again. The <a href=\"https:\/\/capskip.com\/api-docs\/\">API docs<\/a> note that every attempt draws a fresh puzzle<\/td>\n<\/tr>\n<tr>\n<td>ERROR_TASK_NOT_SUPPORTED<\/td>\n<td>The API docs list this for a task type that is switched off in CapSkip<\/td>\n<td>Check the AWS WAF settings in the CapSkip app<\/td>\n<\/tr>\n<tr>\n<td>ERROR_PROXY_FORMAT<\/td>\n<td>The proxy string could not be parsed<\/td>\n<td>Use user:pass@ip:port<\/td>\n<\/tr>\n<tr>\n<td>ERROR_NO_SLOT_AVAILABLE<\/td>\n<td>The API key has a request rate limit and you went over it<\/td>\n<td>Wait a few seconds and submit again<\/td>\n<\/tr>\n<tr>\n<td>NetworkException<\/td>\n<td>CapSkip is not running, or the host or port is wrong<\/td>\n<td>Start CapSkip and check the connection settings<\/td>\n<\/tr>\n<tr>\n<td>voucher exchange failed: HTTP 4xx<\/td>\n<td>Most likely a stale or mismatched pair, or the wrong TOKEN_BASE. The CapSkip app&#8217;s code expects HTTP 400 from AWS for an invalid existing_token. I could not reproduce this one against real AWS<\/td>\n<td>Exchange at once, check the folder in the Network tab, and submit a new task<\/td>\n<\/tr>\n<tr>\n<td>The form post is still refused<\/td>\n<td>The token expired, or the page passes it in a header and you sent a cookie<\/td>\n<td>Copy what the page&#8217;s own request carries<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Running the solver on a server<\/h2>\n<p>The script needs two things from CapSkip: a host and a port. In Local mode that is 127.0.0.1 and your own PC. In Server mode CapSkip listens on your network address or a public IP, so a script on another PC, a VPS or a hosted platform can call it over the same API. Change the host and port in the CapSkip constructor and nothing else in the code changes. A static public IP is recommended for stable remote access. Server mode is still your own hardware and still unmetered. It does open the port to any address, so add a Windows Firewall block rule for the addresses that should not reach it. The <a href=\"https:\/\/capskip.com\/setup-guide\/#connection-settings\">connection settings<\/a> show how to switch modes.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">FAQ<\/h2>\n<details style=\"border:1px solid #e2e5ee;border-radius:10px;padding:14px 18px;margin:0 0 12px;\">\n<summary style=\"cursor:pointer;\">\n<h3 style=\"font-size:1.15rem;line-height:1.4;display:inline;margin:0;\">How do I tell a widget page from a 405 challenge page?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">A challenge page answers the request with HTTP 405 and defines gokuProps. A widget page is an ordinary page that loads jsapi.js and calls renderCaptcha, usually on a form. The same amazonWaf method handles both, with different values.<\/p>\n<\/details>\n<details style=\"border:1px solid #e2e5ee;border-radius:10px;padding:14px 18px;margin:0 0 12px;\">\n<summary style=\"cursor:pointer;\">\n<h3 style=\"font-size:1.15rem;line-height:1.4;display:inline;margin:0;\">Can I solve an AWS WAF CAPTCHA in Node.js without a browser?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">Your script needs no browser. It sends plain HTTP requests to CapSkip and to the site, and CapSkip solves the puzzle in a real browser on the machine where it runs, as the SDK documentation describes. You only open a browser yourself once, to read the values off the page.<\/p>\n<\/details>\n<details style=\"border:1px solid #e2e5ee;border-radius:10px;padding:14px 18px;margin:0 0 12px;\">\n<summary style=\"cursor:pointer;\">\n<h3 style=\"font-size:1.15rem;line-height:1.4;display:inline;margin:0;\">Does the SDK return the token directly?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">No. It returns captcha_voucher and existing_token, and you trade them at the voucher endpoint. The JSON createTask API accepts AWS WAF too, and several of its formats return the token directly. The <a href=\"https:\/\/capskip.com\/api-docs\/\">API docs<\/a> list them.<\/p>\n<\/details>\n<details style=\"border:1px solid #e2e5ee;border-radius:10px;padding:14px 18px;margin:0 0 12px;\">\n<summary style=\"cursor:pointer;\">\n<h3 style=\"font-size:1.15rem;line-height:1.4;display:inline;margin:0;\">Can a hosted platform run this against my CapSkip?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">Yes, through Server mode. Set CapSkip to listen on your network or public IP, point the CapSkip constructor at that host and port, and the loop works the same as on one PC. The solver stays on your hardware.<\/p>\n<\/details>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">The short version<\/h2>\n<p>An AWS WAF CAPTCHA in Node.js is the same puzzle on a widget as on a 405 page, in a different place: read the apiKey and the jsapi.js address, call amazonWaf, trade the pair at the voucher endpoint, and send the token the way the page does. CapSkip is a <a href=\"https:\/\/capskip.com\/\">captcha bypass<\/a> that runs on your own machine, so a puzzle it cannot answer costs you a retry and not a charge. For the rest of the SDK, see the <a href=\"https:\/\/capskip.com\/nodejs-captcha-solver\/\">Node.js CAPTCHA solver page<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>\u6709\u4e9b\u9875\u9762\u4e0d\u663e\u793a 405 \u9875\u9762\uff0c\u800c\u662f\u5728\u8868\u5355\u91cc\u7ed8\u5236 AWS WAF \u62fc\u56fe\u3002\u4e0b\u9762\u662f\u5904\u7406\u8fd9\u79cd\u7ec4\u4ef6\u7684 Node.js \u5faa\u73af\uff1a\u627e\u5230 apiKey \u548c jsapi.js\uff0c\u8c03\u7528 CapSkip \u7684 amazonWaf\uff0c\u7528 voucher \u6362\u53d6 token\uff0c\u518d\u63d0\u4ea4\u8868\u5355\u3002<\/p>","protected":false},"author":1,"featured_media":27129,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"AWS WAF CAPTCHA in Node.js: Solve the Widget | CapSkip","rank_math_description":"Solve an AWS WAF CAPTCHA in Node.js on a widget page: find apiKey and jsapi.js, call amazonWaf, trade the voucher for a token. Mock tested. Copy the code.","rank_math_focus_keyword":"aws waf captcha in node.js","footnotes":""},"categories":[70],"tags":[],"class_list":["post-27130","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-captcha"],"_links":{"self":[{"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts\/27130","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/comments?post=27130"}],"version-history":[{"count":2,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts\/27130\/revisions"}],"predecessor-version":[{"id":27134,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts\/27130\/revisions\/27134"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/media\/27129"}],"wp:attachment":[{"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/media?parent=27130"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/categories?post=27130"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/tags?post=27130"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}