Fetch-after-redirect SSRF, integration-identity squatting, and control-plane upload boundaries¶
Source: GitHub Security Advisory unreviewed wave published 2026-09-16T21:32Z, cross-checked against vendor advisories. Related same-wave items were folded into existing product pages: June 5 Crawl4AI/changedetection page, June 22 ComfyUI page, Aug 10 Feast page, and Aug 24 Chroma page.
This batch is durable because every entry names a reusable boundary that generalizes past its product: validate-then-redirect fetchers, attestation/identity decisions made from attacker-supplied input, integration-object claims without control proof, and import/upload surfaces that rewrite execution configuration.
What changed¶
| Advisory | Component | Boundary | Operator value |
|---|---|---|---|
| GHSA-pgwr-3c66-c473 / CVE-2026-92789 | Graylog ≤ 7.1.4 | Lookup tables / event-notification outbound URLs are allow-list validated before the request but not re-validated after following redirects → an allowlisted endpoint that 302s to an internal service makes the server fetch and return internal responses (needs lookup-table or notification permission) | The canonical validate-then-redirect SSRF: any allow-list guard that checks only the initial URL is bypassed with one attacker-or-owner-controlled redirect on an allowlisted host |
| GHSA-7mqp-q79v-g27v / CVE-2026-92775 | Wiki.js ≤ 2.5.314 | Image Prefetch renderer fetches arbitrary URLs with no protocol, host, or address validation; any page-editing user injects img elements with the prefetch-candidate class → server-side requests to internal services and cloud metadata with responses returned to the attacker | Low-privilege CMS editor → full-read SSRF. Editors on shared wikis are common footholds; treat every "optimize/preload/proxy image" feature as a fetch sink |
| GHSA-69p6-97m5-6vpp / CVE-2026-92774 | Wiki.js ≤ 2.5.314 | Multiple GraphQL resolvers omit page tags from authorization checks — tag-scoped page restrictions are enforced on one entry point and skipped on others | Resolver-generation drift: test every GraphQL field that reaches the same model, not just the UI-used one |
| GHSA-mxgr-89mh-cjg6 / CVE-2026-92776 | Wiki.js ≤ 2.5.314 | START/END page-rule path matching does not require path separators, so rule /private matches path /privatepage | Prefix rule matching without a separator boundary is an authorization allowlist bypass class — same shape as the sibling-prefix file containment bugs; always test the no-separator sibling name |
| GHSA-48fw-mh64-g6wf / CVE-2026-92773 | Trigger.dev < 4.6.0 | GitHub App installation binding never verifies the authenticated user controls the installation: replay the OAuth state cookie and supply sequential installation IDs → bind a victim's GitHub App installation to your org → access to the victim's repositories | Integration-object squatting: any "connect app/repo/webhook by numeric ID" flow that trusts the ID without a control proof (ownership check or nonce-bound callback) lets attackers enumerate and claim neighbors' installations |
| GHSA-9cq6-jmp6-5vf7 / CVE-2026-92763 | Rundeck ≤ 6.2.1 | Project archive import authorizes only the importProject action, not the archive contents: importConfig/importNodesSources let a project-import-only role replace node executors and SSH key paths — security-relevant settings that govern how jobs execute | Import endpoints that treat "can import" as "can import anything": the archive payload is config authority. On Rundeck this is a short path from scoped project role to job-execution control |
| GHSA-6h6w-7p6f-xrh3 / CVE-2026-92748 | BC Security Empire < 6.7.1 | Multipart filename on C2 upload endpoints is unvalidated → path traversal writes files to arbitrary C2-server locations for code execution (authenticated operator) | C2-side upload surfaces share the same bug class as every app upload sink: server-side storage paths built from client-controlled filenames. Relevant both when assessing C2 infrastructure in authorized red-team engagements and when auditing your own upload handlers |
| GHSA-mc6j-cg9w-v46r / CVE-2026-92795 | Coze Studio ≤ 0.5.1 | Plugin-tool registration accepts an unrestricted server URL — an authenticated user points the backend at internal services and cloud metadata and reads the responses. Sibling GHSA-77xw-26q5-6wjg / CVE-2026-92788: workflow SQL customization nodes pass table names into SQL without validation | AI-platform plugin registries are SSRF farms: "register your own tool endpoint" features fetch attacker-chosen URLs from the backend network on every invocation. Table-name passthrough in low-code SQL nodes is the SQLi analog |
| GHSA-fjm6-6gxh-mvf2 / CVE-2026-92812 | decap-server | Local proxy containment guard is a plain string prefix comparison without path-separator validation → sibling directories whose names start with the repository directory name are fully readable/writable/deletable outside the intended root | One more instance of the sibling-prefix containment class: startsWith(repoRoot) without a trailing separator admits repoRoot-secret. Test ../<rootname>-canary on every prefix-based path guard |
| GHSA-wj5x-jrhc-mqcx / CVE-2026-92792 | OpenNHP ≤ 1.0.2 | The trusted-execution attestation verifier is selected from attacker-supplied evidence: include a test_purpose key → FallbackVerifier runs unconditionally; pair enrolled measure/serial values from the allowlist → attestation bypass | Test-mode hooks and fallback verifiers selected by the very evidence they should verify — check every attestation/TEE gateway for a caller-visible verifier discriminator |
Operator validation patterns¶
1. Validate-then-redirect SSRF sweep (Graylog, Coze Studio, Wiki.js)¶
- Identify every server-side URL sink: lookup tables, webhook/notification URLs, image prefetch/proxy endpoints, plugin/connector registration URLs.
- Build the two-tier test: (a) direct submission of an internal canary URL — does the validator reject it; (b) an owned public redirector (on an allowlisted host where an allowlist exists) 302-ing to the same internal canary — does the fetcher follow it.
- Record a decision table per sink: validator verdict vs delivered request, with the final destination captured on an owned listener (
httpbin-style on your own host). Evidence is the final-peer hit, never a scan of live internal services. - Check protocol scope on the same sink: does the redirect path also accept
file:or scheme-casing variants (compare the Crawl4AIfile://proofs on the June 5 page). - Wiki.js specifics: proof is an
img class="prefetch-candidate"in a lab page you own, prefetch hitting an owned callback; cloud-metadata framing requires a lab instance inside the target cloud with an approved canary role — do not chase real metadata endpoints on engagements without explicit scope.
2. Integration-object squatting (Trigger.dev)¶
- The reusable check on any "bind by numeric ID + OAuth callback" flow: does the callback verify the completing user controls the object being bound? Replay pattern in a lab: two disposable GitHub accounts, one GitHub App installation; account B replays its own state token with account A's installation ID and confirms the binding lands.
- Report as missing control proof at claim time (ID enumeration + state replay), separate from the OAuth state handling itself.
3. Archive-import config authority (Rundeck)¶
- On any platform with archive/bundle import (CI/CD, job schedulers, workflow engines): enumerate which config keys an archive can overwrite versus which action permission gates the import. If a project-scoped import can replace execution-related config (executors, key paths, plugin descriptors), that permission is effectively execution authority.
- Prove in a lab project only: import an archive that sets a marker config value and a fake SSH key path; capture the before/after config diff. Never import archives against shared Rundeck instances.
4. Sibling-prefix guard differential (decap-server, Wiki.js path rules)¶
- Same harness as the model-checkpoint and Jupyter prefix-guard checks: create the allowed root
foo/, plus sibling canariesfoo-secret/andfoopage, then test the guard's own entry point against the no-separator sibling name. Record raw-guard verdict vs normalized-path verdict; the divergence is the finding.
5. Attester-selected verifier (OpenNHP)¶
- On any attestation gateway: does the evidence payload contain a field that influences which verifier parses it? Send evidence naming test/fallback modes and confirm whether the fallback accepts self-described values. Proofs stay with lab-enrolled synthetic measures/serials.
6. September 17 18:01Z follow-up: signature verified, identity not bound (libp2p PeerStore, GHSA-vrf4-mx87-p53w / CVE-2026-86039, CVSS 8.2)¶
GHSA-vrf4-mx87-p53w / CVE-2026-86039: @libp2p/peer-store's consumePeerRecord verifies the RecordEnvelope signature but never checks that the envelope signer equals the PeerRecord.peerId claimed inside the payload. An attacker seals a record with their own key while the wrapped payload names a victim peer ID; the store then persists attacker-chosen multiaddrs as certified addresses under the victim's identity. The gossipsub Peer Exchange path doesn't save you — expectedPeer is compared against the wire pi.peerID, which the attacker also controls. Result: peers in the mesh dial the attacker believing they hold victim-certified addresses (address-book poisoning / on-path position, 0.17.x line). The library's own protocol-identify utils already contain the missing invariant (peerRecord.peerId.equals(envelopePeer)) — the store path just never ran it.
- Reusable check — signature-verified ≠ identity-bound: on any system that ingests signed envelopes (P2P peer records, OIDC/JWT delegation chains, signed service registries, artifact attestations, SAML envelopes), ask whether the code verifies who signed AND that the signer equals the identity asserted inside the payload. The
expectedPeerpattern is itself a trap: if the expected value is also derived from attacker-controlled wire data, the "check" proves nothing. - The sibling-code clue is a free audit oracle: when one module in a codebase performs a binding check (here
protocol-identify/utils.ts) and a sibling ingestion path omits it, test the omitted path first — same alternate-entry-point-drop-the-check family as the OpenNHP fallback verifier and the Wiki.js tagless resolvers on this page. - Validation stays lab-only: two disposable libp2p peers plus your own local store — forge a victim-named envelope with an attacker key, confirm the store accepts it and marks addresses certified. Never inject forged records into public/mainnet meshes.
7. September 18 follow-up: the same invariant skipped inside gossipsub itself (StrictSign RSA author spoof, GHSA-c3gv-825q-fvmp / CVE-2026-86038, CVSS 7.5)¶
GHSA-c3gv-825q-fvmp / CVE-2026-86038: the second instance of this pattern landed in the same library's message validator. @libp2p/gossipsub validateToRawMessage under the default StrictSign policy binds the embedded msg.key to the claimed from peer ID only conditionally:
Ed25519 peer IDs inline their public key, so the check runs. RSA peer IDs parsed from the wire multihash have publicKey === undefined, so the entire binding check is silently skipped — the validator then verifies the signature against the attacker-supplied msg.key and accepts the message as signed by the victim RSA peer ID. Forged messages pass topic validation and gossip onward, poisoning anything that trusts message.from (validators, ACLs, reputation, accounting, audit logs). Fixed in gossipsub 16.0.5 by adding the missing peerIdFromPublicKey(publicKey).equals(fromPeerId) derivation.
- Reusable check — conditional guards on optional fields are fail-open: any check shaped like
if (optionalField !== undefined && !check(...))rejects nothing when the field is absent. Enumerate which identity/key representations leave the field empty (RSA and other non-inlined key IDs, keyless schemes, unsigned transports) and target those first — the guard you're probing looks present in code review. - Cross-key-type differential: when a signature scheme supports multiple identity encodings (inline-key vs hash-of-key), test each type separately. A fix verified on Ed25519 proves nothing about RSA-style IDs; the same differential applies to JWT
kid/JWKS resolution, SSH host-key pinning, and certificate-pinning bypasses where the pin is only checked when a public key is locally available. - The advisory's own PoC is a reusable harness shape: generate attacker Ed25519 + victim RSA keys locally, build a
RPC.Messagewithfrom= victim multihash, sign with the attacker key, embed the attacker key inmsg.key, and record whether the validator returnsvalidwithfrom == victim. Two local keypairs, zero network.
8. September 18 follow-up: state-changing route gated on the read permission (Apache Airflow, GHSA-gmw9-258q-r8gm / CVE-2026-75157)¶
GHSA-gmw9-258q-r8gm / CVE-2026-75157: Airflow's asset queued-events DELETE endpoints checked the caller's Dag-axis permission with READ instead of EDIT. Any user who could read a Dag could delete that Dag's queued asset events — silently suppressing asset-triggered scheduling. No special configuration is required: Dag read granted more widely than Dag edit is the normal RBAC arrangement, so the default deployment shape is the precondition. Fixed in 3.3.2.
- Reusable check — verb-permission mismatch sweep: for every state-changing endpoint (DELETE/POST/PATCH), confirm the authorization check demands a write-class permission and, where the platform has a resource axis, the same axis the mutation actually touches. Read-gated mutations hide exactly where read access is broad (viewers, auditors, analysts) — the target population is large by design.
- Audit triage on any RBAC platform with fine-grained DAG/pipeline/workflow models: enumerate DELETE routes whose gate name contains
can_read/GET/VIEW, and in a two-role lab (reader canary vs editor canary) confirm the reader can perform the destructive action. Proof = a synthetic queued event on your own test Dag deleted by the reader canary; never touch live scheduling state.
October 1 follow-up: attestation report relay — verified measurements, unbound machine identity (Contrast, GHSA-jw33-f4wc-8736 / CVE-2026-100835)¶
GHSA-jw33-f4wc-8736 / CVE-2026-100835: Contrast < 1.16.0 accepted any TEE attestation report that verified correctly — expected firmware patch levels, expected software measurements — regardless of which machine produced it. An attacker who can MITM the CLI↔Coordinator (or Coordinator↔attested-component) leg and owns one TEE machine (or can extract a report from one) relays that genuine report to impersonate any Coordinator or workload, defeating attested TLS identity. Third instance on this page of the verified ≠ bound invariant, new axis: the binding that was missing is report → physical machine, via the attestation report's reportData/nonce field — a fresh per-session nonce echoed into the report is what makes a report non-replayable; measure-level checks alone certify software, not location. Audit rule for every TEE/attestation integration (Nitro, SEV-SNP, TDX, SGX DCAP): does the verifier (1) require a caller-supplied nonce inside the report, (2) pin the machine/TCB identity (measurement of the key that signed it, not just the signing certificate chain), and (3) reject reports whose freshness it cannot date? Test with your own two lab enclaves: generate a valid report on enclave A, replay into a session opened against B's identity, and record whether the verifier notices. Contrast's TEE-adjacent siblings this quarter (Rollback/rollback-resistance themes in the ToB Sept-25 TEE+MPC piece) reinforce: TEE layers verify state, MPC protocols verify behavior — each deployment silently assumes one of the two is covered.
Reporting heuristics¶
- Lead each finding with the boundary sentence: "the allow-list checked URL A, the fetcher delivered URL B", "the numeric ID was claimed without a control proof", "the import action gated the action, not the contents", "the caller's evidence selected its own verifier."
- Separate permission preconditions honestly (Graylog needs lookup/notification permission; Coze needs an authenticated workspace user; Empire needs an operator session; Wiki.js SSRF needs page-edit). A low-privilege precondition is still a boundary break on multi-user instances — say which tier breaks.
- Keep evidence to owned callbacks, marker config diffs, and decision tables; never capture real cloud-credential responses, victim repo contents, or C2-server files.
Tracked without publication¶
- Wiki.js and Cisco adjacency (76450/76447 ISE OCSP/related hygiene rows), Metabase
0.0.0.0host validation (92813), PrestaShop blockwishlist/psgdpr (92810/92809), phpList mass-action CSRF (92806), Manticore statement-permission drift (92796), UVdesk unauthenticated installer skeleton (92805), Docs websocket revocation (92800), Nango connection-config validation (92804), cc-connect card-action allowlist (92801), LibreTranslate missingaccess_checkroute (92803), Higress cookie-segment panic (92790), GoAdmin unanchored logout pattern (92793), Uber Kraken tag param (92791), Angel Kryo deserialization (92785 — no reachable-service detail published), LightGBM text-model parser bounds (92786), refinedev inferencer field-name interpolation XSS (92784), Builder.io Gen2 prototype pollution pair (92779/92781), KnowStreaming REST RBAC (92780), CMAK CSRF filter + feature-toggle drift (92751/92778), ArcherySec org ownership (92765), OpenCVE token scoping (92764), Pelican Panel startup write perms (92762), Harbor scannerqfilter (92770), Leantime HTMX plugin-install authz (92772), Twenty groupBy-with-records field/row perms (92771), Shlink Mercure key-role drift (92760), metasfresh attachment/comment endpoint checks (92752), WebVirtCloud UserInstance flags (92761), SecObserve disclosure (92759), PatrowlManager events/user access control (92753/92754), Cisco BroadWorks CommPilot (76438), changedetection.io unescaped notification title (92814). No reusable operator workflow beyond patterns already published.
Source: hourly offensive-security scan, 2026-09-17 (Sept 16 21:32Z unreviewed wave). Tracked in the source index.