Skip to content

URL parser, policy-path, tenant-object, and OAuth redirect boundaries

A late July 30 advisory wave exposes four reusable operator patterns: an SSRF validator and fetcher can disagree about URL authority; an application-layer policy and backend can normalize paths differently; object IDs can cross tenant or channel scope; and OAuth redirect matching can confuse a hostname suffix with a subdomain boundary.

Sources:

Authorized fixtures only

Use owned callback domains, local canary HTTP services, synthetic tenant records, disposable OAuth clients, and a lab Calico workload. Never target metadata services, internal production hosts, real agent executions, customer inventory/assets, victim authorization codes, or shared cluster policy.

One decision matrix

Boundary Trusted decision input Actual sink input Bounded positive
SSRF URL sanitized or initially resolved authority original URL, redirect destination, or later DNS answer owned final listener receives the canary
HTTP policy raw path matched by prefix rule backend-normalized path permitted wire path reaches denied canary route
tenant object authenticated tenant plus supplied ID global execution/entity lookup foreign synthetic marker is read or changed
OAuth redirect suffix-matched hostname browser's actual redirect authority code-shaped canary reaches an owned non-client host

Preserve validation, parsing, DNS, connection, redirect, policy, backend normalization, object lookup, and authorization as separate edges. Do not infer final-destination access from a validator return value or cross-tenant impact from a globally formatted ID.

Validator-to-fetcher authority differential

  1. Put a harmless marker service on an isolated address and an owned public listener on another address. Instrument both; return no credentials or environment data.
  2. Pass the same candidate through the exact validator and HTTP client used by the application. Record the raw string, validator-normalized string, parsed username/password/hostname/port, DNS answer set, connected peer, redirect chain, and final listener.
  3. Build a corpus around authority grammar rather than one payload: userinfo delimiters, percent-encoded delimiters, bracketed hosts, trailing dots, mixed-case names, multiple DNS answers, redirects, and rebinding between validation and connection.
  4. For dssrf, specifically compare the raw URL parsed by the fetcher with the string produced after @ removal. A positive requires that validation approves one authority while the unchanged fetch input reaches the owned canary authority.
  5. For Swarms image/audio loaders, resolve only owned hostnames. Test whether each address is classified, whether the connected address is pinned to the approved result, and whether every redirect is revalidated.
  6. Repeat against dssrf 1.0.4 or the relevant corrected Swarms commit with identical fixtures.

Report validator authority A -> fetcher authority B -> owned final-destination receipt. Never use cloud metadata or a production internal service as proof.

Calico policy-to-backend path differential

The affected application-layer policy is disabled by default. First confirm that Dikastes-backed HTTP rules are actually enabled; ordinary Kubernetes NetworkPolicy is not enough.

  1. Deploy a disposable workload with /allowed/canary and /denied/canary; both increment separate no-op counters.
  2. Apply a Prefix policy permitting only /allowed/. Put a raw-byte recorder before the backend if the lab architecture permits it.
  3. Send one path mutation at a time: dot segments, encoded slash forms accepted by the stack, and repeated slashes. Capture the wire target, policy decision, proxy-forwarded target, backend-decoded path, selected route, and counter delta.
  4. Use negative controls for a denied canonical path, a permitted canonical path, no policy, a backend that does not normalize the candidate, and corrected Calico builds.
  5. A positive is Dikastes permits the wire path under the allowed prefix -> downstream normalization selects /denied/canary -> only the denied counter changes.

Do not test destructive endpoints. This is a parser differential, not proof that every ingress, service mesh, or backend normalizes identically.

Tenant and channel object-ID substitution

  1. Create tenants/channels A and B, separate low-privilege users, and records containing random markers only.
  2. Establish controls: A can access A's execution/asset/stock location; A is denied B's object through intended UI and list routes.
  3. From A, substitute B's ID only in Julep execution-detail requests and Vendure asset or stock-location update requests. For Vendure, change a harmless marker field and immediately restore it.
  4. Compare read, update, list, and delete independently. Capture actor tenant/channel, route, object owner, supplied ID, status, returned marker label, and before/after hash.
  5. Repeat with nonexistent IDs, same-tenant IDs, an administrator, and corrected builds.

