Request-derived identity, injected-guard composition, and a blind-SSRF status oracle (Sept 17 14:48–14:59Z wave)¶
Source: hourly offensive-security scan of GitHub Security Advisories, 2026-09-17 (wave published 2026-09-17T14:48Z–14:59Z).
One theme ties the durable records: the security decision reads its own scope parameter, or its own composition operator, from the request. Tina takes the identity issuer from the query string, Vendure composes its injected visibility guard with the caller's chosen filterOperator, and Marten interpolates the filter key (not just the value) into SQL. Nuxt OG Image adds a reusable blind-SSRF measurement trick.
Advisory table¶
| Product | GHSA | CVE | Sev. | Boundary |
|---|---|---|---|---|
Tina (@tinacms/auth) | GHSA-g74q-6g2f-874x | CVE-2026-63506 | high | isAuthorized(req) validates the bearer token against identity.tinajs.io/v2/apps/${req.query.clientID}/currentUser where clientID is caller-supplied and never compared to the site's own configured TinaCloud app id. Any TinaCloud user creates a free app, gets a token for their own app, and sends ?clientID=<own-app> + their token to a victim self-hosted site; the authorized callback checks only user.verified (the attacker's own email verification) → media read/upload/delete, and with the wizard-default TinaCloudBackendAuthProvider() full GraphQL content read/write/delete |
Vendure (@vendure/core) | GHSA-6j36-r6pr-59x4 | CVE-2026-63472 | critical | ExternalAuthenticationService.createCustomerAndUser() links a newly-presented external (OAuth/social) login to a pre-existing account matched purely by email, without requiring config.verified === true. Any strategy that forwards an unverified provider email (email_verified not checked) → register at the provider with the victim's email → ATO of the victim's account |
| Vendure (Shop API) | GHSA-xf65-r35x-wmmv | CVE-2026-63461 | medium | Mandatory public-visibility filters (Product.enabled = true, Collection.isPrivate = false, Facet.isPrivate = false) were combined with the caller filter using the caller-controlled filterOperator; filterOperator: OR ORs the guard away → disabled products and private collections/facets from the unauthenticated public Shop API |
| Vendure (dashboard) | GHSA-xhq9-whgq-49j5 | CVE-2026-63459 | high | RichTextDescriptionCell "strips HTML" by assigning description to a detached element's innerHTML and reading back textContent — the assignment itself parses active markup and fires <img src=x onerror=…> even on detached nodes in Chromium/Firefox → channel-scoped admin stores payload, higher-privilege admin executes it on list view |
| Marten (.NET document DB) | GHSA-rfx3-98h7-v3xp | CVE-2026-75513 | critical | LINQ provider interpolates a dictionary indexer key into generated SQL as an unescaped single-quoted literal: Where(x => x.Attributes[key] == v) with key = "nonexistent' = '' or 1=1 --" returns all rows — multi-tenant authorization bypass + blind exfil; with Npgsql default multi-statement batching, data modification too. Same class in ContainsKey(key), constant strings projected through Select, and tenant ids reaching projection teardown / partition DDL |
| Nuxt OG Image | GHSA-q8hw-4fvp-9rwv | CVE-2026-61793 | medium | Unauthenticated /_og/d/** base64url-decodes + JSON.parses a URL segment and passes fonts[i].path straight into server-side fetch() — no scheme allowlist, no loopback/RFC1918 block, no rebinding mitigation at documented defaults (security.strict = false, empty secret). Fetch is blind (response consumed as font bytes) but the outer HTTP status is 500 when the target returned 2xx and 200 when it failed/non-2xx → internal port sweep, IMDSv1 reachability + credential-issuance detection |
Tracked without publication (DoS/crash-only, no operator workflow): GHSA-8h9x-89f2-m7x3 (Grav forged-ZIP-size decompression cap bypass), GHSA-rw4j-r22c-9gc3 (AsyncSSH max-packet-size 0 loop freeze), GHSA-9rm7-3qhh-h2mc (Wire decoder overflow crash), GHSA-jh4v-gfqj-7rhx (RabbitMQ client frame-level OOM), GHSA-jgm3-qmp2-c4p7 (Vendure regex filter ReDoS on SQLite).
Folded to existing pages: Sanic chunked-trailer smuggling and oRPC reflected Vary onto the Sept 15 parser/header page, LiteLLM nested user_config.api_base onto the LiteLLM page, Redocly CLI split traversal + cyclonedx-npm Windows --workspace injection onto the developer-control page.
Why this is worth an operator page¶
- The request-derived scope parameter is the identity equivalent of a Host-header trust.
isAuthorizedanswers "is this a valid user of whatever app the caller names?" instead of "…of THIS site." Audit rule: on any SaaS-to-self-hosted auth relay, send your own app/tenant/client identifier (freely registrable on the IdP) with your own valid token and record whether the victim site accepts. The same issuance-provenance axis as the Sept 17 WordPress QR-login and feed-key items — validation of existence where what matters is provenance. - Injected authorization guards must be AND-composed, never operator-composed with caller input.
filterOperator: ORis a public, unauthenticated parameter on the Vendure Shop API; the same shape shows up in any GraphQL/list API that lets clients pick AND/OR over filters that include a server-injected predicate. Probe: add a predicate that matches a hidden entity, flip the operator, compare result sets against a negative control with the operator absent. - Dictionary/EAV filter keys are injection surfaces that value-focused fuzzing misses. Bug-bounty angle: wherever an API filter passes a user string as a key (
attributes[<user>], tag/label/field selectors,ContainsKey), send a single quote before sending payloads in the value position; the key leg reaches SQL string-literal assembly on the Marten class of ORM. innerHTML→textContentis a fake sanitizer. Any React/Vue "strip HTML for preview cell" that round-trips through a detached element executesonerror-style handlers during parse. Treat every admin-panel list cell rendering stored rich-text fields as a stored-XSS candidate; prove with an image-request or marker-event canary, never cookie fetch.- The Nuxt OG Image status oracle is a generic blind-SSRF measurement pattern. Where an SSRF response is consumed opaquely (fonts, images, parsers), differential outer status codes (fail-vs-target-2xx) are frequently still emitted; harvest the oracle before concluding "blind". It supports port liveness and IMDSv1 credential-issuance detection without reading a byte of response body.
Validation workflow (authorized scope only)¶
Disposable identities and owned callbacks only
Use throwaway accounts, apps, and tenants on every leg. Never link real accounts, never read real customer catalogs, media buckets, or tenant rows, never hit metadata endpoints outside an explicitly authorized target.
- Tina-class auth relay: with two disposable TinaCloud (or analogous IdP) apps, authenticate app A normally, then replay token A against the victim self-hosted site with
?clientID=<app-A>. Evidence: 200 + acceptedauthorizedverdict and a benign media-list or content-query call; do not upload/delete against a real bucket. - Guard-composition probe: in a disposable storefront, disable one product, mark one collection private, then query the public Shop API with a predicate matching the hidden entity under
filterOperator: ORand without it. Evidence: presence-only rows/fields in the decision table. - External-auth ATO: two lab accounts; configure a mock OAuth strategy that returns an unverified victim-lab email; confirm link-to-existing vs create-new behavior at the service boundary. Provider must be lab-controlled; never use real social providers.
- Detached-node XSS canary: in an authorized lab dashboard, set a product description to an
<img>whoseonerrorfires a fetch to an owned no-content callback; evidence is the single callback hit. - ORM key-position SQLi: against an owned Marten-backed app only, request a filtered route with
key = "nonexistent' = '' or 1=1 --"and confirm row-scope change or distinct error; use a read-only DB role; never send statement separators against a live multi-statement connection. - Blind-SSRF oracle: point
fonts[].pathat owned redirectors/callbacks only — one returning 200, one returning 404 — record the outer status mapping, then stop. Metadata/LAN enumeration applies only where that scope is explicitly authorized, and capture reachability booleans, never credentials.
Reporting heuristic: title by the broken binding — auth verdict scoped by caller-supplied app id, visibility guard OR-composed with caller filter, dictionary filter key to SQL literal, SSRF response discarded but status leaked — and include the positive/negative decision table plus patched-version behavior.
September 20 follow-up: Argo Workflows NotEquals field selector drops the cluster-scoped access review¶
GHSA-3fwg-9p8x-37mv / CVE-2026-93991 (7.7, Argo Workflows 4.1.0–4.1.3): ListArchivedWorkflows fails to apply the cluster-scoped access review when the metadata.namespace field selector uses the NotEquals operator. A caller with only namespace-scoped list permissions sends metadata.namespace!=<own-namespace> (or iterates the negation) and retrieves archived workflows from all other namespaces — exposing spec arguments, parameter values, and annotations.
Operator value: this is guard-composition drift reached through operator polarity rather than AND/OR. The authorization branch keys on recognizing a positive namespace selector to trigger the broader access review; the negated selector form falls through a code path that never triggers the review. Audit rule: for any API where a caller-selectable filter operator (Equals/NotEquals/In/NotIn/contains) intersects a server-injected scope predicate, enumerate the full operator matrix — including negations and the operator-absent case — and diff the returned-scope set per cell. A scope check proven on = says nothing about !=; the negative form is precisely the one that describes "everything except my scope." On Kubernetes-adjacent surfaces, also sweep field-selector and label-selector parser surfaces for operator spellings (=, ==, !=, ~, !~, set-based) that take different authorization branches. Validation: two disposable namespaces with synthetic archived workflows carrying marker parameters, one low-privilege namespace-scoped ServiceAccount; capture the operator-vs-visible-namespaces decision table on affected and fixed versions; never read real workflow payloads.
October 1 follow-up: the OG-image surface grows a second, worse leg (next/og Node ImageResponse RCE)¶
GHSA-vcvr-r3jv-pc5j (critical, Sept 30): the Node.js ImageResponse implementation from next/og is affected by an upstream vulnerability giving RCE whenever attacker-controlled values are passed into SVG content, attributes, or styles during image generation (the advisory's own example: ?value= interpolated into <svg><title>). Edge-runtime ImageResponse is not affected — the runtime split is the fingerprint. Operator value: this page's Nuxt OG Image leg proved the blind-SSRF/status-oracle class on OG-image endpoints; this is the same endpoint family reached one level deeper — the renderer's SVG/asset pipeline is an execution sink. Sweep rule for any dynamic-OG/social-image feature: enumerate the renderer stack (next/og Node vs Edge, satori, resvg, sharp, headless chrome) and treat every user value reaching markup, attribute, style, font, or remote-asset fields as a candidate parser/memory/code sink, not just a URL to allowlist. Validation: route-gating evidence (public OG route + parameterized SVG interpolation visible in source), then stop at a benign marker value on affected builds in an owned app; do not develop the RCE leg against third-party sites. Report shape: renderer identity (Node vs Edge), interpolation trace param→SVG field, patched-version behavior.