Server-Side Request Forgery (SSRF) in plantuml/plantuml
Description
SSRF vulnerability in PlantUML allows attackers to send unauthorized requests, fixed in version 1.2023.9.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
SSRF vulnerability in PlantUML allows attackers to send unauthorized requests, fixed in version 1.2023.9.
Vulnerability
CVE-2023-3432 is a Server-Side Request Forgery (SSRF) vulnerability in PlantUML, a tool for generating diagrams from textual descriptions. The flaw exists in versions prior to 1.2023.9 and arises from insufficient validation of user-supplied input, allowing attackers to craft malicious diagram descriptions that trigger arbitrary HTTP requests from the server [1][2].
Exploitation
An attacker can exploit this vulnerability by submitting a specially crafted diagram to a PlantUML server. No authentication is required if the service is publicly accessible. The server processes the malicious input and sends HTTP requests to internal or external resources as dictated by the attacker, bypassing typical access controls [1].
Impact
Successful exploitation enables an attacker to probe internal networks, access sensitive internal services, or perform actions on internal systems. This can lead to data exposure, lateral movement, or further compromise of the affected infrastructure [1].
Mitigation
The vulnerability is fixed in PlantUML version 1.2023.9. Users are strongly advised to upgrade to this version or later. The official commit [2] contains the necessary patch. No alternative workarounds have been documented [1][2].
AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
net.sourceforge.plantuml:plantuml-mitMaven | < 1.2023.9 | 1.2023.9 |
net.sourceforge.plantuml:plantumlMaven | < 1.2023.9 | 1.2023.9 |
Affected products
6- osv-coords5 versionspkg:deb/ubuntu/plantuml@1:1.2020.2+ds-1?arch=source&distro=jammypkg:deb/ubuntu/plantuml@1:1.2020.2+ds-3ubuntu1?arch=source&distro=noblepkg:deb/ubuntu/plantuml@1:1.2020.2+ds-5?arch=source&distro=oracularpkg:maven/net.sourceforge.plantuml/plantumlpkg:maven/net.sourceforge.plantuml/plantuml-mit
>= 0+ 4 more
- (no CPE)range: >= 0
- (no CPE)range: >= 0
- (no CPE)range: >= 0
- (no CPE)range: < 1.2023.9
- (no CPE)range: < 1.2023.9
Patches
1b32500bb61aefix: improve filelist support and nwdiag
12 files changed · +259 −100
gradle.properties+1 −1 modified@@ -1,4 +1,4 @@ # Warning, "version" should be the same in gradle.properties and Version.java # Any idea anyone how to magically synchronize those :-) ? -version = 1.2023.9beta3 +version = 1.2023.9beta4 org.gradle.workers.max = 3 \ No newline at end of file
src/net/sourceforge/plantuml/file/AParentFolderRegular.java+5 −5 modified@@ -57,15 +57,15 @@ public AFile getAFile(String nameOrPath) throws IOException { final SFile filecurrent; // Log.info("AParentFolderRegular::looking for " + nameOrPath); // Log.info("AParentFolderRegular::dir = " + dir); - if (dir == null) { + if (dir == null) filecurrent = new SFile(nameOrPath); - } else { + else filecurrent = dir.getAbsoluteFile().file(nameOrPath); - } + // Log.info("AParentFolderRegular::Filecurrent " + filecurrent); - if (filecurrent.exists()) { + if (filecurrent.exists()) return new AFileRegular(filecurrent.getCanonicalFile()); - } + return null; }
src/net/sourceforge/plantuml/filesdiagram/FilesEntry.java+111 −0 added@@ -0,0 +1,111 @@ +/* ======================================================================== + * PlantUML : a free UML diagram generator + * ======================================================================== + * + * (C) Copyright 2009-2024, Arnaud Roques + * + * Project Info: https://plantuml.com + * + * If you like this project or if you find it useful, you can support us at: + * + * https://plantuml.com/patreon (only 1$ per month!) + * https://plantuml.com/paypal + * + * This file is part of PlantUML. + * + * PlantUML is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * PlantUML distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public + * License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + * + * + * Original Author: Arnaud Roques + * + */ +package net.sourceforge.plantuml.filesdiagram; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +import net.sourceforge.plantuml.klimt.UTranslate; +import net.sourceforge.plantuml.klimt.creole.Display; +import net.sourceforge.plantuml.klimt.drawing.UGraphic; +import net.sourceforge.plantuml.klimt.font.FontConfiguration; +import net.sourceforge.plantuml.klimt.geom.HorizontalAlignment; +import net.sourceforge.plantuml.klimt.shape.TextBlock; +import net.sourceforge.plantuml.style.ISkinParam; + +public class FilesEntry implements Iterable<FilesEntry> { + + private final String name; + private FilesType type; + private List<FilesEntry> children = new ArrayList<>(); + + public FilesEntry(String name, FilesType type) { + this.name = name; + this.type = type; + } + + public FilesEntry addRawEntry(String raw) { + final int x = raw.indexOf('/'); + if (x == -1) { + final FilesEntry result = new FilesEntry(raw, FilesType.DATA); + children.add(result); + return result; + } + final FilesEntry folder = getOrCreateFolder(raw.substring(0, x)); + final String remain = raw.substring(x + 1); + if (remain.length() == 0) + return folder; + return folder.addRawEntry(remain); + } + + private FilesEntry getOrCreateFolder(String folderName) { + for (FilesEntry child : children) + if (child.type == FilesType.FOLDER && child.getName().equals(folderName)) + return child; + + final FilesEntry result = new FilesEntry(folderName, FilesType.FOLDER); + children.add(result); + return result; + } + + @Override + public Iterator<FilesEntry> iterator() { + return Collections.unmodifiableCollection(children).iterator(); + } + + public String getName() { + return name; + } + + public String getEmoticon() { + if (type == FilesType.FOLDER) + return "<:1f4c2:>"; + // return "<:1f4c1:>"; + return "<:1f4c4:>"; + } + + public UGraphic drawAndMove(UGraphic ug, FontConfiguration fontConfiguration, ISkinParam skinParam, double deltax) { + final Display display = Display.getWithNewlines(getEmoticon() + getName()); + TextBlock result = display.create(fontConfiguration, HorizontalAlignment.LEFT, skinParam); + result.drawU(ug.apply(UTranslate.dx(deltax))); + ug = ug.apply(UTranslate.dy(result.calculateDimension(ug.getStringBounder()).getHeight() + 2)); + for (FilesEntry child : children) + ug = child.drawAndMove(ug, fontConfiguration, skinParam, deltax + 21); + return ug; + } + +}
src/net/sourceforge/plantuml/filesdiagram/FilesListing.java+4 −16 modified@@ -34,26 +34,19 @@ */ package net.sourceforge.plantuml.filesdiagram; -import java.util.ArrayList; -import java.util.List; - -import net.sourceforge.plantuml.klimt.UTranslate; -import net.sourceforge.plantuml.klimt.creole.Display; import net.sourceforge.plantuml.klimt.drawing.UGraphic; import net.sourceforge.plantuml.klimt.font.FontConfiguration; import net.sourceforge.plantuml.klimt.font.StringBounder; import net.sourceforge.plantuml.klimt.font.UFont; -import net.sourceforge.plantuml.klimt.geom.HorizontalAlignment; import net.sourceforge.plantuml.klimt.geom.XDimension2D; import net.sourceforge.plantuml.klimt.shape.AbstractTextBlock; -import net.sourceforge.plantuml.klimt.shape.TextBlock; import net.sourceforge.plantuml.style.ISkinParam; public class FilesListing extends AbstractTextBlock { private final ISkinParam skinParam; private final FontConfiguration fontConfiguration = FontConfiguration.blackBlueTrue(UFont.courier(14)); - private final List<String> tmp = new ArrayList<>(); + private final FilesEntry root = new FilesEntry("", FilesType.FOLDER); public FilesListing(ISkinParam skinParam) { this.skinParam = skinParam; @@ -66,18 +59,13 @@ public XDimension2D calculateDimension(StringBounder stringBounder) { @Override public void drawU(UGraphic ug) { - for (String s : tmp) { - final Display display = Display.getWithNewlines("<:1f4c4:>" + s); - TextBlock result = display.create(fontConfiguration, HorizontalAlignment.LEFT, skinParam); - result.drawU(ug); - ug = ug.apply(UTranslate.dy(result.calculateDimension(ug.getStringBounder()).getHeight())); - } - + for (FilesEntry ent : root) + ug = ent.drawAndMove(ug, fontConfiguration, skinParam, 0); } public void add(String line) { if (line.startsWith("/")) - tmp.add(line.substring(1)); + root.addRawEntry(line.substring(1)); } }
src/net/sourceforge/plantuml/filesdiagram/FilesType.java+40 −0 added@@ -0,0 +1,40 @@ +/* ======================================================================== + * PlantUML : a free UML diagram generator + * ======================================================================== + * + * (C) Copyright 2009-2024, Arnaud Roques + * + * Project Info: https://plantuml.com + * + * If you like this project or if you find it useful, you can support us at: + * + * https://plantuml.com/patreon (only 1$ per month!) + * https://plantuml.com/paypal + * + * This file is part of PlantUML. + * + * PlantUML is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * PlantUML distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public + * License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + * + * + * Original Author: Arnaud Roques + * + */ +package net.sourceforge.plantuml.filesdiagram; + +public enum FilesType { + FOLDER, DATA; + +}
src/net/sourceforge/plantuml/klimt/sprite/SpriteColor.java+15 −15 modified@@ -66,26 +66,26 @@ public SpriteColor(int width, int height) { } public void setGray(int x, int y, int level) { - if (x < 0 || x >= width) { + if (x < 0 || x >= width) return; - } - if (y < 0 || y >= height) { + + if (y < 0 || y >= height) return; - } - if (level < 0 || level >= 16) { + + if (level < 0 || level >= 16) throw new IllegalArgumentException(); - } + gray[y][x] = level; color[y][x] = -1; } public void setColor(int x, int y, int col) { - if (x < 0 || x >= width) { + if (x < 0 || x >= width) return; - } - if (y < 0 || y >= height) { + + if (y < 0 || y >= height) return; - } + gray[y][x] = -1; color[y][x] = col; } @@ -99,14 +99,14 @@ public int getWidth() { } public UImage toUImage(ColorMapper colorMapper, HColor backcolor, HColor forecolor) { - final BufferedImage im = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + final BufferedImage im = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); - if (backcolor == null) { + if (backcolor == null) backcolor = HColors.WHITE; - } - if (forecolor == null) { + + if (forecolor == null) forecolor = HColors.BLACK; - } + final HColorGradient gradient = HColors.gradient(backcolor, forecolor, '\0'); for (int col = 0; col < width; col++) { for (int line = 0; line < height; line++) {
src/net/sourceforge/plantuml/nwdiag/core/NServer.java+2 −0 modified@@ -112,6 +112,8 @@ public void blankSomeAddress() { } public void learnThisAddress(String address) { + if (address == null) + address = ""; for (Entry<Network, String> ent : connections.entrySet()) { if (ent.getValue().length() == 0) { connections.put(ent.getKey(), address);
src/net/sourceforge/plantuml/nwdiag/NwDiagram.java+16 −0 modified@@ -150,11 +150,27 @@ public CommandExecutionResult openNetwork(String name) { for (NStackable element : stack) if (element instanceof Network) return CommandExecutionResult.error("Cannot nest network"); + + if (networks.size() == 0 && groups.size() == 0) + eventuallyConnectAllStandaloneServersToHiddenNetwork(); + final Network network = createNetwork(name); stack.add(0, network); return CommandExecutionResult.ok(); } + private void eventuallyConnectAllStandaloneServersToHiddenNetwork() { + Network first = null; + for (NServer server : servers.values()) + if (server.isAlone()) { + if (first == null) { + first = createNetwork(""); + first.goInvisible(); + } + server.connectMeIfAlone(first); + } + } + public CommandExecutionResult closeSomething() { if (initDone == false) return errorNoInit();
src/net/sourceforge/plantuml/security/SURL.java+7 −1 modified@@ -250,7 +250,7 @@ private boolean isUrlOk() { /** * Regex to remove the UserInfo part from a URL. */ - private static final Pattern PATTERN_USERINFO = Pattern.compile("(^https?://)([-_0-9a-zA-Z]+@)([^@]*)"); + private static final Pattern PATTERN_USERINFO = Pattern.compile("(^https?://)([-_0-9a-zA-Z]+@)([^@]*)$"); private static final ExecutorService EXE = Executors.newCachedThreadPool(new ThreadFactory() { public Thread newThread(Runnable r) { @@ -292,6 +292,9 @@ public String toString() { } private boolean forbiddenURL(String full) { + // Thanks to Agasthya Kasturi + if (full.contains("@")) + return true; if (full.startsWith("https://") == false && full.startsWith("http://") == false) return true; if (full.matches("^https?://[-#.0-9:\\[\\]+]+/.*")) @@ -305,6 +308,9 @@ private boolean forbiddenURL(String full) { private boolean isInUrlAllowList() { final String full = cleanPath(internal.toString()); + // Thanks to Agasthya Kasturi + if (full.contains("@")) + return false; for (String allow : getUrlAllowList()) if (full.startsWith(cleanPath(allow))) return true;
src/net/sourceforge/plantuml/tim/TFunctionImpl.java+30 −33 modified@@ -60,37 +60,36 @@ public class TFunctionImpl implements TFunction { public TFunctionImpl(String functionName, List<TFunctionArgument> args, boolean unquoted, TFunctionType functionType) { final Set<String> names = new HashSet<>(); - for (TFunctionArgument tmp : args) { + for (TFunctionArgument tmp : args) names.add(tmp.getName()); - } + this.signature = new TFunctionSignature(functionName, args.size(), names); this.args = args; this.unquoted = unquoted; this.functionType = functionType; } public boolean canCover(int nbArg, Set<String> namedArguments) { - for (String n : namedArguments) { - if (signature.getNamedArguments().contains(n) == false) { + for (String n : namedArguments) + if (signature.getNamedArguments().contains(n) == false) return false; - } - } - if (nbArg > args.size()) { + + if (nbArg > args.size()) return false; - } + assert nbArg <= args.size(); int neededArgument = 0; for (TFunctionArgument arg : args) { - if (namedArguments.contains(arg.getName())) { + if (namedArguments.contains(arg.getName())) continue; - } - if (arg.getOptionalDefaultValue() == null) { + + if (arg.getOptionalDefaultValue() == null) neededArgument++; - } + } - if (nbArg < neededArgument) { + if (nbArg < neededArgument) return false; - } + assert nbArg >= neededArgument; return true; } @@ -108,9 +107,9 @@ private TMemory getNewMemory(TMemory memory, List<TValue> values, Map<String, TV } else { value = arg.getOptionalDefaultValue(); } - if (value == null) { + if (value == null) throw new IllegalStateException(); - } + result.put(arg.getName(), value); } return memory.forkFromGlobal(result); @@ -125,11 +124,9 @@ public void addBody(StringLocated s) throws EaterExceptionLocated { body.add(s); if (s.getType() == TLineType.RETURN) { this.containsReturn = true; - if (functionType == TFunctionType.PROCEDURE) { + if (functionType == TFunctionType.PROCEDURE) throw EaterExceptionLocated .located("A procedure cannot have !return directive. Declare it as a function instead ?", s); - // this.functionType = TFunctionType.RETURN; - } } } @@ -147,39 +144,39 @@ public void executeProcedure(TContext context, TMemory memory, LineLocation loca public void executeProcedureInternal(TContext context, TMemory memory, List<TValue> args, Map<String, TValue> named) throws EaterException, EaterExceptionLocated { - if (functionType != TFunctionType.PROCEDURE && functionType != TFunctionType.LEGACY_DEFINELONG) { + if (functionType != TFunctionType.PROCEDURE && functionType != TFunctionType.LEGACY_DEFINELONG) throw new IllegalStateException(); - } + final TMemory copy = getNewMemory(memory, args, named); context.executeLines(copy, body, TFunctionType.PROCEDURE, false); } public TValue executeReturnFunction(TContext context, TMemory memory, LineLocation location, List<TValue> args, Map<String, TValue> named) throws EaterException, EaterExceptionLocated { - if (functionType == TFunctionType.LEGACY_DEFINE) { + if (functionType == TFunctionType.LEGACY_DEFINE) return executeReturnLegacyDefine(location, context, memory, args); - } - if (functionType != TFunctionType.RETURN_FUNCTION) { + + if (functionType != TFunctionType.RETURN_FUNCTION) throw EaterException.unlocated("Illegal call here. Is there a return directive in your function?"); - } + final TMemory copy = getNewMemory(memory, args, named); final TValue result = context.executeLines(copy, body, TFunctionType.RETURN_FUNCTION, true); - if (result == null) { + if (result == null) throw EaterException.unlocated("No return directive found in your function"); - } + return result; } private TValue executeReturnLegacyDefine(LineLocation location, TContext context, TMemory memory, List<TValue> args) throws EaterException, EaterExceptionLocated { - if (legacyDefinition == null) { + if (legacyDefinition == null) throw new IllegalStateException(); - } + final TMemory copy = getNewMemory(memory, args, Collections.<String, TValue>emptyMap()); final String tmp = context.applyFunctionsAndVariables(copy, location, legacyDefinition); - if (tmp == null) { + if (tmp == null) return TValue.fromString(""); - } + return TValue.fromString(tmp); // eaterReturn.execute(context, copy); // // System.err.println("s3=" + eaterReturn.getValue2()); @@ -211,9 +208,9 @@ public boolean hasBody() { } public void finalizeEnddefinelong() { - if (functionType != TFunctionType.LEGACY_DEFINELONG) { + if (functionType != TFunctionType.LEGACY_DEFINELONG) throw new UnsupportedOperationException(); - } + if (body.size() == 1) { this.functionType = TFunctionType.LEGACY_DEFINE; this.legacyDefinition = body.get(0).getString();
src/net/sourceforge/plantuml/version/LicenseInfo.java+27 −28 modified@@ -108,27 +108,27 @@ public static boolean retrieveNamedOrDistributorQuickIsValid() { public static synchronized LicenseInfo retrieveNamedSlow() { cache = LicenseInfo.NONE; - if (OptionFlags.ALLOW_INCLUDE == false) { + if (OptionFlags.ALLOW_INCLUDE == false) return cache; - } + final String key = prefs.get("license", ""); if (key.length() > 0) { cache = setIfValid(retrieveNamed(key), cache); - if (cache.isValid()) { + if (cache.isValid()) return cache; - } + } for (SFile f : fileCandidates()) { try { if (f.exists() && f.canRead()) { final LicenseInfo result = retrieve(f); - if (result == null) { + if (result == null) return null; - } + cache = setIfValid(result, cache); - if (cache.isValid()) { + if (cache.isValid()) return cache; - } + } } catch (IOException e) { Log.info("Error " + e); @@ -157,13 +157,13 @@ public static BufferedImage retrieveDistributorImage(LicenseInfo licenseInfo) { } try { final byte[] s1 = PLSSignature.retrieveDistributorImageSignature(); - if (SignatureUtils.toHexString(s1).equals(SignatureUtils.toHexString(licenseInfo.sha)) == false) { + if (SignatureUtils.toHexString(s1).equals(SignatureUtils.toHexString(licenseInfo.sha)) == false) return null; - } + final InputStream dis = PSystemVersion.class.getResourceAsStream("/distributor.png"); - if (dis == null) { + if (dis == null) return null; - } + try { final BufferedImage result = SImageIO.read(dis); return result; @@ -178,21 +178,20 @@ public static BufferedImage retrieveDistributorImage(LicenseInfo licenseInfo) { public static LicenseInfo retrieveDistributor() { final InputStream dis = PSystemVersion.class.getResourceAsStream("/distributor.txt"); - if (dis == null) { + if (dis == null) return null; - } + try { final BufferedReader br = new BufferedReader(new InputStreamReader(dis)); final String licenseString = br.readLine(); br.close(); final LicenseInfo result = PLSSignature.retrieveDistributor(licenseString); final Throwable creationPoint = new Throwable(); creationPoint.fillInStackTrace(); - for (StackTraceElement ste : creationPoint.getStackTrace()) { - if (ste.toString().contains(result.context)) { + for (StackTraceElement ste : creationPoint.getStackTrace()) + if (ste.toString().contains(result.context)) return result; - } - } + return null; } catch (Exception e) { Logme.error(e); @@ -208,34 +207,34 @@ public static Collection<SFile> fileCandidates() { if (s == null) continue; SFile dir = new SFile(s); - if (dir.isFile()) { + if (dir.isFile()) dir = dir.getParentFile(); - } - if (dir != null && dir.isDirectory()) { + + if (dir != null && dir.isDirectory()) result.add(dir.file("license.txt")); - } + } return result; } private static LicenseInfo setIfValid(LicenseInfo value, LicenseInfo def) { - if (value.isValid() || def.isNone()) { + if (value.isValid() || def.isNone()) return value; - } + return def; } private static LicenseInfo retrieve(SFile f) throws IOException { final BufferedReader br = f.openBufferedReader(); - if (br == null) { + if (br == null) return null; - } + try { final String s = br.readLine(); final LicenseInfo result = retrieveNamed(s); - if (result != null) { + if (result != null) Log.info("Reading license from " + f.getAbsolutePath()); - } + return result; } finally { br.close();
src/net/sourceforge/plantuml/version/Version.java+1 −1 modified@@ -46,7 +46,7 @@ public class Version { // Warning, "version" should be the same in gradle.properties and Version.java // Any idea anyone how to magically synchronize those :-) ? - private static final String version = "1.2023.9beta3"; + private static final String version = "1.2023.9beta4"; public static String versionString() { return version;
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-ff3m-68vj-h86pghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2023-3432ghsaADVISORY
- github.com/plantuml/plantuml/commit/b32500bb61ae617bb312496d6d832e4be8190797ghsaWEB
- huntr.dev/bounties/8ac3316f-431c-468d-87e4-3dafff2ecf51ghsaWEB
- lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/FV7XL3CY3K3K5ER3ASMEQA546MIQQ7QMghsaWEB
- lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/FV7XL3CY3K3K5ER3ASMEQA546MIQQ7QM/mitre
News mentions
0No linked articles in our index yet.