TRENDING
Rows of red climbing helmets, carabiners and harness straps laid out on the ground at a ropes course
October 21, 2026
How to Build a Safe AI Agent Harness in Python With Tool Validation, a File Sandbox, and Run Limits
A thin transparent polymer test specimen stretched between the metal grips of a tensile testing machine
October 21, 2026
Ai2’s FairStress Turns Fairness Testing Into a Pressure Test, but the Released Files Do Not Match the Report
A vintage Banda Model Ten spirit duplicator, a hand-cranked paper copying machine, shown with its instruction book on a white background
October 21, 2026
An Anonymous Comment Can Hand Duplicator a Payload That Fires at Migration Time, and Under a Third of Free Installs Run the Fix
Colorful horizontal layers of sedimentary rock exposed in a cliff face at Makhtesh Ramon in Israel, with two desert shrubs in the foreground
October 21, 2026
How to Build an LSM-Tree Key-Value Store in Python With a Write-Ahead Log, SSTables, Bloom Filters, and Compaction
A broadcast studio teleprompter with script lines shown mirrored on its lower monitor and reflected in the glass in front of a camera lens
October 21, 2026
WhatsApp’s Leaked Chatbot Notes Turn a Help Page Into Two Documents, and the Hidden One Told the Bot Not to Recommend a Regulator
21 Oct 2026
SXZ.io SXZ.io
  • Home
Search the Site
Popular Searches:
Technology Amazon AI
Recent Posts
An out-of-order sign standing in an airport sink basin, with a title, a friendly explanation and a Spanish translation, like the members of an error response
How to Return RFC 9457 Problem Details From a FastAPI API and Stop Error Responses From Leaking Secrets
October 21, 2026
Heavily corroded cogwheels and a rusted ring gear inside an abandoned sulphur factory beside the sea on White Island
Cloudflare’s tsrs Turns an Agent-Written Compiler Port Into a Differential-Testing Bet, and the 8x Headline Depends on the Baseline
October 21, 2026
A black public warning siren with a wide dish-shaped horn on a short pole against a clear blue sky
PHP Warns of Two Critical Flaws and Sets a 14:00 UTC Patch, but the Leak, the Score and the Exploitation Reports Do Not Agree
October 21, 2026
SXZ.io SXZ.io
  • Home

Categories

Articles 255 Posts
News 258 Posts
Learning Hub 228 Posts
Home/Articles/Cloudflare’s Programmable Gateway Turns Security Policy Into Customer Code, and the Launch Samples Show What That Code Must Get Right
Articles

Cloudflare’s Programmable Gateway Turns Security Policy Into Customer Code, and the Launch Samples Show What That Code Must Get Right

Cloudflare’s new Forward action fails closed and skips WebSocket traffic. Running the launch post’s samples in workerd found a JWT role check that trusts unsigned tokens and an alert sample that...

October 16, 2026 13 Min Read
58

On October 15, Cloudflare put a new Zero Trust Gateway action called Forward into public beta on all Zero Trust plans. When a policy matches an HTTP request that Gateway has already permitted, the request goes to a Worker the customer wrote. That Worker can change the request, change the response, or answer in place of the origin. The launch post, by AJ Gerstenhaber and Abe Carryl, presents this as a way to retire hand-built proxy pools, and it adds Workers that validate data loss prevention (DLP) matches. A third feature, custom functions that feed policy conditions, is promised “before the end of the year.”

Table Of Content

  • What shipped, and what is still a promise
  • What the docs say that the post does not
  • The Forward action fails closed
  • Traffic the Worker never sees, and bytes DLP never rescans
  • The Workers Free allowance
  • Running the four launch samples
  • Sample 2: a role taken from an unsigned token
  • A replacement that checks the signature
  • Sample 3: an alert on a DLP match that the docs say cannot happen
  • Sample 4: a GitHub token validator that treats a 401 as proof a token is dead
  • Before you turn Forward on
  • What I tested and what I did not