Strong evidence is valid low-privilege session + foreign global ID -> backend object lookup succeeds without binding object scope to actor scope -> synthetic disclosure or mutation. Do not collect prompts, task outputs, temporal tokens, catalog media, or inventory from real tenants.

OAuth redirect hostname-boundary checks

  1. Register https://client.example.test/callback for a disposable MaxKey client and operate a separate suffix-confusion host such as notclient.example.test only if both names are owned lab domains.
  2. Generate authorization requests that vary exact host, true subdomain, lookalike suffix, port, scheme, path, case, trailing dot, and IDNA form one dimension at a time.
  3. Stop before real authorization. Use a synthetic user and a code-shaped marker that the owned listener records but cannot exchange for production access.
  4. Record registered URI, supplied URI, parser-derived hostname, match result, browser final authority, and whether the fixed build rejects it.
  5. A hostname suffix is acceptable only when the implementation intentionally permits subdomains and verifies candidate == registered or candidate.endsWith("." + registered) at a canonical DNS-label boundary. Path and port policy remain separate.

Report registered host suffix comparison -> attacker-owned lookalike host accepted -> synthetic authorization response delivered to that host. Redact all actual codes and tokens.

July 30 Kanboard numeric-IPv4 follow-up

GHSA-rj28-q96f-28vw / CVE-2026-57862 adds a concrete parser differential to this methodology. Kanboard 1.2.52 and earlier reportedly passed a user-controlled web-link URL through FILTER_VALIDATE_IP; hexadecimal IPv4 was rejected as an IP literal by that validator and therefore treated as safe, while cURL still resolved it as an address.

  1. Use a lab Kanboard user, an owned HTTP canary on an isolated private test address, and no route to production networks or metadata services.
  2. Submit the same canary destination as canonical dotted decimal, hexadecimal IPv4, decimal integer, octal-like form, mixed-base form if the exact cURL build accepts it, and an owned hostname. Change one representation at a time.
  3. Record raw URL, PHP validator result, parsed hostname, cURL version, DNS use, connected peer, redirect chain, and final canary receipt.
  4. Compare direct client behavior, application behavior, a public owned address, malformed numeric forms, and a corrected implementation that converts every accepted representation to a canonical address before policy.
  5. A positive is validator classifies alternate numeric host as non-IP/safe -> cURL connects to the same blocked-class lab address -> owned canary records the request.

Do not use localhost, RFC1918 production services, cloud metadata, or any internal service as proof.

Evidence checklist

Preserve exact package/product versions, enabled feature flags, raw and normalized inputs, parser outputs, DNS and connected-peer evidence, route/policy decisions, synthetic ownership tables, and fixed-version differentials. Keep claims narrow: parser disagreement, final-destination reachability, policy bypass, foreign-object access, and redirect delivery are distinct findings.

October 7 follow-up: Actual Sync Server CORS-proxy GitHub allowlist prefix bypass — same-owner sibling repos read with the server token (CVE-2026-57449 / GHSA-m62c-5q34-f3cf, high)

Actual Sync Server's GET /cors-proxy?url=... fetcher is confined to an allowlist of official plugin repositories, but the api.github.com branch of isUrlAllowed() tests url.pathname.startsWith('/repos/{owner}/{repo}') with no path-segment boundary after the repo name (the generic branch requires repoUrl + '/'; the API branch drops it). An allowlisted public repo acme/plugin therefore authorizes /repos/acme/plugin-private/contents/.env, /repos/acme/plugin-secrets/actions/secrets, /repos/acme/plugin-internal/releases — and the proxy then attaches the server's ACTUAL_GITHUB_TOKEN, so any authenticated Actual user reads private same-owner repos through server authority. Classic confused-deputy proxy: the credential belongs to the server, the allowlist belongs to the policy, and the prefix test silently widens one into the other.

