Skip to content
SpideyData
Engineering

Inside reCAPTCHA v3: Tracing Requests and Asynchronous Data

A technical reading of a reCAPTCHA v3 investigation, with the original debugging screenshots: request boundaries, serialization, Promise results, and browser state.

SFSpideyData team
12 min read

Read the evidence before the score

An unfamiliar browser runtime is easier to understand when each claim has an observable boundary: a request, an object before serialization, a resolved Promise, or a verification response. This field note examines those boundaries in a published reCAPTCHA v3 investigation by 無色逆向. All 40 process screenshots below are the original images, in their original order; click any image to inspect its full resolution.

The source follows a request payload backward through an obfuscated JavaScript runtime. Our commentary focuses on how to assess that evidence. Minified names, array positions, and local patches describe the version captured by the author, not a supported Google interface. The reported experiment has not been independently reproduced by SpideyData.

A score is an outcome, not an explanation. A useful record links it to the exact page, action, script version, session, and verification response. Otherwise a change in the environment can be mistaken for a change in the code being investigated. The original author also noted that one demo's quota condition made its repeated score unsuitable for evaluating progress.

Separate the request boundaries

Begin with ownership. The loader, the embedded runtime, and the application backend do different jobs. A Network panel groups them into one timeline, but that does not make every response part of the same contract. Keep the initiator and response body alongside each URL so that an application wrapper cannot be mistaken for a Google endpoint.

In this capture, api.js loads recaptcha__zh_cn.js and assigns an integrity attribute to the script element. The former is the loader; the latter contains the runtime under investigation. Editing that resource changes the experiment, so results from a modified copy must be distinguished from observations made against the unmodified page.

Script loader integrity attribute
Figure 1. Script loader integrity attribute.

Next comes initialization. Read the anchor request and returned document together: one records the inputs sent to the service, while the other records the context made available to the client. A value visible in the returned page is not automatically the final response token used by the application.

Anchor request parameters
Figure 2. Anchor request parameters.
Anchor response initialization
Figure 3. Anchor response initialization.

The reload capture gives the investigation a second boundary: the outgoing body and the returned value. Preserve both before trying to interpret their internal representation. A serialization hypothesis should eventually explain observed bytes, not only the shape of an object in the console.

Reload request body
Figure 4. Reload request body.
Reload response
Figure 5. Reload response.

The last pair shows a demo application's verify.php request and response. Google documents backend verification separately at /recaptcha/api/siteverify. Its response token is single-use and expires after two minutes; the application's wrapper is not itself proof of how that backend call was implemented. Keep those two boundaries distinct when reading a trace.

Demo verification submission
Figure 6. Demo verification submission.
Demo verification response
Figure 7. Demo verification response.

Inspect data before serialization

An XHR/fetch breakpoint is useful when the destination is known but the producer is not. Chrome DevTools can pause on a matching request URL; line breakpoints then narrow the relevant frame. In this trace, the important observation is the state immediately before the request body is constructed.

Paused request call stack
Figure 8. Paused request call stack.
Request array before encoding
Figure 9. Request array before encoding.

Here the visible boundary is new wU(p): p carries the structured input, while the resulting object contains a Uint8Array and the reload destination. Identifying that encoder does not explain the origin of every field. Encoding and collection are separate responsibilities, even when an obfuscated bundle puts them close together.

Serialized request object
Figure 10. Serialized request object.

When documenting a payload, give each field an evidence status. A literal, a value read from configuration, and a result returned by a computation deserve different labels. Record which observation supports the label and what is still unknown. An index seen in one build is a locator for that build, not a durable schema.

Find where the object becomes complete

The inspected p object is an instance of RV. Looking for its construction gives the investigation an earlier observation point than the network sender. The useful question becomes where the required fields first exist and which operation writes them, even when an asynchronous boundary has interrupted the visible call stack.

Payload object constructor
Figure 11. Payload object constructor.
Constructor call inspection
Figure 12. Constructor call inspection.

A before-and-after snapshot can expose a mutation that a single console view hides. For each candidate producer, compare the values on entry, the returned object, and the object seen by the consumer. Copy relevant primitive values at observation time; a reference inspected later may already contain additional mutations.

Caller return value
Figure 13. Caller return value.

The following inputs have distinct roles: Z is a four-element computed result, W.kT holds configuration, and z carries a broader runtime context. In W.kT, the screenshot exposes the site key, origin-related value, language, version, and homepage action. Keeping those roles separate helps avoid treating a successful call as proof that every input belongs to the same session.

Computed input array
Figure 14. Computed input array.
Configuration object
Figure 15. Configuration object.
Runtime context object
Figure 16. Runtime context object.

Treat a local runtime as an experiment

Moving a function out of a large bundle changes more than its location. Lexical bindings, initialization order, global objects, and message delivery may all have been supplied by the original environment. A local call is therefore a new experiment whose assumptions need to be explicit.