A policy engine that runs customer code moves part of the security decision into software the customer now has to get right. This piece reads the post against Cloudflare’s own documentation and the published @cloudflare/zt-types package, then runs the post’s four code samples in workerd 1.20261016.1, the open-source Workers runtime. I called them over Workers RPC, which the docs say is how Gateway reaches the Forward entrypoint. What I found:

  • The Forward docs say failures “fail closed,” that a deleted Worker leaves matching requests returning HTTP 500, and that WebSocket and CONNECT traffic never reaches the Worker. The post does not mention any of that.
  • The JWT role check accepted tokens with no valid signature, returned only a user’s first group, garbled a non-ASCII group name and threw on a token whose payload carried a URL with a query string.
  • The sample that alerts on a DLP match matches no type in the published package, and the docs say a DLP match cannot select a Worker.
  • The GitHub token validator ran. A made-up token drew HTTP 401 from api.github.com and a veto. Going by GitHub’s docs, a working token from a different GitHub deployment would get the same answer.
  • A short replacement for the JWT sample that checks the signature rejected all eight bad tokens I tried and accepted both good ones.

What shipped, and what is still a promise

The post covers several entry points, and they are at different stages. The package is the only machine-readable contract for the shipped pieces, so it is the yardstick for the samples below.

Piece Status on October 16 Where that comes from
Forward action Public beta on all Zero Trust plans The Forward docs: “available in public beta on all Zero Trust plans”
DLP validation Workers The post says they are “available starting today”; the DLP docs say they appear in the entry editor “when this feature is available for your account” Launch post and DLP validation docs
Custom functions as policy selectors “Custom Functions will be available before the end of the year” Launch post; the package has no types for them
Types package @cloudflare/zt-types 2026.10.1, published October 15 at 08:47 UTC, about eight hours before the post npm registry

What the docs say that the post does not

The post’s “Performance and reliability” section says “dispatching to a Worker does not introduce a network hop” and describes one failure setting, for custom functions: “you configure the fallback behavior per function.” The Forward page is blunter about the action that actually shipped.

The Forward action fails closed

The docs say: “Invocation errors, Worker errors, or failures in communication between Gateway and the Worker fail closed. Gateway does not bypass the Worker and forward the original request.” Gateway “resolves the selected Worker only when a Forward policy is saved,” so a deletion has a sharp edge: “Deleting the Worker does not remove or disable the policy. Matching requests then fail with HTTP 500 and do not reach the origin.” Identity errors default the same way. If context.identity() or context.devicePosture() throws and the Worker does not catch it, “Gateway returns HTTP 500,” and the Worker’s catch block decides whether to continue.

Fail closed is the safer default for a security control, and Cloudflare deserves credit for documenting it. It also makes the Worker an availability dependency for every matched user. A bad deploy, a deleted Worker or an uncaught exception shows up as an outage on the covered hosts, not as a gap in enforcement.

Traffic the Worker never sees, and bytes DLP never rescans

The middle column quotes the docs. The third column is my reading of them.

Area What the docs say What it means for a control built on Forward
Protocols “WebSocket traffic connects directly to the original upstream service and bypasses the selected Worker.” Proxy CONNECT tunnels do the same. A justification page or audit trail built on Forward does not cover those connections.
Decryption “HTTPS traffic must be decrypted.” No Worker runs for traffic that matches Do Not Inspect. A TLS inspection exemption is also a Worker exemption, and the Worker plus every library it imports handles plaintext traffic.
Body inspection “Gateway inspects the original body before delivering it to the Worker.” But “Worker invocation does not confirm that body inspection succeeded,” and a body Block “can interrupt an already-running Worker.” A webhook call, an R2 write or an alert can fire for a request that inspection then blocks.
Worker edits “Data the Worker adds to a permitted request does not receive another request-side DLP scan.” Anything a Worker injects upstream is outside request-side DLP.
Selection “Body inspection results, DLP matches, and response selectors cannot select a Worker.” What DLP finds cannot trigger Forward. See sample 3.
Identity “Use Gateway-provided identity for user-aware logic. Client-supplied identity headers are not an authoritative substitute.” Reading a role from a request header goes against the docs. See sample 2.
Outbound calls “A normal Workers fetch() is an independent subrequest.” “Do not assume a fetch() call uses the user’s Gateway policies or egress IP.” Calls from the Worker to Slack, GitHub or a SIEM sit outside the user’s policies.

