Skip to content

Refactor to reduce test code duplication#7

Open
PRiewe wants to merge 35 commits into
masterfrom
feature/generator-test-refactor
Open

Refactor to reduce test code duplication#7
PRiewe wants to merge 35 commits into
masterfrom
feature/generator-test-refactor

Conversation

@PRiewe

@PRiewe PRiewe commented Jan 6, 2026

Copy link
Copy Markdown
Owner

No description provided.

pathArray[i] = name.substring(0, name.indexOf("/"));
name = name.substring(name.indexOf("/") + 1);
}
pathArray[separatorCount + 1] = name;

Check failure

Code scanning / CodeQL

Arbitrary file access during archive extraction ("Zip Slip") High

Unsanitized archive entry, which may contain '..', is used in a
file system operation
.
Unsanitized archive entry, which may contain '..', is used in a
file system operation
.
Unsanitized archive entry, which may contain '..', is used in a
file system operation
.
Unsanitized archive entry, which may contain '..', is used in a
file system operation
.
Unsanitized archive entry, which may contain '..', is used in a
file system operation
.

Copilot Autofix

AI 5 months ago

At a high level, we want to ensure that any filesystem paths derived from untrusted or semi‑trusted data (jar entry names and logical path segments) cannot be used to access files outside an intended base directory. The standard fix is to resolve the candidate path against the base directory, normalize/canonicalize it, and verify that it is still under that base directory before using it in any file operation.

Concretely, in this codebase the risky places are:

  1. addArchive in FileSystem:

    • It stores entry.getName() (jar entry path) as the value in the PathTree. If an entry name contains .. or absolute elements, files.get(path) later returns that unsanitized value and it is passed to FileInputStream as a filesystem path. We should validate the entry name when mounting the archive and either reject or normalize dangerous entries.
  2. getFile in FileSystem:

    • Uses temp.getPath() + toString(path) to create a File in the temp directory, without verifying that the resulting path is inside temp.
    • Uses new FileInputStream(files.get(path)) on a string that may be derived (indirectly) from jar entries.
  3. saveToTemp in FileSystem:

    • Also uses temp.getPath() + toString(path) to create files for writing, with the same lack of boundary checks.

The safest, minimal‑behavior‑change fix is:

  • Introduce a small helper in FileSystem to safely resolve a relative path (string constructed by toString(path) or stored in files) under a base directory, using Path.of(base, relative).normalize() and checking that the resulting path starts with the base directory path. If not, throw an IOException or log and abort.

  • Use this helper:

    • In getFile, before using temp paths: instead of new File(temp.getPath() + toString(path)), compute a Path under temp with normalization and bounds check, then use that Path to open the file.
    • In saveToTemp, do the same when creating/writing files in temp.
    • In getFile’s non‑jar branch (new FileInputStream(files.get(path))), if files.get(path) is intended to be relative to some root, we should at least normalize it and optionally restrict it to not be absolute and not contain ... Since we’re not shown the full semantics of paths and directory roots, we’ll implement a conservative check: disallow absolute paths and .. segments in files.get(path) before using it.
  • In addArchive, prevent storing clearly dangerous entry names. The code only uses the entry name as the value in the tree (not to open files inside the jar except via jar.getEntry(value), which is safe inside the jar), but that same value is later used to open an external file in the else‑branch of getFile. To avoid poisoning the mapping with dangerous values, we can:

    • Skip jar entries whose entry.getName() is absolute or contains .. path elements, and log a warning.
    • This doesn’t affect normal mods but prevents crafted jars from introducing unsafe paths.

Implementation details and locations:

  • File: src/main/java/neon/systems/files/FileSystem.java

    1. Add an import for java.nio.file.Paths or just use Path.of; we already import java.nio.file.Path, so we can use Path.of(...) without new imports.

    2. Add a private helper method, e.g. private Path resolveUnderBase(File baseDir, String relativePath) throws IOException, which:

      • Builds a Path base = baseDir.toPath().toAbsolutePath().normalize();
      • Builds Path candidate = base.resolve(relativePath).normalize();
      • Checks if (!candidate.startsWith(base)) throw new IOException("Path traversal attempt: " + relativePath);
      • Returns candidate.
    3. Update getFile:

      • Replace new File(temp.getPath() + toString(path)) with logic that uses resolveUnderBase(temp, toString(path)).
      • Replace new FileInputStream(temp.getPath() + toString(path)) accordingly.
      • Before new FileInputStream(files.get(path)), validate that files.get(path) is not null, not absolute (!new File(name).isAbsolute()), and does not contain ".." as a segment (simple contains("..") or better, Path.of(name).normalize() and ensure it has no ..). Since we must avoid changing semantics too much and not know the root, we’ll implement a simple guard: if the normalized Path is absolute or contains .. segments (detected by comparing normalize() to Path.of(name) and/or scanning elements), we throw an IOException.

      To keep changes small, we can restrict ourselves to rejecting any files.get(path) that is absolute or whose normalized path starts with ".." or contains "/../" or File.separator + ".." + File.separator. That’s enough to avoid obvious path traversal into the host filesystem.

    4. Update saveToTemp:

      • Use resolveUnderBase(temp, toString(path)) to obtain a Path and then a File for writing (filePath.toFile()), instead of concatenating strings.
    5. Update addArchive:

      • After String name = entry.getName(); (or before adding to files), add a simple validation to skip dangerous names:
        • If name.startsWith("/") or name.contains(".."), log a warning and continue;.
        • This prevents storing entries with traversal patterns.

