GitHub Enterprise Server appliance surface: port-blind SSRF with a timing oracle, sanitize-then-rewrite XSS, and name-scoped diff tokens (Sept 22 21:31Z wave)¶
Date reviewed: 2026-09-22 Source: hourly offensive-security scan of GitHub Security Advisories (GHES advisories published 2026-09-22T21:31Z via the GitHub Bug Bounty program).
Advisories:
| Advisory | CVE | Sev. | Boundary |
|---|---|---|---|
| GHSA-5p5v-w43x-q8v7 | CVE-2026-77987 | critical | Notebook viewer validated the scheme and host of a user-supplied URL but not the port → requests to internal services on other ports of the same appliance. Response bodies were not returned, but response timing was an oracle that leaked instance secrets character by character; an extracted secret then chained into a separate interaction with an internal service for appliance RCE. Unauthenticated when private mode is disabled; any authenticated user otherwise. 3.17–3.22; fixed 3.22.1 / 3.21.6 / 3.20.8 / 3.19.12 / 3.18.15 / 3.17.21 |
| GHSA-6293-4rx2-2p57 | CVE-2026-77912 | high | Stored XSS via rendered Markdown: the rendering pipeline rewrote quote characters in already-sanitized HTML without re-sanitizing the result → arbitrary HTML attribute injection. Crafted Markdown abused same-origin JavaScript gadgets to bypass CSP, gained DOM control on the victim's view, extracted CSRF tokens, performed state-changing actions, and propagated to repositories/organizations the victim could write |
| GHSA-843p-hf47-6r9f | CVE-2026-75101 | medium | Raw PR diff/patch access tokens were scoped to repository name + PR number rather than a globally unique repository id. Any authenticated instance user could create a repo + PR colliding with a target's name and PR number, then use their own token to read the private repo's diff/patch. Requires knowing target repo name + a valid PR number. All versions < 3.22; fixed 3.17.21–3.21.6 |
Why this is worth an operator page¶
- Port is a first-class SSRF validation dimension, and "same host" is not "safe." The notebook-viewer guard checked scheme + host and left port free — on a monolithic appliance,
http://<appliance-own-canonical-host>:<internal-port>is a fully guard-passing URL whose destination is an internal service. Audit rule for every URL-fetching feature on an appliance/monolith: enumerate what is listening on other ports of the approved host (GHES historically runs a fleet of internal HTTP/management services on loopback and appliance IPs), and test the approved-host-plus-foreign-port tuple explicitly — the host allowlist is most often written assuming ports don't matter. - No response body ≠ blind. The chain that matters here is the second leg: timing as a character-by-character secret oracle. When an internal service's response time depends on a guessable prefix (auth comparison, redirect-vs-404 fan-out per secret value), a bodyless SSRF still yields the secret byte-by-byte. Operator sequence: (1) establish a timing differential for one bit/character of the guessed value, (2) script the prefix extension, (3) only then look for the consumer that turns the secret into the sink. This is the blind-SSRF measurement pattern (Nuxt OG Image outer-status oracle, Sept 17) generalized from status codes to latency, and it is what turned a "medium-ish SSRF" into unauthenticated appliance RCE.
- Sanitize-then-rewrite is a sanitizer bug, not a bypass. The GHES XSS was not a filter evasion — the pipeline mutated already-sanitized HTML (quote rewriting) and rendered the mutated copy without a second sanitizer pass. Same invariant as the Aug 5 Grocy sanitize-then-transform item and the Sept 22 Svelte DOM-clobbering family: the security verdict is attached to a doc copy, and any later transformation invalidates it. Audit rule: for any rendering pipeline with a sanitizer, list every post-sanitizer transformation step (entity rewriting, quote normalization, linkification, syntax highlighting, html-post-processing) and treat each as an injection point — fuzz canaries that only become active after the rewrite (quote-containing attribute payloads are the canonical shape). CSP is not a negative control: a same-origin JS gadget corpus (existing routes/framework globals that consume DOM state) is usually enough to execute meaningfully under strict CSP; test attribute injection with the CSP policy enabled.
- Token scoping by non-canonical identifier is an IDOR generator. Diff tokens keyed on
(repo name, PR number)accepted attacker-created colliding names. Anywhere an internal token/secret/URL is derived from a human-readable identifier that another principal can register or duplicate (repo names, slugs, org handles, namespaces, bucket names), mint your own colliding object and replay your token against the target's resource. Sweep rule: ask "is the token's scope key globally unique or name-unique?" — name-unique across a multi-tenant instance = collision IDOR. Cheaper than SQLi and lands in the same authz-drift class as the Sept 22 mcp-atlassian UUID-without-scope items. - Private-mode posture is a precondition probe, not a mitigation story. The SSRF is unauthenticated only when private mode is disabled; report the posture as part of exploitability proof (same config-posture axis as Dancer2
static_handler:0and Give Tributes).
Validation workflow (authorized scope only)¶
GHES instances hold real source code — evidence only
Run every leg against a lab GHES instance or an explicitly authorized target with synthetic repos. Never read real private diffs, never extract live instance secrets, never point a notebook URL at production internal services outside authorized scope.
- Port-blind URL check (lab): in a disposable notebook, reference an external resource URL on the appliance's own canonical hostname with (a) the normal port (control), (b) an internal management port that accepts TCP, (c) a closed port. Evidence: connection-timing / error-class decision table only; no content pulled from internal services.
- Timing-oracle existence proof: demonstrate a single-bit timing differential against a lab-only canary secret with a known-value comparison service on the appliance; show two distinguishable latency populations. Do not script the full extraction on any shared system — the differential is the finding.
- Sanitize-then-rewrite canary: render a Markdown payload carrying a quote-shaped attribute canary that only fires after quote normalization (e.g., an attribute breakout whose active form exists solely in the rewritten copy) in a disposable PR body viewed by a second lab user; evidence is a marker fetch to an owned callback or DOM mutation record, with CSP left at default.
- Name-collision token probe: two disposable orgs; org B creates a repo name colliding with org A's private repo, opens a PR to reach the same PR number, requests a raw-diff token for its own repo, and attempts reuse against org A's diff endpoint. Evidence: 200-vs-403 decision table against the synthetic private diff only; delete the collision repos after.
- Record patched-version behavior for each leg (3.22.1 baseline) to prove the fix boundary.
Reporting heuristic¶
Title by the broken binding: URL guard omits port → same-appliance internal reachability, bodyless fetch with latency oracle → secret extraction → internal-service RCE, post-sanitizer quote rewrite renders an unsanitized copy, token scope keyed on name-unique instead of globally-unique identifier. Include the posture precondition (private mode), the timing-vs-status oracle shape, the pre/post-rewrite HTML diff, and the collision decision table. Stop all proofs at the oracle/decision-table level; do not chain into real secret recovery or internal-service actions.
October 6 follow-up: Atlassian Data Center family — one unauthenticated webapp-root file access CVE across eight products (GHSA-gr66-x5pq-8g2g / CVE-2026-21589)¶
Atlassian published a single advisory covering Bitbucket, Confluence, Jira Software/JSM, Bamboo, Crowd Data Center, Crucible, and Fisheye: an unauthenticated remote attacker can access specific files inside the web application root directory, provided they already know the exact name and path (no directory listing). Introduced across branches going back to 2016–2019 (Bitbucket ≥4.6.0, Confluence ≥5.10.0, Jira ≥7.1.0, Crowd ≥2.11.0), fixed per-product in October 2026 point releases (9.4.26 / 10.2.8 / 10.5.1 style fast-follow triples per product line).
Operator value:
- One CVE ID across a whole vendor family = shared platform component. Eight Atlassian DC products sharing one file-serving bug means the vulnerable handler lives in their common web framework, not per-product code. Recon rule: when a vendor publishes a multi-product advisory, treat every listed product as in-scope for the same probe — and when a vendor ships point-release triples across product lines, fingerprint per-product exact build before assuming a patch level applies family-wide (same exact-build fingerprint rule as this page's NetScaler lesson).
- "Requires exact path knowledge" is not "requires secrets" — it means the enumeration leg is source code, not the network. Webapp-root paths for these products are public: packaging manifests,
web.xml/servlet mappings,.propertiesunderWEB-INF/classes, plugin descriptors, and known config filenames are all discoverable from public sources, version-control history, or docs. Validation workflow in authorized scope: build a candidate path list from the product's own public packaging structure, request each path unauthenticated, and diff against an authenticated control; positive class = 200 with file bytes for a path that every authenticated route through the app's own auth filter would gate. Never fetch real config files with live secrets on a client network — stop at header/status evidence plus a known-public file, and report the reachable-file inventory concept. - Same reach class as this page's port-blind SSRF pair: a monolith's static-serving layer is a second, unauthenticated API over the same filesystem. Where GHES historically leaked internal services via host+port, Atlassian DC leaks webapp-root files via a legacy serving path. Standing check on any Atlassian DC engagement (and, more generally, any Java EE monolith): probe known webapp-root paths unauthenticated before concluding the login gate is the perimeter.
Tracked without publication from the Oct 6 00:32Z batch¶
- ViewSonic ViewBoard vCast pair (CVE-2026-82989 unauthenticated input injection into exposed vCast HTTP endpoints + CVE-2026-82988 arbitrary file download / unprivileged APK install via attacker-served URL through the unauthenticated APK-delivery endpoint): conference-room appliance class with untrusted-network-service posture; product-local and detail-sparse, tracked per appliance precedent.
- AMD Zynq UltraScale+/Versal USB boot-mode DFU overflow pair (CVE-2026-0461, CVE-2026-0482): unbounded DFU download requests overflow the DDR receive buffer into FSBL memory during boot; Versal leg requires board modifications to enable USB boot. Boot-chain memory safety, physical-access precondition — tracked, not promotable.
- Docling METS-GBS pre-limit member allocation (GHSA-f4ch-vxwc-3p2m / CVE-2026-105747, 4.3):
getmembers()allocates the full tar member list before themax_member_countguard, and it runs during format detection beforeallowed_formatsapplies — residual-guard leg of the already-promoted Docling page's guard-parity rule; DoS-only, tracked. - MongoDB Operators for Kubernetes OpsManager CR validation (GHSA-pwxc-m348-gxg8 / CVE-2026-103546, 4.3): CR-writer can push unintended admin changes through backup reconciliation — CR-field-to-privileged-API class already cataloged; tracked.
- TOTOLINK X6000R 10.0 single, girishsaraf Online-Appointment-Booking-System SQLi quintet, openSIS teacher-reset-staff-password, Lilishop/newbee-mall/SpringBlade/reNgine/srs singles — router/sparse-VulDB/generic-app classes per precedent. Note for reNgine specifically:
subdomain_discoveryOS command injection via the target Name field is a recon-tool-own-input-handling instance (the Nuclei page's "the scanner is the softest target" rule); sparse detail, tracked without publication — revisit if the fix PR lands with sink detail.
Tracked without publication (same 21:31Z wave)¶
- SolarWinds Observability Self-Hosted unauthenticated RCE pair: CVE-2026-28324 (integrity checks, non-default/non-secure config) + CVE-2026-28325 (deserialization under a specific communication mode). Critical/high, zero technical detail — revisit when detail lands; fingerprint self-hosted installs on authorized targets meanwhile.
- Rockwell Analytics and Location Engine (ALE) ~10-GHSA appliance cluster: default/hard-coded admin + OS credentials with unauthenticated remote login → full system compromise (CVE-2026-76708, critical), unauthenticated elevated file-system write in an internal admin component (CVE-2026-76709, critical), info-disclosure via internal endpoints (CVE-2026-76710), plus authenticated web/API/maintenance-restore legs. Generic one-line summaries, no vectors published — tracked as a default-credential appliance inventory note (fingerprint ALE management interfaces, check vendor default creds only on owned gear); promote when technical detail survives.
- Unbound ≤1.26.0 DNSSEC validator digest-buffer overflow (CVE-2026-81642, critical): DNSKEY whose owner name carries a compression pointer into its own RDATA overflows the digest buffer; attacker-controlled zone + query = DoS→RCE. Parser memory-safety class — tracked per precedent; note the reusable input idea (self-referential compression pointers are an under-fuzzed DNS fixture) without implying an exploit workflow.
- Emacs 28.1–31.1
read-symbol-shorthandsRCE on file open (CVE-2026-96269, default config) — user-interaction document-triggered; tracked. - Thunderbird mail-header parser (CVE-2026-92238, multi-field-merge / memory safety) and Netty malformed-TE smuggling re-listing (CVE-2026-93562, already on the June 11 smuggling page) — tracked.
- OpenClaw iOS deep-link bearer keys in unified logs (CVE-2026-95815): unrotated persistent keys logged as public diagnostic data; replay in forged deep links submits agent requests without local confirmation prompts. Adjacent to the agent-confirmation-bypass family (Sept 21 x-ui, Sept 15 MCP) but product-local; tracked.
- Mattermost OAuth internal-connection filter not applied to OAuth endpoints (CVE-2026-96259): sysadmin-configured OAuth token/userinfo endpoints become internal-network readers — admin-privilege SSRF with response read; class already covered by the Aug 5 outbound-fetch page; tracked.
- e621ng
url_foroption injection (CVE-2026-95813):host/protocol/portquery params interpreted as Rails URL-generation options → pagination links to attacker origins. Rails-specific option-poisoning spelling of the request-derived-URL class; tracked. - pH7Builder X-Forwarded-For-keyed lockout bypass (CVE-2026-37604) — sixth sibling of the header-spoofed limiter identity family (9router X-9r-Real-Ip, Better Messages prefix); tracked.
- Adobe quarterly pack (AEM Forms JEE SSRF/authz/CSRF set, Adobe Connect stored/reflected XSS + SQLi criticals, Bridge/Substance3D/InDesign memory safety), CAI Content Credentials validation set, MCMS SQLi/XSS singles, YARA
.yrcdeserialization pair, flatcc/MPack/GPAC/yaml-cpp memory safety, Apache Lounge hardcoded-config Windows builds, GNU wget WindowsC:\msys64use_askpasscross-user LPE, openCodeWorktree.removerecursive-delete, CMSimple CSRF-off-by-default, LTSecurity LTK3500SF reversible /etc/shadow creds, lwIP MQTT OOB-write + double-free, D-Link/mcp-file-analyzer/OrdaSoft re-listings — all processed/tracked classes.
October 7 follow-up: GHES secret-scanning validator SSRF→RCE + GraphQL default-branch takeover (CVE-2026-96890 / GHSA-jv57-8h35-3r6r, CVE-2026-103620 / GHSA-mj9w-xm4p-h2mp)¶
GHES 3.20–3.22 published two bug-bounty legs (fixed 3.20.9 / 3.21.7 / 3.22.2, branch-takeout also 3.18.16 / 3.19.13) that both generalize past this product.
- The secret scanner validates credentials by calling endpoints embedded in the committed credential. The GCP service-account-credential validity checker issued an outbound request to the token endpoint parsed out of the credential JSON itself, destination unrestricted — so a repository contributor's committed "credential" steers the appliance into internal hosts, and the advisory says that leg chained to RCE on the appliance. Durable axes: (1) any validator/fetcher that consumes connection parameters from the untrusted artifact it is inspecting is SSRF by construction — secret scanners, malware detonators, link unfurlers, license checkers,
jwks_uri/webhook/OIDC discovery fetchers all share it; when auditing one, look for code paths that use a URL/email/host field from the scanned content rather than just regex-matching it. (2) Gate inventory first: exploitation required GHAS + secret-scanning validity checks enabled (non-default), so the GHES fingerprint pass should check/api/v3/-adjacent feature surfaces and org settings for validity-check enablement before assuming the leg is live. Prove only on your own GHES with a synthetic credential JSON pointing the token endpoint at an owned callback; never push real-shaped credentials to shared instances. - Branch protection is only as strong as the delete permission on the sibling verb. A collaborator with write access could delete the current default branch through GraphQL on repos that required PR review but did not restrict deletion — an attacker branch then becomes default, so fresh clones and default-branch API reads serve attacker content while the review gate still "passes" for future PRs. Durable rules: (1) enumerate state-changing verbs on the setting object (branch delete/rename/default-switch) separately from content verbs — protections applied to PRs don't imply protections on branch lifecycle; GraphQL vs REST verb parity checks apply here (this was GraphQL-only). (2) On authorized source-code engagements of GHES/GitLab/Bitbucket, test default-branch takeover on a scratch repo in scope: create branch, delete default via each API family, verify which branch new clones get; then restore. Impact framing: silent supply-chain substitution of
git cloneoutput, not "just" a review bypass.
Related pages¶
- Aug 5 outbound-fetch / command-wrapper boundaries — final-destination SSRF proof and the command-wrapper family.
- Aug 5 Grocy sanitize-then-transform item — same post-sanitizer transformation invariant.
- July 8 integration/deploy/MCP boundaries — Skipper OPA body-authorization chain (this wave adds the third variant; fold below).
- Sept 21 auth-method precedence page — lifecycle-state authorization axis (Vaultwarden fold below).
- Sept 16 mcp-atlassian page — scope-key drift sibling.
Source: hourly offensive-security scan, 2026-09-22 (21:31Z GitHub advisory wave).