The Workers Free allowance

The post notes that “Workers Free does have execution time limits and daily request limits, which is worth considering as you scale the volume of Gateway traffic you send to your Workers.” Cloudflare’s Workers limits page puts a number on it: “Accounts on the Workers Free plan have a daily request limit of 100,000 requests, resetting at midnight UTC.” A Worker over the limit either returns Error 1027 or is bypassed, depending on a fail mode the page ties to routes. A Forward policy picks its Worker by ID, and the Forward docs say the Worker “does not need a public route,” so it is unclear which mode applies, and neither the Forward docs nor the post say. Cloudflare’s template docs warn that “Page loads produce multiple HTTP requests.” At 50 requests per page load, an assumption of mine rather than a Cloudflare figure, 100,000 requests cover 2,000 page loads for the whole account.

Running the four launch samples

I copied each sample from the post, removed the TypeScript annotations and left the logic alone. The post prints the GitHub host as api.GitHub.com; I used the lowercase host GitHub documents, since host names are case-insensitive. Gateway itself needs a Zero Trust account, enrolled devices and TLS decryption, so the table says what the sample code does, not what a live Gateway does.

Sample in the post Result in workerd Verdict
1. Forward middleware: add X-Gateway-Client-IP from context.srcIp, then set a response header Both header edits applied; the origin stand-in saw 203.0.113.7 Runs. It takes the source address from Gateway’s context, as the docs advise.
2. Custom function: a role from the JWT in the Authorization header Accepted tokens with no valid signature, returned the first group only, garbled a non-ASCII group name, threw on a query-string URL in a claim Not safe as written
3. “Real-time alerting on policy match” on a DLP profile TypeError on a context shaped like the documented one; workerd refused the two-argument RPC call Does not match the shipped contract
4. DLP validator that checks a GitHub token A made-up token drew HTTP 401 from GitHub, and the sample returned a veto Runs; three design questions

Sample 2: a role taken from an unsigned token

The post’s custom function, for a feature that has not shipped yet, reads the Authorization header, decodes the middle segment of the token and returns the first group. Its example rule matches that value against finance_contractor with an Allow action. Here is the class as printed, minus the types:

export class GatewayCustomSelector extends WorkerEntrypoint {
  async compute(ctx) {
    const authHeader = ctx.request.headers.get("Authorization");
    if (!authHeader) return "anonymous";
    const token = authHeader.replace("Bearer ", "");
    const payload = JSON.parse(atob(token.split(".")[1]));
    return payload.groups?.[0] ?? "unknown";
  }
}

It never checks a signature. Cloudflare’s context reference for the Forward action, which is the feature that shipped, says that “Client-supplied identity headers are not an authoritative substitute” for Gateway’s own identity. RFC 8725, the JWT best-current-practice document, goes further: “All cryptographic operations used in the JWT MUST be validated and the entire JWT MUST be rejected if any of them fail to validate.” This is what the sample returned:

Authorization header Sample returns
None anonymous
Well-formed token, groups finance_contractor, placeholder signature finance_contractor
Header alg set to none, empty signature, same claims finance_contractor
Signature AAAA, same claims finance_contractor
Groups everyone, then finance_contractor everyone
Group name Zürich-Finance Zürich-Finance
A picture claim holding a URL with a query string Throws InvalidCharacterError
Not a JWT (notajwt) Throws InvalidCharacterError

The middle rows are the security finding: a token with alg set to none and no signature gets the same role as any other. The application behind the gateway should still verify its own token, so what an attacker gains depends on what the Allow rule unlocks, but the Gateway decision itself rests on input anyone can forge.

The other rows are correctness bugs. atob decodes the standard Base64 alphabet, while RFC 7515 defines base64url using “the URL- and filename-safe character set defined in Section 5 of RFC 4648,” which swaps + and / for - and _. I encoded all 2,097,152 three-byte ASCII sequences: in the base64url form of ASCII text, a - or _ can appear only when the third byte of a three-byte group is >, ?, ~ or DEL. Tokens with plain ASCII claims therefore usually decode, and a claim holding a URL with a query string can break the sample, as the row above shows. I did not measure non-ASCII payloads for crashes. They do cause a quieter fault: atob hands back UTF-8 bytes as Latin-1 characters, which is how Zürich-Finance became Zürich-Finance and would never match a rule written with the real name.

A replacement that checks the signature

Cloudflare’s own context page avoids the problem: await context.identity() returns Gateway’s identity object, and the docs example wraps it in a try and catch that picks the status code. If a rule really needs claims from an application token, the function has to verify the token. The code below is my test code, not Cloudflare’s. It handles RS256 only and takes the public key as an argument; production code would fetch and cache the issuer’s key set and pick a key by kid.

// verify.js: a signature-checking replacement for the post's JWT decode (RS256 only).
export function b64uToBytes(s) {
  // JWT segments are base64url without padding (RFC 7515), so map to the standard alphabet and re-pad.
  s = s.replaceAll("-", "+").replaceAll("_", "/");
  s += "=".repeat((4 - (s.length % 4)) % 4);
  return Uint8Array.from(atob(s), (c) => c.charCodeAt(0));
}