These changes keep functionality for legitimate paths but prevent traversal both when writing to temp and when using potentially poisoned values from files. They also address all CodeQL variants because they sanitize the data at the point where untrusted archive entry names flow into filesystem sinks.


Suggested changeset 1
src/main/java/neon/systems/files/FileSystem.java

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/main/java/neon/systems/files/FileSystem.java b/src/main/java/neon/systems/files/FileSystem.java
--- a/src/main/java/neon/systems/files/FileSystem.java
+++ b/src/main/java/neon/systems/files/FileSystem.java
@@ -130,6 +130,11 @@
       JarEntry entry = entries.nextElement();
       if (!entry.isDirectory()) {
         String name = entry.getName();
+        // basic sanity check on jar entry names to avoid path traversal patterns
+        if (name.startsWith("/") || name.contains("..")) {
+          log.warn("Skipping suspicious jar entry '{}' in archive '{}'", name, path);
+          continue;
+        }
         // this apparently must use "/" because I'm in a jar, and not File.separator
         int separatorCount = name.length() - name.replace("/", "").length();
         String[] pathArray = new String[separatorCount + 2];
@@ -207,8 +212,11 @@
   public <T> T getFile(Translator<T> translator, String... path) {
     try {
       //			System.out.println(Arrays.deepToString(path));
-      if (new File(temp.getPath() + toString(path)).exists()) {
-        InputStream stream = new FileInputStream(temp.getPath() + toString(path));
+      String relative = toString(path);
+      Path tempBase = temp.toPath().toAbsolutePath().normalize();
+      Path tempTarget = tempBase.resolve(relative).normalize();
+      if (tempTarget.startsWith(tempBase) && tempTarget.toFile().exists()) {
+        InputStream stream = new FileInputStream(tempTarget.toFile());
         return translator.translate(stream);
       } else if (jars.containsKey(path[0])) { // path[0] is the name of the mod
         JarFile jar = new JarFile(new File(jars.get(path[0])));
@@ -217,7 +225,16 @@
         jar.close();
         return t;
       } else {
-        InputStream stream = new FileInputStream(files.get(path));
+        String external = files.get(path);
+        if (external == null) {
+          throw new IOException("No file mapped for path " + Arrays.toString(path));
+        }
+        File externalFile = new File(external);
+        // basic guard against absolute paths and traversal segments
+        if (externalFile.isAbsolute() || external.contains("..")) {
+          throw new IOException("Refusing to access suspicious path '" + external + "'");
+        }
+        InputStream stream = new FileInputStream(externalFile);
         return translator.translate(stream);
       }
     } catch (IOException e) {
@@ -263,7 +280,13 @@
    */
   public <T> void saveToTemp(T output, Translator<T> translator, String... path) {
     try {
-      File file = new File(temp.getPath() + toString(path));
+      String relative = toString(path);
+      Path base = temp.toPath().toAbsolutePath().normalize();
+      Path target = base.resolve(relative).normalize();
+      if (!target.startsWith(base)) {
+        throw new IOException("Refusing to write outside temp directory: " + target);
+      }
+      File file = target.toFile();
       if (!file.getParentFile().exists()) {
         makeDir(file.getParent());
       }
EOF
@@ -130,6 +130,11 @@
JarEntry entry = entries.nextElement();
if (!entry.isDirectory()) {
String name = entry.getName();
// basic sanity check on jar entry names to avoid path traversal patterns
if (name.startsWith("/") || name.contains("..")) {
log.warn("Skipping suspicious jar entry '{}' in archive '{}'", name, path);
continue;
}
// this apparently must use "/" because I'm in a jar, and not File.separator
int separatorCount = name.length() - name.replace("/", "").length();
String[] pathArray = new String[separatorCount + 2];
@@ -207,8 +212,11 @@
public <T> T getFile(Translator<T> translator, String... path) {
try {
// System.out.println(Arrays.deepToString(path));
if (new File(temp.getPath() + toString(path)).exists()) {
InputStream stream = new FileInputStream(temp.getPath() + toString(path));
String relative = toString(path);
Path tempBase = temp.toPath().toAbsolutePath().normalize();
Path tempTarget = tempBase.resolve(relative).normalize();
if (tempTarget.startsWith(tempBase) && tempTarget.toFile().exists()) {
InputStream stream = new FileInputStream(tempTarget.toFile());
return translator.translate(stream);
} else if (jars.containsKey(path[0])) { // path[0] is the name of the mod
JarFile jar = new JarFile(new File(jars.get(path[0])));
@@ -217,7 +225,16 @@
jar.close();
return t;
} else {
InputStream stream = new FileInputStream(files.get(path));
String external = files.get(path);
if (external == null) {
throw new IOException("No file mapped for path " + Arrays.toString(path));
}
File externalFile = new File(external);
// basic guard against absolute paths and traversal segments
if (externalFile.isAbsolute() || external.contains("..")) {
throw new IOException("Refusing to access suspicious path '" + external + "'");
}
InputStream stream = new FileInputStream(externalFile);
return translator.translate(stream);
}
} catch (IOException e) {
@@ -263,7 +280,13 @@
*/
public <T> void saveToTemp(T output, Translator<T> translator, String... path) {
try {
File file = new File(temp.getPath() + toString(path));
String relative = toString(path);
Path base = temp.toPath().toAbsolutePath().normalize();
Path target = base.resolve(relative).normalize();
if (!target.startsWith(base)) {
throw new IOException("Refusing to write outside temp directory: " + target);
}
File file = target.toFile();
if (!file.getParentFile().exists()) {
makeDir(file.getParent());
}
Copilot is powered by AI and may make mistakes. Always verify output.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants