From 5ed336d27a1eff2588b19b6b25379ffb4a32f94c Mon Sep 17 00:00:00 2001 From: iroqueta Date: Fri, 5 Dec 2025 07:30:54 -0300 Subject: [PATCH 1/2] Fix Fortify security scanning * Arbitrary file access during archive extraction ("Zip Slip") Issue: 207277 --- .../com/genexus/compression/GXCompressor.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/gxcompress/src/main/java/com/genexus/compression/GXCompressor.java b/gxcompress/src/main/java/com/genexus/compression/GXCompressor.java index 85b4ed934..20f6f74e3 100644 --- a/gxcompress/src/main/java/com/genexus/compression/GXCompressor.java +++ b/gxcompress/src/main/java/com/genexus/compression/GXCompressor.java @@ -588,6 +588,9 @@ private static void decompressZip(File archive, String directory) throws IOExcep ZipEntry zipEntry; while ((zipEntry = zis.getNextEntry()) != null) { File newFile = new File(directory, zipEntry.getName()); + if (HasZipSlipVulnerability(newFile, directory)) { + throw new IOException("Bad tar entry: " + zipEntry.getName()); + } if (zipEntry.isDirectory()) { if (!newFile.isDirectory() && !newFile.mkdirs()) { throw new IOException("Failed to create directory " + newFile); @@ -614,6 +617,9 @@ private static void decompress7z(File archive, String directory) throws IOExcept SevenZArchiveEntry entry; while ((entry = sevenZFile.getNextEntry()) != null) { File newFile = new File(directory, entry.getName()); + if (HasZipSlipVulnerability(newFile, directory)) { + throw new IOException("Bad tar entry: " + entry.getName()); + } if (entry.isDirectory()) { if (!newFile.isDirectory() && !newFile.mkdirs()) { throw new IOException("Failed to create directory " + newFile); @@ -640,6 +646,9 @@ private static void decompressTar(File archive, String directory) throws IOExcep TarArchiveEntry entry; while ((entry = tis.getNextEntry()) != null) { File newFile = new File(directory, entry.getName()); + if (HasZipSlipVulnerability(newFile, directory)) { + throw new IOException("Bad tar entry: " + entry.getName()); + } if (entry.isDirectory()) { if (!newFile.isDirectory() && !newFile.mkdirs()) { throw new IOException("Failed to create directory " + newFile); @@ -787,4 +796,12 @@ private static void decompressJar(File archive, String directory) throws IOExcep } } } + + // Check for Zip Slip vulnerability: ensure extracted file remains within target directory + // Use Path.normalize() and Path.startsWith() + private static boolean HasZipSlipVulnerability(File file, String directory) { + java.nio.file.Path destDirPath = new File(directory).toPath().toAbsolutePath().normalize(); + java.nio.file.Path newFilePath = file.toPath().toAbsolutePath().normalize(); + return !newFilePath.startsWith(destDirPath); + } } From 7932d53ff4fe56e53d8aaa160271cd35d96d5c5d Mon Sep 17 00:00:00 2001 From: iroqueta Date: Mon, 15 Dec 2025 16:10:07 -0300 Subject: [PATCH 2/2] Remove GX Jar ClassLoader that has security issues and is not used. --- .../main/java/com/genexus/db/Namespace.java | 24 -- .../java/com/genexus/GXJarClassLoader.java | 235 ------------------ .../main/java/com/genexus/db/Namespace.java | 23 -- 3 files changed, 282 deletions(-) delete mode 100644 common/src/main/java/com/genexus/GXJarClassLoader.java diff --git a/android/src/main/java/com/genexus/db/Namespace.java b/android/src/main/java/com/genexus/db/Namespace.java index a13e5e892..dcd2b2494 100644 --- a/android/src/main/java/com/genexus/db/Namespace.java +++ b/android/src/main/java/com/genexus/db/Namespace.java @@ -16,9 +16,6 @@ public class Namespace extends AbstractNamespace public static final int GXDB_CLIENT = 0; public static final int GXDB_SERVER = 1; private static Hashtable namespaceList = new Hashtable(); - - private GXJarClassLoader classLoader; - private Hashtable dataSources = new Hashtable(); private String name; @@ -233,27 +230,6 @@ public static Namespace getNamespace(String name) public void reset() { - if (classLoader != null) - { - classLoader = null; -/* try - { - classLoader.resetClassLoader(); - } - catch (java.io.IOException e) - { - System.err.println("Error resetting namespace classloader " + e.getMessage()); - } -*/ - } - } - - public synchronized GXJarClassLoader getClassLoader() - { - if(classLoader == null) - classLoader = new GXJarClassLoader(classesArchive, autoReload); - else classLoader = classLoader.getClassLoaderInstance(); - return classLoader; } public int getDataSourceCount() diff --git a/common/src/main/java/com/genexus/GXJarClassLoader.java b/common/src/main/java/com/genexus/GXJarClassLoader.java deleted file mode 100644 index dfa3c23bd..000000000 --- a/common/src/main/java/com/genexus/GXJarClassLoader.java +++ /dev/null @@ -1,235 +0,0 @@ - -package com.genexus; -import com.genexus.diagnostics.core.ILogger; -import com.genexus.diagnostics.core.LogManager; - -import java.io.*; -import java.util.*; -import java.util.zip.*; - -public class GXJarClassLoader extends ClassLoader -{ - private String source; - private ZipFile zipFile = null; - private boolean sourceIsJAR; - private Hashtable classTimeStamps = new Hashtable<>(); // Contiene el los timeStamps - private Hashtable classes = new Hashtable<>(); - private long jarTimeStamp = 0; - private boolean autoReload; - private int loadDepth; // Esta variable mantiene un depth de intentos de lectura del Zip - - private static final ILogger logger = LogManager.getLogger(GXJarClassLoader.class); - - /** El Nombre esta medio mal, porque el GXJarClassLoader obtiene las clases de un JAR o - * de una directorio base - * @param location Archivo ZIP/JAR o ruta a directorio base - * @param autoReload Indica si se llevara info sobre el autoReload - * @see getClassLoader - */ - public GXJarClassLoader(String location, boolean autoReload) - { - this.source = location.trim().toLowerCase(); - this.autoReload = autoReload; - - sourceIsJAR = new File(source).isFile(); - loadDepth = 0; - if(!autoReload)openJar(); - logger.debug("## GXJarClassLoader: Initialized (autoReloading: " + autoReload + ")"); - } - - /** Obtiene el ClassLoader asociado. En efecto lo que hace es retornarse a s� mismo en el - * caso en que el autoReload sea false o que el ClassLoader siga siendo v�lido y sino retorna - * un nuevo classLoader - * @return GXJarClassLoader asociado - */ - public GXJarClassLoader getClassLoaderInstance() - { - if (!autoReload || !wasModified()) - return this; - else - { - logger.debug("## GXJarClassLoader: Changed classes detected ..." ); - return new GXJarClassLoader(source, autoReload); - } - } - - /** Indica si se lleva info de autoReload - * @return boolean indicando si se lleva info de AutoReload - */ - public boolean getAutoReload() - { - return autoReload; - } - - public Class loadClass(String className) throws ClassNotFoundException - { - Class cls = (loadClass(className, true)); - - return cls; - } - - private void openJar() - { - if(!sourceIsJAR)return; - if(zipFile == null) - { - try - { - zipFile = new ZipFile(source); - jarTimeStamp = new File(source).lastModified(); - } - catch (IOException e) - { - if (source.length() != 0) - System.err.println("Can't open JAR File: " + source + " : " + e.getMessage()); - } - } - loadDepth ++; - } - - private void closeJar() - { - if(!sourceIsJAR || zipFile == null)return; - loadDepth --; - if(loadDepth > 0)return; - try - { - zipFile.close(); - }catch(IOException e) { ; } - zipFile = null; - - } - - public synchronized Class loadClass(String className, boolean resolveIt) throws ClassNotFoundException - { - Class result; - byte[] classBytes; - - // Primero vemos si es una SystemClass - try - { - result = super.findSystemClass(className); - return result; - } - catch (Throwable e) { ; } - - // Ahora la busco en el cache - - if ((result = classes.get(className)) != null) - return result; - - // HACK: - // Intentamos mantener abierto el Jar en el inter�n de carga de clases - // Como el resolveIt es True, una llamada al loadClass puede implicar la lectura - // de varias clases de una vez. Todas estas se van a cargar desde el mismo zipFile - if (autoReload) - openJar(); - - classBytes = loadBytes(className); - - if (classBytes == null) - { - closeJar(); - // OK, si nuestro classLoader NO pudo encontrar la clase, le damos una �ltima oportunidad - // buscando en el ParentClassLoader (no tiene por qu� haber uno) - // Esto es importante cuando el esquema de classloaders es el de Java 2 (x ej, el usado en el Tomcat 4) - try - { - result = this.getClass().getClassLoader().loadClass(className); - logger.debug("## GXJarClassLoader: Loading ParentClass: " + className); - return result; - }catch(Throwable e) { ; } - throw new ClassNotFoundException(className); - } - result = defineClass(className, classBytes, 0, classBytes.length); - - if (result == null) - { - closeJar(); - throw new ClassFormatError(); - } - - if (resolveIt) - resolveClass(result); - - classes.put(className, result); - - // Ahora 'cerramos' el Jar, porque sino no permitimos que se pueda sobreescribir - // Esto s�lo lo hacemos si este ClassLoader tiene el flag de autoReload marcado - // Aparte se 'cierra' unicamente en el caso en que loadDepth sea 1 - if(autoReload)closeJar(); - - return result; - } - - /** Obtiene los bytes de la clase desde el JAR o del directorio - */ - private byte[] loadBytes(String className) - { - byte[] result = null; - className = className.replace('.', '/') + ".class"; - - logger.debug("## GXJarClassLoader: Loading class: " + className + " [" + source + "]"); - BufferedInputStream bis = null; - InputStream theStream = null; - try - { - if(sourceIsJAR) - { // Si el source se trata de un .JAR - ZipEntry theEntry; - if((theEntry = zipFile.getEntry(className)) != null) - { - result = new byte[(int)theEntry.getSize()]; - theStream = zipFile.getInputStream(theEntry); - new DataInputStream(new BufferedInputStream(theStream)).readFully(result); - theStream.close(); - } - } - else - { // OK, si es un directorio - File theFile = new File(source + File.separator + className); - if(theFile.exists()) - { - result = new byte[(int)theFile.length()]; - theStream = new FileInputStream(theFile); - new DataInputStream(new BufferedInputStream(theStream)).readFully(result); - theStream.close(); - classTimeStamps.put(className, new Long(theFile.lastModified())); - } - } - }catch(IOException e) { ; } - finally - { - try{ - if (bis != null) bis.close(); - if (theStream != null) theStream.close(); - } - catch (IOException ioe) { logger.error("## GXJarClassLoader: Failed to close buffered input stream", ioe ); } - } - return result; - } - - /** Indica si este classLoader tiene clases 'viejas' - * @return true si el classLoader tiene clases viejas - * - */ - - public boolean wasModified() - { - if (sourceIsJAR) - { - return (new File(source).lastModified()) != jarTimeStamp; - } - else - { - // Si el source es un directorio, tengo que chequear todos las clases cargadas - for(Enumeration enum1 = classTimeStamps.keys(); enum1.hasMoreElements();) - { - String className = (String) enum1.nextElement(); - if (new File(source + File.separator + className).lastModified() != classTimeStamps.get(className).longValue()) - return true; - } - return false; - } - } -} \ No newline at end of file diff --git a/java/src/main/java/com/genexus/db/Namespace.java b/java/src/main/java/com/genexus/db/Namespace.java index 0f283493b..cbbcd1614 100644 --- a/java/src/main/java/com/genexus/db/Namespace.java +++ b/java/src/main/java/com/genexus/db/Namespace.java @@ -18,8 +18,6 @@ public class Namespace extends AbstractNamespace public static final int GXDB_SERVER = 1; private static ConcurrentHashMap namespaceListCheck = new ConcurrentHashMap(); private static ConcurrentHashMap namespaceList = new ConcurrentHashMap(); - - private GXJarClassLoader classLoader; private ConcurrentHashMap dataSources = new ConcurrentHashMap(); private String name; @@ -257,19 +255,6 @@ public static Namespace getNamespace(String name) public void reset() { - if (classLoader != null) - { - classLoader = null; -/* try - { - classLoader.resetClassLoader(); - } - catch (java.io.IOException e) - { - System.err.println("Error resetting namespace classloader " + e.getMessage()); - } -*/ - } for (Enumeration en = DBConnectionManager.getInstance().getServerConnections(); en.hasMoreElements();) { ServerUserInformation user = (ServerUserInformation) en.nextElement(); @@ -280,14 +265,6 @@ public void reset() } } - public synchronized GXJarClassLoader getClassLoader() - { - if(classLoader == null) - classLoader = new GXJarClassLoader(classesArchive, autoReload); - else classLoader = classLoader.getClassLoaderInstance(); - return classLoader; - } - public int getDataSourceCount() { return dataSources.size();