Durable axes (extends this page's Calico prefix-rule and MaxKey suffix-boundary legs):

  1. Prefix/suffix allowlists must be tested for a boundary character, per parser branch. The bug's sharpest tell: the same function had a correct + '/' check on one branch and a bare startsWith on the API branch. When auditing any allowlist matcher, enumerate its alternation branches separately — a fix/correctness applied to one branch proves nothing about siblings. Probe family: for allowlisted entry X, request X + -marker, X + _marker, X%2F-encoded, and X/ proper; the -marker variant returning 200-with-data while X/../foreign returns 403 is the boundary-missing fingerprint.
  2. Target naming-convention clusters, not just the allowlisted object. Enumerate the allowlist source (here a public plugins.json), then enumerate the same owner's repo namespace (GitHub search/API, or username-guessing) looking for prefix siblings with secrets-flavored names (-private, -internal, -secrets, -infra). The server token's ACL, not the allowlist, defines the true read scope — check what the token can actually see by testing a canary private sibling you control in a lab fork.
  3. Self-hosted sync-server instances are the recon target, not app.hexagon.cn. Feature-flag fingerprint: the route only exists with ACTUAL_CORS_PROXY_ENABLED=true, and GitHub-token behavior shows up as a distinctive User-Agent (Actual-Budget-Plugin-System) if you control a URL the proxy visits. On authorized engagements, GET /cors-proxy?url= reachability + an owned-callback canary proves the proxy is live before testing allowlist shape.
  4. Proof discipline: use a disposable org with canary-plugin (public, allowlisted in a lab plugins.json) + canary-plugin-private holding a synthetic marker file; prove the read with the lab token only. Never pull real vendor/customer private-repo contents or production tokens into evidence.

October 8 06:31Z follow-up: Heimdall's SSRF guard exists but only guards one controller (CVE-2026-107449 / GHSA-hrj5-rw8j-34qj, linuxserver Heimdall ≤2.8.3)

Heimdall ships a SafeUrlFetcher with IP-address restrictions — applied only to ItemController. The enhanced-application test and live-stats flows (POST /test_config, GET /get_stats) fetch through SupportedApps::execute(), a plain GuzzleHttp client with no IP restrictions, reachable (via CSRF) by an unauthenticated attacker → server-side requests to arbitrary internal hosts/ports including 169.254.169.254, with a status/port oracle plus partial response data.

  • Durable axis, sharpened for dashboards/apps with an "app catalogue": a security control's existence proves nothing about its coverage — map guard-to-caller, not guard existence. The out-of-band catalog-integration feature (each supported-app template defines its own health/test URL shape) is the classic second fetch path: when a product adds a safe fetcher for the user-facing CRUD, every other outbound caller — plugin/app-integration executors, stats pollers, webhooks, update checkers, preview renderers — needs the same guard, and routinely doesn't. Fingerprint rule: on any self-hosted dashboard/monitoring/product-catalog app, enumerate feature-test/live-stats/webhook endpoints (the catalog templates name them) and drive them at loopback and link-local ranges; a status-code/port oracle from an unauthenticated CSRFable route is a finding even without full-body read.
  • Same wave tracked without publication: SecuShare Pro unauthenticated OS command injection (CVE-2026-107459 / GHSA-4jx8-6ccx-h2wp, critical — detail-free single, cmdi canonical), Stump ≤0.1.10 GraphQL smartlist mutation authorization (GHSA-2w7f-m7xm-pw66, missing-authorization canonical), Wallet System for WooCommerce trio / SMS Alert / Track Orders / Appointment Booking trio / Ultimate Multisite / GPTranslate / Final Tiles Grid pair / Frontend Dashboard / EDD block / Database Addon for WPForms / BackWPup capability-drift legs — all missing-ownership/capability/sanitizer canonicals per precedent (BackWPup's topology-dependent legs promoted on the Sept 19 WP alternate-surface page).

October 8 18:3xZ follow-up: FFmpeg RTSP 3xx redirect escapes -protocol_whitelist — the demuxer's redirect hop is a second fetch with the guard left behind (CVE-2026-107698 / GHSA-jhxg-38qw-fjxv, medium, fixed 7.1.4 / 8.0.2)

ff_rtsp_connect() follows RTSP 3xx Location: redirects without validating the target URL against the configured protocol whitelist — a malicious RTSP server redirects the client to internal hosts and ports under other schemes, defeating the one control operators believe constrains media-tool fetches. Durable axes for media-pipeline SSRF:

  1. Guards that consume the input URL are per-request-hop, not per-session. Same check-vs-connect and validation-on-first-URL-only genealogy as the Crawlee sitemap leg, the 9router prefetch TOCTOU, and the Malcolm FilePond FOLLOWLOCATION leg folded the same day: when any fetch path enables redirect following, the whitelist/allowlist/scheme check that ran on URL #1 says nothing about URLs #2..N. Probe battery on every media/preview/crawler fetch surface: owned redirector that 3xx's from an allowed scheme/port to a disallowed scheme (rtsp: → http://169.254.169.254/, file:-class shapes, internal port), then diff whitelist enforcement first-hop vs redirect-hop.
  2. Protocol whitelist is the media-stack equivalent of a scheme allowlist — test the redirect leg first. Fingerprint: any service that hands a user URL to FFmpeg/avconv (thumbnailers, streaming ingest, forensics viewers, the Malcolm/Arkime ecosystem itself) is reachable to internal ports through a hostile upstream media server, no vulnerable codec needed.
  3. Proof stays lab-bounded: your own RTSP responder emitting a crafted 3xx, your own callback listener as the redirect target; never pivot into unowned internal ranges.

October 11 follow-up: Miniflux proxy_url is an operator-configured SSRF primitive, and Shepherd re-resolves DNS after the guard checked it

Two more legs for the validator-to-fetcher differential theme on this page:

  • Miniflux 2.3.0-2.3.3 (GHSA-f5cv-wgfv-h2x2 / CVE-2026-108735, medium): the per-feed proxy_url setting accepts loopback/internal addresses with no validation, and the fetcher then routes every feed request through it — FETCHER_ALLOW_PRIVATE_NETWORKS=false guards the feed URL, not the proxy hop. An authenticated user points proxy_url at http://127.0.0.1:PORT and reads responses through feed-content parsing: internal port probing and proxy-style requests to services the fetcher can reach. Rule: SSRF guards must cover every URL field in the request path, including indirection fields (proxy, webhook relay, redirect-target storage). Enumerate all URL-shaped settings on any fetcher product (feed readers, webhooks, importers, previewers) and probe each with an owned callback before touching the headline URL parameter — the guard is usually written for the obvious field only. This is the Aug 4 Outerbase link-preview and Oct 8 Heimdall guard-coverage genealogy in configuration form.
  • Shepherd (shepherd-ai) up to 0.3.1 (GHSA-53wh-3r96-488v / CVE-2026-108741, low, but the shape is canonical): the citation-checker's public_url guard validates a resolved address, then the actual fetch re-resolves the hostname at connect time — a planted reference URL under attacker DNS with short TTL rebinds from public to internal between check and connect, and the internal response is captured into evidence files. This is the validate-then-re-resolve (DNS-rebinding) leg the page's validator-to-fetcher authority differential always implied: resolution at guard time is not resolution at fetch time. Operator checklist for any "safe URL" validator: does it (a) resolve once and connect to the pinned IP (correct — Langflow's validate_and_resolve_url shape), (b) validate a resolved IP but re-resolve later (broken — this leg), or (c) validate only syntax (broken — most)? Test (b) with an owned domain, TTL-1 record flipped to an owned listener between check and fetch.

October 12 follow-up: Appwrite screenshots — a headless-browser fetcher with an attacker-controlled headers param, and a metadata-header deny-list as the surviving control (CVE-2026-108770 / GHSA-cfw6-h8vp-h745, ≤2.3.0, fixed 2.4.0, fix commit 393255e)

Appwrite's Avatars module ships three user-URL fetchers — /avatars/image, /avatars/favicon, and /avatars/screenshot, the last rendering via a headless browser — and pre-fix the url param was validated with a bare syntax-only URL(['http','https']) validator (checklist case (c)): unauthenticated-adjacent (key-authenticated) server-side fetch of any internal URL, with the screenshot route returning the rendered page back as a PNG. Sharper than plain SSRF: the same route accepts a headers assoc param that is forwarded verbatim on the browser request — a credentialed-request relay ({"Authorization":"Bearer …"} toward internal APIs), not just a port oracle.

The fix diff is the operator intel here:

  1. The vendor's own comment concedes the rebinding leg survives. New BLOCKED_HEADERS docblock: "The browser resolves the host itself, so a rebound DNS answer can point the target origin at an internal address." The new PublicURL validator (public-suffix-list Domain::isKnown() + PublicHostname IP check) resolves at guard time, but the headless browser re-resolves at fetch time — so on patched 2.4.0+ instances the remaining attack path is the Shepherd case-(b) shape: owned TTL-1 domain, flip to an internal target between validation and browser navigation. The only mitigation left for metadata endpoints is the header deny-list, not address confinement. Probe on authorized targets: TTL-1 owned domain → 169.254.169.254; response renders as image, so read the page body optically (PNG of metadata JSON is still a read).
  2. A deny-list is a fingerprint of the vendor's threat model — enumerate past it. BLOCKED_HEADERS = host, metadata, metadata-flavor, x-google-metadata-request, x-aws-ec2-metadata-token, x-aws-ec2-metadata-token-ttl-seconds, x-aliyun-ecs-metadata-token. Grep any product for a metadata-header block list, then test the clouds it omits (OCI/Tencent/vOpenStack token headers, provider path variants that need no token header at all such as the plain link-local IMDSv1 GET or /latest/meta-data/ shapes that need neither). Case-sensitivity of the matcher is the second probe: HTTP header names are case-insensitive, so if the deny-list compares raw strings, X-AWS-EC2-Metadata-Token sails through — build the decision table (exact/case-folded/normalized) before crafting.
  3. Sibling-route parity was the original bug class: all three avatars endpoints (image, favicon, screenshot) shared the syntax-only validator, and the fix had to move the public-domain + public-address checks into a shared PublicURL param validator — the page's guard-to-caller mapping rule in reverse. When you find one user-URL fetcher in a product, sweep every route that takes a URL-shaped param; the render-engine differences (curl vs headless browser vs icon parser) change the response oracle (status → partial body → full rendered screenshot), not the reachability.
  4. Fingerprint/evidence notes: patch-level tell is whether url rejects a literal-IP or unknown-TLD host with the validator's custom reason string ("Hostname 'x' is not a known public domain.") — that error text confirms 2.4.0+ and the rebind-dependent residual; the fix also moved the browser service onto its own docker network, so egress from the browser container to the host/other containers may differ from the PHP app's — test both surfaces' reachability separately with owned callbacks. Proof stays lab-bounded: owned redirector/DNS, synthetic bearer canaries, never real IMDS or sibling tenants.

October 12 follow-up: ShareFile Storage Zones Controller — queue-worker leg chaining to unauthenticated SSRF against local handlers, and remote file IDs that escape the temp-dir confinement (CVE-2026-93693/-93694/-93695, v5.12.6 and below)

Progress's September 2026 ShareFile bulletin publishes three SZC legs that read as one operator playbook for asymmetric file-copy worker planes — the pattern where a web front-end enqueues work for a separate background service that trusts the queue's contents:

  • Unauthenticated chain to local-handler SSRF + queue operations (GHSA-474p-wmhw-pxfq / CVE-2026-93693, medium): vendor wording is "improper validation of outbound request destinations and parameters, unsafe JSONP response formatting, and insufficient validation on queue service can be chained by a remote unauthenticated attacker to issue server-side requests to unintended local handlers and perform unauthorized queue operations." Each leg alone rates low/medium; the vendor itself describes the chain as unauthenticated. The JSONP leg is the tell: a callback-name parameter reflected unvalidated into a JSONP wrapper is a DOM sink and often a URL-shape passthrough into the outbound request builder — enumerate JSONP callback=-shaped params on any file-sync product, they are frequently wired into the worker control plane rather than just the response.
  • Callback-URL ack + race selects whose metadata the copy worker consumes (GHSA-28j9-4grr-cjxp / CVE-2026-93694, medium): queue-item processing sends acknowledgements to an unvalidated callback URL, and combined with a race the attacker gets FileCopyService to consume attacker-controlled metadata/content. Two axes: (a) queue items carry server-trusted metadata (destination, identifiers) that an ack/callback field lets an outsider steer — the queue is a trust boundary only if every item-adjacent URL field is validated; (b) race-gated exploitation is still exploitation — plan for repeated-attempt automation and idempotent canaries when a prerequisite is timing, don't write it off as theoretical.
  • Remote file identifier splices into a local temp path without containment (GHSA-3hhm-qff2-f56h / CVE-2026-93695, high): FileCopyService builds a local temporary file path from a remote file identifier with no confinement, so attacker-controlled content lands anywhere the service account can write — RCE when an IIS-served directory is writable. This is validation-target-vs-write-target mismatch again (the October 8 Bricksforge temporaryFileUploads parameter-pair rule): the identifier is validated as a remote ID but consumed as a local path component. Sweep rule for any sync/copy/upload worker: for every remote-ID field, trace to the local path construction and check containment at the SINK; then map which web-served directories the service account's token can write (icacls/permission walk in lab; on authorized engagements, passive ACL enumeration of the app pool identity's write set is a fast win condition check before attempting anything).
  • Engagement framing: SZC is self-hosted on customer premises (IIS/Windows), so it shows up in enterprise-perimeter scopes. Patch-band fingerprint: ≤5.12.6 vulnerable, bulletin fixes 5.x line. Proof stays lab-bounded — owned callback URLs, synthetic file IDs into a disposable writable temp dir outside IIS roots, never real tenant file copies.

October 12 21:3xZ follow-up: redirect-hop revalidation and silent TLS-absent fail-open — SPIP's ten-hop syndication fetch, openstack-network-exporter's credentialless metrics, and Quay's webhook hostname deny-list (CVE-2026-109378 / GHSA-8gmq-ghhj-j4rh, CVE-2026-83535 / GHSA-45q5-ghf8-56pr, CVE-2026-85471 / GHSA-f2g8-c8v2-qmmr)

Three legs, all variations on guards that stop applying after the first hop or when a companion control is missing:

  • SPIP <4.4.18 recuperer_url() follows up to 10 redirects without re-validating targets (CVE-2026-109378, 4.3, ecrire/inc/distant.php): an authenticated author supplies a remote URL (syndicated feed, remote document); valider_url_distante() checks hop #1, then the fetch follows redirects to internal/link-local/cloud-metadata destinations. Same axis as this page's FFmpeg RTSP Location leg and the October 9 Open WebUI loader — guards consume hop 1; the final peer is what matters. Operator note: SPIP remains a large installed base of editorial/admin panels (/ecrire/), and the ten-hop budget means a redirect chain you own can also be used to cross network boundaries that a single hop could not; prove with owned redirectors, lab instances only.
  • openstack-network-exporter silently bypasses basic auth when TLS is omitted (CVE-2026-83535, 7.4): operator configures credentials but no TLS ⇒ basic-auth enforcement is skipped, no error, no warning. An adjacent attacker reads OVS/OVN dataplane metrics: bridge names, port counters, OVN controller state, host topology. Two rules: (a) auth-that-depends-on-transport-is a fail-open pair — on any exporter/telemetry endpoint, test credentials over plaintext and over TLS and compare; a 200 without credentials is the finding; (b) dataplane topology is a recon prize — OVN/OVS bridge+port inventories map tenant networks, VIFs, and host placement for lateral planning. Fingerprint: prometheus-style /metrics on exporter-specific ports (this exporter's own README band), then one unauthenticated GET.
  • Quay repository-admin webhook hostname blacklist bypass (CVE-2026-85471, 6.4): the webhook notification destination filter is a deny-list, so a spelling/normalization variant of a blocked host reaches internal services and cloud metadata. Deny-lists on URL fields have been this page's standing rule since the Flowise 38-regex leg; add the concrete battery for host deny-lists: decimal/hex IP forms, DNS-rebind names, IPv6 bracket forms, trailing-dot, userinfo-prefixed, and DNS aliases that resolve into the blocked range at connect time rather than check time. Repository-admin is a low-to-mid tier role in most Quay deployments — the escalation value is that webhook egress runs with the registry's network position, which typically sits inside the cluster.

Single reusable line for reports: the guard's scope is measured in hops, transports, and spelling variants, not in intent — check each one before crediting the control.

October 13 15:3xZ follow-up: Hono serveStatic prefix-middleware bypass via double path decoding (3g9q/CVE-2026-109955, medium, 4.12.4–<4.13.11)

Hono's serveStatic (and the prefix middleware wired ahead of it, e.g. auth on /static/*) matches the route on the decoded path, while the router reached the handler via an encoded-form match: a malformed/double-percent-encoded path (%252e%252e, mixed-case %25) matches the router's encoded pattern, then decodes inside the static handler into a path the prefix middleware never saw or gated. Result: unauthenticated reads of static files the prefix middleware was supposed to protect. New spelling-variant leaf for this page's standing rule: the mismatch is not between validator and fetcher but between middleware-path and handler-path decode generations — each decode pass creates a second spelling of the same path, and only one of them was inspected.

  • Sweep rule for any middleware-chain framework (Hono, Express with express.static, Fastify @fastify/static, koa-static, Rails static file server): before testing traversal, test decode-generation drift against the gate — request the protected file through %25xx double-encoded, %u00xx (IIS-lineage), over-encoded UTF-8 (%c0%ae), and mixed-case percent forms; a 200 on the gate's own file set while direct paths 401/403 = the finding. Compare what the auth middleware's path matcher normalizes vs what the static handler's resolver normalizes; they are two implementations of "the same" path.
  • Audit fingerprint: middleware that pattern-matches ctx.req.path (raw) while the handler serves from a re-derived/decoded path — the gate and the file share no canonical form. Fix direction vendors took: match middleware on the decoded/canonical path (Hono 4.13.11).
  • Report shape: one table — encoded request form × middleware verdict × served file marker — on a lab app with an owned gate and a marker file under the static root; never read outside the root.

October 14 15:3xZ follow-up: ONLYOFFICE DocumentServer /downloadfile — the SSRF filter lives inside the JWT enforcement path, so the out-of-the-box config ships filter-less (hfqc/CVE-2026-110652, before 10.0.0, unauth)

/downloadfile/:docid accepts a request-body URL, and the private-IP filtering agent is skipped when the URL is marked isInJwtToken — a flag whose trusted handling presumes JWT validation is enabled, which it is not in the default configuration. Unauthenticated attackers submit a body with an internal/loopback URL in the JWT-token slot and the server fetches and proxies back arbitrary private-network and cloud-metadata content.

Durable axes:

  • Security control nested inside another security control = fail-open when the outer toggle is off. The URL guard only ran for JWT-asserted payloads because "someone who could set that flag must be authenticated" is only true when JWT enforcement is on — and defaults ship it off. Sweep rule: on any JWT-gated document/converter service, first determine enforcement state from the outside (unsigned request → 401 vs 200 on a benign endpoint); if enforcement is off, re-run the privileged request shapes (token-slot fields, admin-flag params) unauthenticated — the privileged fetch path is yours.
  • Flag-selected fetch mode is a guard-scope selector. isInJwtToken decided whether the private-IP filter applied, not just provenance. Enumerate body/query flags that switch fetch classes ("trusted URL", "internal", "direct") and test each class's guard coverage independently — same shape as the converter/callback SSRF legs this page has carried since July.
  • Default-off JWT is the ONLYOFFICE deployment norm (most tutorials ship JWT_ENABLED=false), so fingerprint = DocumentServer banner + unauthenticated /downloadfile accepting a URL body. Prove with owned callbacks + a synthetic local marker service; metadata reads are the existence proof, then stop.