A scraping gate must cover the data routes

Ten requests through the reference Worker show where clearance is checked, which requests reach the origin, and why an image header does not admit an export.

5 min read
RSS

A protected page can fetch its data from an unprotected endpoint. Checking the HTML navigation alone leaves that endpoint outside the gate. Heretic's reference Cloudflare Worker instead checks every routed request unless its path appears in an explicit public set.

We tested that boundary in workerd. The experiment records both the response status and whether the gate contacted a synthetic origin. A redirect is useful evidence only if the protected request did not already reach the application.

Ten requests, with origin calls counted

The fixture uses Miniflare 5.20260811.1-alpha and Node 22.22.2. It loads the actual reference gate and clearance verifier, generates an ephemeral Ed25519 signer and supplies a synthetic public-key response. Every outbound fetch is intercepted. Nothing is sent to a production origin or Heretic's API.

The cases ran sequentially in one Worker instance on 17 October 2026. Counts below are per request, not cumulative. The address used for comparison is injected by the test.

Request Clearance Status Origin calls
GET /private absent 303 0
GET /api/data.json, destination image absent 303 0
GET /export.pdf, destination image absent 303 0
GET /private.css, destination image absent 303 0
POST /submit absent 403 0
GET /robots.txt absent 200 1
GET /api/data.json, destination image valid and admitted 200 1
GET /api/data.json valid signature, denied verdict 403 0
GET /api/data.json valid, different request address 403 0
GET /api/data.json unknown key ID, key endpoint unavailable 503 0

The image header and filename suffix never admit a request. The admitted API request is the control: the same data route can reach the origin when the gate's conditions hold. /robots.txt is a second control because the reference configuration deliberately makes it public.

Put the decision before the origin fetch

The relevant ordering starts with a small, explicit set:

js
const PUBLIC_PATHS = new Set([
  '/robots.txt',
  '/favicon.ico',
  '/.well-known/heretic-site-verification',
]);

For a path outside that set, the cookie branch verifies the clearance and then evaluates the customer's admission rule:

js
const checked = await verifyClearance(cookie, options);
if (checked.valid) {
  return allowed(checked.claims, request) ? fetch(request) : refuse();
}
if (checked.reason === 'jwks') return unavailable();

Those two conditions establish different facts. Verification authenticates the signed report under the configured contract. The reference allowed function compares the selected device address with the request address, then checks the verdict or ceremony outcome. A customer can replace that policy. Our denied-verdict case uses a correctly signed report and still receives 403.

When no usable clearance cookie is present, ordinary GET and HEAD requests enter the redirect flow. Other ordinary methods return 403. A verified cookie that fails the admission policy returns 403 directly, including for GET. Callback routes have their own validation branches. The tested POST body is never forwarded or replayed through a redirect. An application using background verification must complete that flow before submitting the protected operation again.

The key-fetch failure is distinct. We first exercise a known signing key, then send a token naming an unknown key and make the synthetic key endpoint return 503. That forces a lookup instead of merely reusing the cached known key. The gate returns 503 with zero origin calls. This case says nothing about whether already cached keys remain usable during an outage.

Destination metadata does not grant clearance

The Fetch Metadata specification defines Sec-Fetch-Dest as the request's intended destination. In a browser, an image request can carry image. That describes how the response is to be used; it is not an assertion that a Heretic check passed.

The Sec- prefix prevents ordinary browser script from setting these headers. Our fixture submits them directly to workerd. It therefore tests the server's admission behavior, not a browser-script bypass. The gate requires clearance regardless of the supplied destination.

The address comparison also has a deployment premise. This Worker reads CF-Connecting-IP in Cloudflare's request context. Cloudflare documents different behavior for Worker subrequests. Copying the comparison into a server that accepts an arbitrary client-supplied header would change that premise. This local test supplies the address deliberately and does not validate the production header path.

Route coverage remains a separate requirement

The experiment dispatches every request through the gate. It cannot prove that a deployed route configuration covers every API or export URL, or that an origin cannot be reached directly. Both must be checked in the deployment that uses it.

The useful assertion is narrower and testable: for a protected request handled by this gate, the origin receives no call until the clearance and admission checks succeed. Counting origin calls makes that assertion visible. A page that looks protected is not enough evidence that its data requests are protected too.

Reproduce the request matrix

Download the standalone fixture and recorded results, extract it, then run:

sh
cd scraping-gate
npm ci
node matrix.mjs

The lockfile pins the test dependencies. Installation may download packages; the test intercepts Worker outbound requests. The output records each status, origin-call count and key-fetch count. The ten rows repeated during separate acceptance on 18 October.

Next article A canvas hash can change without a readback hook An opaque control separates ordinary alpha conversion from a deliberate one-byte canvas readback alteration.

The probe runs on your own browser.

The public demo returns the verdict and its named findings.