Bundle scope structure
Figure 17. Bundle scope structure.
Local invocation setup
Figure 18. Local invocation setup.

An environment stub can help isolate a dependency, but the existence of an API-shaped object does not establish browser-equivalent behavior. Message delivery and callbacks have timing semantics as well as method names. A useful harness records which capabilities are real, which are simulated, and which have not been exercised.

Local environment scaffolding
Figure 19. Local environment scaffolding.
Scope-dependent expression
Figure 20. Scope-dependent expression.

A local patch that removes an exception answers a narrow question: execution can advance past that point. It does not establish that the resulting payload is correct. Compare the resulting values with a captured baseline before interpreting the absence of an exception as progress at the protocol boundary.

Follow values across Promise boundaries

The scheduling expression in this build is QY.BA(z.eO.bind(z, new Bk(W.IB)), 2). Its continuation receives Z. The bind call fixes the receiver and an argument before the callback executes; inspecting only the continuation misses that captured state. Connect the scheduler's inputs to the resolved value, and keep a run identifier with each observation.

Asynchronous result consumer
Figure 21. Asynchronous result consumer.
Bound callback inspection
Figure 22. Bound callback inspection.

Promise.all introduces a join: it fulfills with values in input order, regardless of the order in which the individual operations complete. It rejects when an input rejects. Those semantics matter when inspecting an array assembled from asynchronous producers; the order of console messages is not the order of the final result.

Promise join point
Figure 23. Promise join point.

Observe each branch at its resolution boundary, then observe the joined result. Matching a value's shape is a starting hypothesis. Stronger evidence also connects the execution, the producer's inputs, and the exact value passed to the consumer.

First Promise result
Figure 24. First Promise result.
Second Promise result
Figure 25. Second Promise result.

The two captures below reconnect the intermediate data with the request object. That closes one local dependency path. It remains separate from server verification: a function returning an array and an application accepting a response are different observations.

Intermediate array assembly
Figure 26. Intermediate array assembly.
Local serialization output
Figure 27. Local serialization output.

Distinguish collection from transformation

The state snapshot contains a 79-slot array, with 52 populated entries reported in the source. Its entries are not all collected at once. Browser-state analysis therefore needs to distinguish what was observed, when it was filled in, and how it was represented; recovering that representation does not explain the remote risk model.

Collected state array
Figure 28. Collected state array.

The next captures show why observation time matters. A collection can exist before every producer has filled its entries. Record both its shape and the point in the lifecycle where that shape was observed; an empty slot may mean pending work rather than an absent capability.

State Promise producer
Figure 29. State Promise producer.
Partially populated array
Figure 30. Partially populated array.
Later array state
Figure 31. Later array state.

At a transformation boundary, compare input and output while holding unrelated conditions steady. Logpoints can record values without repeatedly pausing execution, which is useful when suspension itself changes timing. Preserve enough context to associate each record with a single call.

Transformation call site
Figure 32. Transformation call site.
Numeric intermediate result
Figure 33. Numeric intermediate result.

Repeated runs help distinguish constants from varying inputs. A value that stayed stable in a small sample should be labeled observed stable, not universally constant. If randomness or time is involved, capture it alongside the transformed data instead of treating every changed byte as a new algorithm.

Random-value branch
Figure 34. Random-value branch.
String transformation input
Figure 35. String transformation input.
Logged transformation result
Figure 36. Logged transformation result.
Collected log samples
Figure 37. Collected log samples.

The author describes a custom transformation combining hash-derived key material, cyclic XOR, a deterministic shuffle driven by an LCG, and Base64URL encoding. The final pair compares one encoded tuple with a recovered string. A stronger check would round-trip multiple fixed samples, including empty input and boundary lengths, while keeping byte encoding, integer behavior, and operation order explicit.

Encoded tuple sample
Figure 38. Encoded tuple sample.
Recovered string sample
Figure 39. Recovered string sample.

Keep the validation claim bounded

The original investigation ends with this execution output. It is retained as the author's evidence, not as a newly reproduced result. This field note does not establish a current success rate or show that another site, action, session, or script version would behave the same way.

Author's validation output
Figure 40. Author's validation output.

For an integration you operate, Google's documented verification result is the relevant application boundary: check token validity and the expected action, inspect the hostname, and make a risk decision appropriate to the protected operation. Google also notes that scores in staging or early deployments can differ from production. A score without its context is a weak basis for a general claim.

The reusable debugging method is to connect observations across boundaries. Preserve a clean baseline, identify the data consumer, locate its producers, join asynchronous observations by execution, and compare the final result with the actual application outcome. That produces an explanation a second engineer can assess, even after the bundle's minified names have changed.

Sources and further reading

Next step

Try the smallest path that fits your target

Start with the public Fetch guide, then add browser rendering, crawling, or extraction when the workflow needs it.