CVE-2022-41967 (published 2022-12-28T00:15Z)

Dragonfly is a Java runtime dependency management library. Dragonfly v0.3.0-SNAPSHOT does not configure DocumentBuilderFactory to prevent XML external entity (XXE) attacks. This issue is patched in 0.3.1-SNAPSHOT. As a workaround, since Dragonfly only parses XML `SNAPSHOT` versions are being resolved, this vulnerability may be avoided by not trying to resolve `SNAPSHOT` versions.

CVSS v3 base score: 7.5, HIGH (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N)

Weakness information:
cwe_id: CWE-611
cwe_name: Improper Restriction of XML External Entity Reference
description: The product processes an XML document that can contain XML entities with URIs that resolve to documents outside of the intended sphere of control, causing the product to embed incorrect documents into its output.
extended_description: 
            XML documents optionally contain a Document Type Definition (DTD), which, among other features, enables the definition of XML entities. It is possible to define an entity by providing a substitution string in the form of a URI. The XML parser can access the contents of this URI and embed these contents back into the XML document for further processing.
            By submitting an XML file that defines an external entity with a file:// URI, an attacker can cause the processing application to read the contents of a local file. For example, a URI such as "file:///c:/winnt/win.ini" designates (in Windows) the file C:\Winnt\win.ini, or file:///etc/passwd designates the password file in Unix-based systems. Using URIs with other schemes such as http://, the attacker can force the application to make outgoing requests to servers that the attacker cannot reach directly, which can be used to bypass firewall restrictions or hide the source of attacks such as port scanning.
            Once the content of the URI is read, it is fed back into the application that is processing the XML. This application may echo back the data (e.g. in an error message), thereby exposing the file contents.
         
         

Fix information:
repo_url: https://github.com/HyperaDev/Dragonfly
commit_details: repo_url: https://github.com/HyperaDev/Dragonfly
author: Joshua Sing
author_date: 2022-12-24 03:14:42+11:00
msg: fix: CVE-2022-41967

Dragonfly v0.3.0-SNAPSHOT fails to properly configure the
DocumentBuilderFactory to prevent XML enternal entity (XXE) attacks when
parsing maven-metadata.xml files provided by external Maven repositories
during "SNAPSHOT" version resolution.

This patches CVE-2022-41967 by disabling features which may lead to XXE.
If you are currently using v0.3.0-SNAPSHOT it is STRONGLY advised to
update Dragonfly to v0.3.1-SNAPSHOT just to be safe.
repository: repo_url: https://github.com/HyperaDev/Dragonfly
repo_name: HyperaDev/Dragonfly
description: Java runtime dependency management library
repo_language: Java
owner: HyperaDev
file_changes: filename: AUTHORS
change_type: ModificationType.DELETE
diff: @@ -1,8 +0,0 @@
-#  This is a list of entities who own copyright to portions of Chameleon's code.
-#  Please keep this list sorted alphabetically.
-#
-#  Acceptable formats:
-#    Organization Name
-#    My Name <myname@email.com>
-
-Joshua Sing <joshua@hypera.dev>
\ No newline at end of file

programming_language: unknown
filename: LICENSE
change_type: ModificationType.MODIFY
diff: @@ -1,6 +1,6 @@
 MIT License
 
-Copyright (c) 2021 Joshua Sing <joshua@hypera.dev>
+Copyright (c) 2021-2022 Joshua Sing <joshua@hypera.dev>
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal

programming_language: Markdown
filename: SECURITY.md
change_type: ModificationType.MODIFY
diff: @@ -7,8 +7,8 @@
 
 ### Supported Versions
 | Version | Supported          |
-| ------- | ------------------ |
-| 0.3.0   | :white_check_mark: |
+|---------| ------------------ |
+| 0.3.x   | :white_check_mark: |
 | < 0.3   | :x:                |
 
 ### Reporting a Vulnerability