export async function verifyJwt(token, jwk, { iss, aud, now = Date.now() / 1000 }) {
  const parts = token.split(".");
  if (parts.length !== 3) throw new Error("not a JWT");
  const header = JSON.parse(new TextDecoder().decode(b64uToBytes(parts[0])));
  // Never let the token choose the algorithm.
  if (header.alg !== "RS256") throw new Error("unexpected alg");
  const key = await crypto.subtle.importKey("jwk", jwk, { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, false, ["verify"]);
  const ok = await crypto.subtle.verify(
    "RSASSA-PKCS1-v1_5",
    key,
    b64uToBytes(parts[2]),
    new TextEncoder().encode(parts[0] + "." + parts[1]),
  );
  if (!ok) throw new Error("bad signature");
  const claims = JSON.parse(new TextDecoder().decode(b64uToBytes(parts[1])));
  if (claims.iss !== iss) throw new Error("wrong issuer");
  const audOk = Array.isArray(claims.aud) ? claims.aud.includes(aud) : claims.aud === aud;
  if (!audOk) throw new Error("wrong audience");
  if (typeof claims.exp !== "number" || claims.exp <= now) throw new Error("expired");
  return claims;
}
Token verifyJwt result
Signed with the right key; groups everyone, Zürich-Finance, finance_contractor Returns all three groups, name intact
Same, with a query-string URL in a claim so the payload contains _ Returns all three groups
Header alg set to none Rejected: unexpected alg
Header alg set to HS256 Rejected: unexpected alg
Signature AAAA Rejected: bad signature
Signed by a different RSA key Rejected: bad signature
Payload swapped after signing (groups admins) Rejected: bad signature
Expired Rejected: expired
Wrong audience Rejected: wrong audience
Wrong issuer Rejected: wrong issuer

It follows RFC 8725’s rule that “Libraries MUST enable the caller to specify a supported set of algorithms and MUST NOT use any other algorithms”: the token does not get to choose the algorithm. It also returns every group, not the first.

Sample 3: an alert on a DLP match that the docs say cannot happen

The post’s example for “Real-time alerting on policy match” says: “A Worker bound to a Forward policy receives traffic whenever a data loss prevention (DLP) profile matches.” Its code is a default export with an invoke(ctx, env) method that reads ctx.identity.email, ctx.rule.name and ctx.request.url. Against the shipped contract:

What the sample uses What the docs and package say
A default export with invoke(ctx, env) Gateway invokes “its GatewayMiddleware entrypoint, rather than a default fetch() handler,” as handle(request, context, next).
ctx.identity.email identity is a method, called as await context.identity(). Read as a property it is a function, so .email is undefined.
ctx.rule.name The context type has no rule field, only policyId.
ctx.request.url The context type has no request field. The request is the first argument to handle.
The type GatewayContext Not exported by @cloudflare/zt-types 2026.10.1.
A DLP profile match as the trigger “Body inspection results, DLP matches, and response selectors cannot select a Worker,” and “Context does not include custom function results, DLP match details, or raw internal device records.”

Run with a context shaped like the documented one, the sample throws TypeError: Cannot read properties of undefined (reading 'name'). Workerd also refused it as an RPC target, because a top-level handler function takes exactly one argument over RPC and a second one needs class syntax. Cloudflare’s own Slack template does something narrower that fits the documented entrypoint: it sends a background message “when a request to a selected application endpoint completes.” The post may be sketching what custom functions will do later. If so it should say so, because a team that copies the sample today gets a Worker that Forward would not invoke as written.

Sample 4: a GitHub token validator that treats a 401 as proof a token is dead

The sample decodes the excerpt Gateway passes, sends the matched string to https://api.github.com/user as a bearer token, and returns a score of 1.0 for a successful response, a veto for a 401 and 0 for anything else. The DLP docs define the outcomes: “A score of 0 leaves confidence unchanged,” and "veto" rejects the match. I ran it on a made-up string shaped like a classic personal access token, ghp_ plus 36 characters. GitHub answered 401 with “Bad credentials,” and the sample returned a veto. Three questions follow from the docs rather than from the run:

  • A 401 says only that the token is not valid at that host. GitHub’s Enterprise Server documentation lists the same token prefixes (ghp_, github_pat_, gho_, ghu_, ghs_), and its REST API guide puts the API at http(s)://HOSTNAME/api/v3. A working token from an Enterprise Server instance is unknown to api.github.com, so it looks like my made-up string and gets vetoed. The leak the profile exists to catch is then dropped. I had no Enterprise Server token to test, so this is reasoning from the docs.
  • The endpoint accepts some tokens and not others. GitHub’s users page lists GitHub App user access tokens and fine-grained personal access tokens as the supported fine-grained types for “Get the authenticated user.” It does not list installation access tokens, which the “Get a user using their ID” and “List users” entries on the same page do list. Such a token should land in the neutral 0 case rather than earn the boost. I could not test that without a live installation token.
  • The validator uses the secret it found. It authenticates to a third party with a credential taken from employee traffic, and that credential may belong to a partner or customer. Whether a DLP system should do that is a decision for legal and security teams. The post does not raise it.

Two smaller points. The sample has no try and catch, so a GitHub outage becomes a Worker error, and the outcome then depends on the “default behavior for Worker errors or timeouts” that the DLP docs let you choose: “No fallback,” “Veto,” or “Score.” A Veto fallback would silently discard matches while GitHub is unreachable. And the package types describe start as the “Start index of the DLP match in” the excerpt, which is Base64 text, while the sample treats it as a byte offset into the decoded bytes. The docs I read do not state the unit, so test with multibyte text before relying on it.

Before you turn Forward on

  1. Treat the Worker as production code on the request path. It fails closed, so start with a test host and user group, catch errors around identity() and devicePosture() and choose the status code yourself, and remove policies before you delete a Worker.
  2. Read identity from context, not headers. Use await context.identity() and the posture list. Trusting a header is what the docs warn against.
  3. If you must read a token, verify it. Check the signature with an algorithm you choose, then the issuer, audience and expiry, using real base64url decoding. Return every group.
  4. Do not make Forward your only control. WebSocket, CONNECT, Do Not Inspect traffic and protocol upgrades skip the Worker, and what the Worker adds skips request-side DLP.
  5. Put side effects after the decision, and keep captured data small. A Worker can start before body inspection finishes. Cloudflare’s templates say to “Capture only data your organization permits you to store,” while the post suggests writing a downloaded file “in its entirety” to R2 “as a permanent record.”
  6. Ask Cloudflare which fail mode applies at 100,000 daily requests, or run Forward on a paid plan.
  7. For DLP validators, do not read a 401 as proof that a token is dead. Cover every deployment that issues the token type, pick the error fallback on purpose and test the excerpt offsets with multibyte context.

Trusting what a caller supplies is a recurring theme. Our look at Cloudflare Traces covered a setting that decides whether the edge joins a caller’s trace, our tutorial on building and verifying JWTs walks through the alg=none forgery and an algorithm allowlist, and the WSO2 API Manager attacks show forged tokens used in the wild.

What I tested and what I did not

I read the launch post, the Forward, context, worker-template and DLP validation docs and the Workers limits page on October 16, 2026, and downloaded @cloudflare/zt-types 2026.10.1 from npm to read its type definitions. I ran the samples in workerd 1.20261016.1 on Windows, using the npm release of the standalone binary, with one test worker calling the post’s classes over service bindings. The GitHub call went to the live API with a made-up token. The ASCII check encoded every three-byte ASCII sequence. The replacement function ran with an RSA-2048 key pair generated inside workerd.

I did not use a Cloudflare account, so I did not run Gateway, a Forward policy, TLS decryption, a DLP profile or a Free-plan limit. Custom functions have not shipped, so the JWT sample was tested as plain code only. I had no Enterprise Server or GitHub App installation token, so those two GitHub points rest on GitHub’s documentation. Whether Gateway hands a Worker the same kind of request and response objects as my stand-ins is untested; the first sample’s header edits worked in my setup. The validator ran against github.com only.

Tags:

Application SecurityCloudflareCloudflare WorkersData Loss PreventionJWTZero Trust

Share

Wooden portcullis with iron spikes raised inside a stone gateway arch at Cahir Castle in Ireland
Previous Post

A Suspected China-Nexus Group Used a BeyondTrust Remote Support Zero-Day in August, and Patching Will Not Undo a Compromise

A pile of small stainless steel coil springs lying on a white background
Next Post

How to Shrink Small JSON Records in Python With Zstandard Dictionaries

No Comment! Be the first one.

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

Latest
21 Oct
How to Return RFC 9457 Problem Details From a FastAPI API and Stop Error Responses From Leaking Secrets
21 Oct
Cloudflare’s tsrs Turns an Agent-Written Compiler Port Into a Differential-Testing Bet, and the 8x Headline Depends on the Baseline
Trending
October 21, 2026
How to Return RFC 9457 Problem Details From a FastAPI API and Stop Error Responses From Leaking Secrets
October 21, 2026
Cloudflare’s tsrs Turns an Agent-Written Compiler Port Into a Differential-Testing Bet, and the 8x Headline Depends on the Baseline
October 21, 2026
PHP Warns of Two Critical Flaws and Sets a 14:00 UTC Patch, but the Leak, the Score and the Exploitation Reports Do Not Agree
October 20, 2026
How to Build a Safe AI Agent Harness in Python With Tool Validation, a File Sandbox, and Run Limits
October 20, 2026
Ai2’s FairStress Turns Fairness Testing Into a Pressure Test, but the Released Files Do Not Match the Report
October 20, 2026
An Anonymous Comment Can Hand Duplicator a Payload That Fires at Migration Time, and Under a Third of Free Installs Run the Fix

Related Posts

Blue-lit server racks in a modern data center, illustrating the compute infrastructure behind the AI boom.
Articles

The AI Boom Is Spending Real Money Before Proving Real Returns

June 7, 2026
Technician working with a laptop beside server racks, representing enterprise AI retrieval infrastructure
Articles

Google’s Agentic RAG Push Makes Enterprise AI Less of a One-Shot Guess

June 7, 2026
A person with a laptop and smartphone, representing digital attention and AI-assisted work
Articles

AI Chatbots Are Making Attention a Design Problem

June 7, 2026
A customer-support representative wearing a headset against a dark studio background.
Articles

The Meta AI Support Hack Was a Plain Old Authorization Failure

June 7, 2026
SXZ.io SXZ.io
  • [email protected]

Categories

Articles
Learning Hub
News

All Rights Reserved by SXZ.io ©2026