programming_language: Markdown
filename: pom.xml
change_type: ModificationType.MODIFY
diff: @@ -29,7 +29,7 @@
 
   <groupId>dev.hypera</groupId>
   <artifactId>Dragonfly</artifactId>
-  <version>0.3.0-SNAPSHOT</version>
+  <version>0.3.1-SNAPSHOT</version>
   <packaging>jar</packaging>
 
   <name>Dragonfly</name>
@@ -121,7 +121,7 @@
     <dependency>
       <groupId>org.jetbrains</groupId>
       <artifactId>annotations</artifactId>
-      <version>22.0.0</version>
+      <version>23.1.0</version>
     </dependency>
     <dependency>
       <groupId>me.lucko</groupId>

programming_language: Markdown
filename: Dragonfly.java
change_type: ModificationType.MODIFY
diff: @@ -40,6 +40,7 @@ import java.util.concurrent.CompletableFuture;
 import java.util.function.Consumer;
 import java.util.stream.Collectors;
 import org.jetbrains.annotations.ApiStatus.Internal;
+import org.jetbrains.annotations.NotNull;
 
 /**
  * Main Dragonfly class.
@@ -47,16 +48,16 @@ import org.jetbrains.annotations.ApiStatus.Internal;
  */
 public class Dragonfly {
 
-	private static final String VERSION = "0.3.0-SNAPSHOT";
+	private static final @NotNull String VERSION = "0.3.1-SNAPSHOT";
 
 	private final int timeout;
-	private final Path directory;
-	private final Set<String> repositories;
-	private final Consumer<Status> statusHandler;
+	private final @NotNull Path directory;
+	private final @NotNull Set<String> repositories;
+	private final @NotNull Consumer<Status> statusHandler;
 
-	private final DependencyDownloader dependencyDownloader = new DependencyDownloader(this);
-	private final DependencyRelocator dependencyRelocator;
-	private final DependencyLoader dependencyLoader;
+	private final @NotNull DependencyDownloader dependencyDownloader = new DependencyDownloader(this);
+	private final @NotNull DependencyRelocator dependencyRelocator;
+	private final @NotNull DependencyLoader dependencyLoader;
 
 
 	@Internal
@@ -74,7 +75,7 @@ public class Dragonfly {
 		}
 	}
 
-	public static String getVersion() {
+	public static @NotNull String getVersion() {
 		return VERSION;
 	}
 
@@ -84,7 +85,7 @@ public class Dragonfly {
 	 * @param dependencies Dependencies to be loaded.
 	 * @return If the load was successful, in the form of a {@link CompletableFuture<Boolean>}.
 	 */
-	public CompletableFuture<Boolean> load(Dependency... dependencies) {
+	public @NotNull CompletableFuture<Boolean> load(@NotNull Dependency... dependencies) {
 		return CompletableFuture.supplyAsync(() -> {
 			try {
 				statusHandler.accept(Status.STARTING);
@@ -137,11 +138,11 @@ public class Dragonfly {
 		return timeout;
 	}
 
-	public Path getDirectory() {
+	public @NotNull Path getDirectory() {
 		return directory;
 	}
 
-	public Set<String> getRepositories() {
+	public @NotNull Set<String> getRepositories() {
 		return repositories;
 	}
 
@@ -151,7 +152,7 @@ public class Dragonfly {
 	 * @return Stored instance of {@link DependencyDownloader}.
 	 */
 	@Internal
-	public DependencyDownloader getDependencyDownloader() {
+	public @NotNull DependencyDownloader getDependencyDownloader() {
 		return dependencyDownloader;
 	}
 

programming_language: Java
filename: DragonflyBuilder.java
change_type: ModificationType.MODIFY
diff: @@ -32,6 +32,7 @@ import java.util.Arrays;
 import java.util.HashSet;
 import java.util.Set;
 import java.util.function.Consumer;
+import org.jetbrains.annotations.Contract;
 import org.jetbrains.annotations.NotNull;
 
 /**
@@ -115,6 +116,7 @@ public class DragonflyBuilder {
 	 *
 	 * @return New {@link Dragonfly} instance.
 	 */
+	@Contract(value = "-> new", pure = true)
 	public @NotNull Dragonfly build() {
 		try {
 			return new Dragonfly(timeout, classLoader, directory, repositories, deleteOnRelocate, statusHandler);

programming_language: Java
filename: MavenDownloader.java
change_type: ModificationType.MODIFY
diff: @@ -41,8 +41,8 @@ import org.jetbrains.annotations.NotNull;
 @Downloader(MavenDependency.class)
 public class MavenDownloader implements IDownloader<MavenDependency> {
 
-	private final MavenResolver resolver = new MavenResolver();
-	private final MavenSnapshotResolver snapshotResolver = new MavenSnapshotResolver();
+	private final @NotNull MavenResolver resolver = new MavenResolver();
+	private final @NotNull MavenSnapshotResolver snapshotResolver = new MavenSnapshotResolver();
 
 	@Override
 	public void download(@NotNull Dragonfly dragonfly, @NotNull MavenDependency dependency) throws DownloadFailureException {

programming_language: Java
filename: DownloadFailureException.java
change_type: ModificationType.MODIFY
diff: @@ -23,25 +23,29 @@
 
 package dev.hypera.dragonfly.exceptions;
 
+import org.jetbrains.annotations.NotNull;
+
 public class DownloadFailureException extends DragonflyException {
 
+	private static final long serialVersionUID = 5648475409314204882L;
+
 	public DownloadFailureException() {
 		super();
 	}
 
-	public DownloadFailureException(String message) {
+	public DownloadFailureException(@NotNull String message) {
 		super(message);
 	}
 
-	public DownloadFailureException(String message, Throwable cause) {
+	public DownloadFailureException(@NotNull String message, @NotNull Throwable cause) {
 		super(message, cause);
 	}
 
-	public DownloadFailureException(Throwable cause) {
+	public DownloadFailureException(@NotNull Throwable cause) {
 		super(cause);
 	}
 
-	protected DownloadFailureException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
+	protected DownloadFailureException(@NotNull String message, @NotNull Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
 		super(message, cause, enableSuppression, writableStackTrace);
 	}
 

programming_language: Java
filename: DragonflyException.java
change_type: ModificationType.MODIFY
diff: @@ -23,29 +23,33 @@
 
 package dev.hypera.dragonfly.exceptions;
 
+import org.jetbrains.annotations.NotNull;
+
 /**
  * Dragonfly Exception.
  * @author Joshua Sing <joshua@hypera.dev>
  */
 public class DragonflyException extends Exception {
 
+	private static final long serialVersionUID = 3565376065959848642L;
+
 	public DragonflyException() {
 		super();
 	}
 
-	public DragonflyException(String message) {
+	public DragonflyException(@NotNull String message) {
 		super(message);
 	}
 
-	public DragonflyException(String message, Throwable cause) {
+	public DragonflyException(@NotNull String message, @NotNull Throwable cause) {
 		super(message, cause);
 	}
 
-	public DragonflyException(Throwable cause) {
+	public DragonflyException(@NotNull Throwable cause) {
 		super(cause);
 	}
 
-	protected DragonflyException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
+	protected DragonflyException(@NotNull String message, @NotNull Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
 		super(message, cause, enableSuppression, writableStackTrace);
 	}
 

programming_language: Java
filename: LoadFailureException.java
change_type: ModificationType.MODIFY
diff: @@ -23,25 +23,29 @@
 
 package dev.hypera.dragonfly.exceptions;
 
+import org.jetbrains.annotations.NotNull;
+
 public class LoadFailureException extends DragonflyException {
 
+	private static final long serialVersionUID = -8555618454694039541L;
+
 	public LoadFailureException() {
 		super();
 	}
 
-	public LoadFailureException(String message) {
+	public LoadFailureException(@NotNull String message) {
 		super(message);
 	}
 
-	public LoadFailureException(String message, Throwable cause) {
+	public LoadFailureException(@NotNull String message, @NotNull Throwable cause) {
 		super(message, cause);
 	}
 
-	public LoadFailureException(Throwable cause) {
+	public LoadFailureException(@NotNull Throwable cause) {
 		super(cause);
 	}
 
-	protected LoadFailureException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
+	protected LoadFailureException(@NotNull String message, @NotNull Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
 		super(message, cause, enableSuppression, writableStackTrace);
 	}
 

programming_language: Java
filename: RelocationFailureException.java
change_type: ModificationType.MODIFY
diff: @@ -23,25 +23,29 @@
 
 package dev.hypera.dragonfly.exceptions;
 
+import org.jetbrains.annotations.NotNull;
+
 public class RelocationFailureException extends DragonflyException {
 
+	private static final long serialVersionUID = -2817144091943660838L;
+
 	public RelocationFailureException() {
 		super();
 	}
 
-	public RelocationFailureException(String message) {
+	public RelocationFailureException(@NotNull String message) {
 		super(message);
 	}
 
-	public RelocationFailureException(String message, Throwable cause) {
+	public RelocationFailureException(@NotNull String message, @NotNull Throwable cause) {
 		super(message, cause);
 	}
 
-	public RelocationFailureException(Throwable cause) {
+	public RelocationFailureException(@NotNull Throwable cause) {
 		super(cause);
 	}
 
-	protected RelocationFailureException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
+	protected RelocationFailureException(@NotNull String message, @NotNull Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
 		super(message, cause, enableSuppression, writableStackTrace);
 	}
 

programming_language: Java
filename: ResolveFailureException.java
change_type: ModificationType.MODIFY
diff: @@ -23,25 +23,29 @@
 
 package dev.hypera.dragonfly.exceptions;
 
+import org.jetbrains.annotations.NotNull;
+
 public class ResolveFailureException extends DownloadFailureException {
 
+	private static final long serialVersionUID = -8393996338726021911L;
+
 	public ResolveFailureException() {
 		super();
 	}
 
-	public ResolveFailureException(String message) {
+	public ResolveFailureException(@NotNull String message) {
 		super(message);
 	}
 
-	public ResolveFailureException(String message, Throwable cause) {
+	public ResolveFailureException(@NotNull String message, @NotNull Throwable cause) {
 		super(message, cause);
 	}
 
-	public ResolveFailureException(Throwable cause) {
+	public ResolveFailureException(@NotNull Throwable cause) {
 		super(cause);
 	}
 
-	protected ResolveFailureException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
+	protected ResolveFailureException(@NotNull String message, @NotNull Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
 		super(message, cause, enableSuppression, writableStackTrace);
 	}
 

programming_language: Java
filename: DependencyLoader.java
change_type: ModificationType.MODIFY
diff: @@ -29,6 +29,7 @@ import dev.hypera.dragonfly.exceptions.LoadFailureException;
 import java.net.MalformedURLException;
 import java.util.List;
 import org.jetbrains.annotations.ApiStatus.Internal;
+import org.jetbrains.annotations.NotNull;
 
 /**
  * Dependency loader.
@@ -37,11 +38,11 @@ import org.jetbrains.annotations.ApiStatus.Internal;
  */
 public class DependencyLoader {
 
-	private final Dragonfly dragonfly;
-	private final IClassLoader classLoader;
+	private final @NotNull Dragonfly dragonfly;
+	private final @NotNull IClassLoader classLoader;
 
 	@Internal
-	public DependencyLoader(Dragonfly dragonfly, IClassLoader classLoader) {
+	public DependencyLoader(@NotNull Dragonfly dragonfly, @NotNull IClassLoader classLoader) {
 		this.dragonfly = dragonfly;
 		this.classLoader = classLoader;
 	}
@@ -52,7 +53,7 @@ public class DependencyLoader {
 	 * @param dependencies Dependencies to be loaded.
 	 * @throws LoadFailureException if something went wrong while loading the dependencies.
 	 */
-	public void load(List<Dependency> dependencies) throws LoadFailureException {
+	public void load(@NotNull List<Dependency> dependencies) throws LoadFailureException {
 		for (Dependency dependency : dependencies) {
 			load(dependency);
 		}
@@ -64,7 +65,7 @@ public class DependencyLoader {
 	 * @param dependency Dependency to be loaded.
 	 * @throws LoadFailureException if something went wrong while loading the dependency.
 	 */
-	private void load(Dependency dependency) throws LoadFailureException {
+	private void load(@NotNull Dependency dependency) throws LoadFailureException {
 		try {
 			classLoader.addURL(dragonfly.getDirectory().resolve(dependency.getFileName()).toUri().toURL());
 		} catch (MalformedURLException ex) {

programming_language: Java
filename: DragonflyClassLoader.java
change_type: ModificationType.MODIFY
diff: @@ -26,6 +26,7 @@ package dev.hypera.dragonfly.loading;
 import java.net.URL;
 import java.net.URLClassLoader;
 import org.jetbrains.annotations.ApiStatus.Internal;
+import org.jetbrains.annotations.NotNull;
 
 /**
  * Dragonfly class loader, a child-first {@link URLClassLoader} used for loading Dragonfly's internal dependencies.
@@ -35,17 +36,17 @@ import org.jetbrains.annotations.ApiStatus.Internal;
 @Internal
 public class DragonflyClassLoader extends URLClassLoader {
 
-	public DragonflyClassLoader(ClassLoader classLoader) {
+	public DragonflyClassLoader(@NotNull ClassLoader classLoader) {
 		super(new URL[0], classLoader);
 	}
 
 	@Override
-	public void addURL(URL url) {
+	public void addURL(@NotNull URL url) {
 		super.addURL(url);
 	}
 
 	@Override
-	protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
+	protected @NotNull Class<?> loadClass(@NotNull String name, boolean resolve) throws ClassNotFoundException {
 		Class<?> loadedClass = findLoadedClass(name);
 		if (null == loadedClass) {
 			try {

programming_language: Java
filename: DependencyRelocator.java
change_type: ModificationType.MODIFY
diff: @@ -41,6 +41,7 @@ import java.util.List;
 import java.util.Map;
 import org.jetbrains.annotations.ApiStatus.Internal;
 import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
 
 /**
  * Dependency relocator.
@@ -49,21 +50,21 @@ import org.jetbrains.annotations.NotNull;
  */
 public class DependencyRelocator {
 
-	private static final String RELOCATED_FILENAME = "%s-relocated.jar";
+	private static final @NotNull String RELOCATED_FILENAME = "%s-relocated.jar";
 
-	private final Dragonfly dragonfly;
+	private final @NotNull Dragonfly dragonfly;
 	private final boolean deleteOld;
 
-	private final DragonflyClassLoader classLoader;
-	private final DependencyLoader dependencyLoader;
+	private final @NotNull DragonflyClassLoader classLoader;
+	private final @NotNull DependencyLoader dependencyLoader;
 
 	private boolean dependenciesLoaded;
 
-	private Constructor<?> constructor;
-	private Method method;
+	private @Nullable Constructor<?> constructor;
+	private @Nullable Method method;
 
 	@Internal
-	public DependencyRelocator(Dragonfly dragonfly, boolean deleteOld) {
+	public DependencyRelocator(@NotNull Dragonfly dragonfly, boolean deleteOld) {
 		this.dragonfly = dragonfly;
 		this.deleteOld = deleteOld;
 		this.classLoader = new DragonflyClassLoader(getClass().getClassLoader());

programming_language: Java
filename: Relocation.java
change_type: ModificationType.MODIFY
diff: @@ -32,10 +32,10 @@ import org.jetbrains.annotations.NotNull;
  */
 public class Relocation {
 
-	private final String from;
-	private final String to;
+	private final @NotNull String from;
+	private final @NotNull String to;
 
-	private Relocation(String from, String to) {
+	private Relocation(@NotNull String from, @NotNull String to) {
 		this.from = from.replace("\\.", ".");
 		this.to = to;
 	}
@@ -44,11 +44,11 @@ public class Relocation {
 		return new Relocation(from, to);
 	}
 
-	public String getFrom() {
+	public @NotNull String getFrom() {
 		return from;
 	}
 
-	public String getTo() {
+	public @NotNull String getTo() {
 		return to;
 	}
 

programming_language: Java
filename: MavenResolver.java
change_type: ModificationType.MODIFY
diff: @@ -40,7 +40,7 @@ import org.jetbrains.annotations.Nullable;
  */
 public class MavenResolver implements IResolver<MavenDependency> {
 
-	private static final String FORMAT = "%s%s/%s/%s/%s-%s.jar";
+	private static final @NotNull String FORMAT = "%s%s/%s/%s/%s-%s.jar";
 
 	@Override
 	public @Nullable String resolve(@NotNull Dragonfly dragonfly, @NotNull MavenDependency dependency) throws ResolveFailureException {

programming_language: Java
filename: MavenSnapshotResolver.java
change_type: ModificationType.MODIFY
diff: @@ -27,16 +27,19 @@ import dev.hypera.dragonfly.Dragonfly;
 import dev.hypera.dragonfly.dependency.impl.MavenDependency;
 import dev.hypera.dragonfly.exceptions.ResolveFailureException;
 import dev.hypera.dragonfly.resolvers.IResolver;
+import java.io.IOException;
 import java.io.StringReader;
 import java.util.Set;
 import java.util.stream.Collectors;
 import javax.xml.parsers.DocumentBuilder;
 import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 import org.w3c.dom.Document;
 import org.w3c.dom.Element;
 import org.xml.sax.InputSource;
+import sun.tools.jstat.ParserException;
 
 /**
  * Maven snapshot resolver.
@@ -45,8 +48,26 @@ import org.xml.sax.InputSource;
  */
 public class MavenSnapshotResolver implements IResolver<MavenDependency> {
 
-	private static final String FORMAT = "%s%s/%s/%s/maven-metadata.xml";
-	private static final String OUTPUT_FORMAT = "%s/%s-%s-%s-%s.jar";
+	private static final @NotNull String FORMAT = "%s%s/%s/%s/maven-metadata.xml";
+	private static final @NotNull String OUTPUT_FORMAT = "%s/%s-%s-%s-%s.jar";
+	private final @NotNull DocumentBuilderFactory documentBuilderFactory;
+
+	public MavenSnapshotResolver() {
+		try {
+			/* The below is an attempt to create an XML parser while preventing XML External Entity attacks */
+			/* Read more: https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html#java */
+			DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+			factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
+			factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
+			factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
+			factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
+			factory.setXIncludeAware(false);
+			factory.setExpandEntityReferences(false);
+			this.documentBuilderFactory = factory;
+		} catch (ParserConfigurationException ex) {
+			throw new RuntimeException("Failed to create DocumentBuilderFactory", ex);
+		}
+	}
 
 	@Override
 	public @Nullable String resolve(@NotNull Dragonfly dragonfly, @NotNull MavenDependency dependency) throws ResolveFailureException {
@@ -72,7 +93,7 @@ public class MavenSnapshotResolver implements IResolver<MavenDependency> {
 			}
 
 			try {
-				DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
+				DocumentBuilder builder = documentBuilderFactory.newDocumentBuilder();
 				Document document = builder.parse(new InputSource(new StringReader(data)));
 				Element root = document.getDocumentElement();
 				Element snapshotData = (Element) root.getElementsByTagName("snapshot").item(0);

programming_language: Java