This is an automated email from the ASF dual-hosted git repository.
jacopoc pushed a commit to branch release24.09
in repository https://gitbox.apache.org/repos/asf/ofbiz-framework.git
The following commit(s) were added to refs/heads/release24.09 by this push:
new d094e5a411 Fixed: Improve file path validation to work with mounted
directories
d094e5a411 is described below
commit d094e5a4115c7f056096a3e1d6583a001aef28cc
Author: Jacopo Cappellato <[email protected]>
AuthorDate: Fri May 1 08:40:38 2026 +0200
Fixed: Improve file path validation to work with mounted directories
File.getCanonicalPath() resolves all symlinks in a path. OS-level mount
points (Docker bind mounts, EFS) appear as symlinks. When a subdirectory like
runtime/ is mounted, a file's canonical path resolves to the mount target (e.g.
/mnt/efs/data/file.pdf), which no longer shares a prefix with the canonical
path of ofbiz.home (/opt/ofbiz)
(cherry picked from commit b3eadf1cff2e3431e57c1f2002333fb22e0a458a)
---
.../ofbiz/content/data/DataResourceWorker.java | 15 +++-
.../org/apache/ofbiz/security/SecurityUtil.java | 13 ++--
.../apache/ofbiz/security/SecurityUtilTest.java | 82 ++++++++++++++++++++++
3 files changed, 104 insertions(+), 6 deletions(-)
diff --git
a/applications/content/src/main/java/org/apache/ofbiz/content/data/DataResourceWorker.java
b/applications/content/src/main/java/org/apache/ofbiz/content/data/DataResourceWorker.java
index 1948489166..aa49223991 100644
---
a/applications/content/src/main/java/org/apache/ofbiz/content/data/DataResourceWorker.java
+++
b/applications/content/src/main/java/org/apache/ofbiz/content/data/DataResourceWorker.java
@@ -37,6 +37,7 @@ import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
+import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.Comparator;
import java.util.HashMap;
@@ -473,12 +474,24 @@ public class DataResourceWorker implements
org.apache.ofbiz.widget.content.DataR
/**
* Checks that the given file is within the provided context root
directory.
+ * Uses a dual-check strategy to support EFS/Docker mount points:
+ * 1. Canonical paths (resolves symlinks on both sides) — works for
non-mounted paths.
+ * 2. Normalized absolute paths (collapses ".." without following
symlinks) — fallback for
+ * when contextRoot or a subdirectory inside it is a mount point,
causing canonical paths
+ * to diverge. Path traversal via ".." is still blocked by the
normalization step.
*/
static void checkContextFileBoundary(File file, String contextRoot) throws
GeneralException {
try {
String canonicalAllowed = new File(contextRoot).getCanonicalPath();
String canonicalFilePath = file.getCanonicalPath();
- if (!canonicalFilePath.startsWith(canonicalAllowed +
File.separator)) {
+ boolean passesCanonical =
canonicalFilePath.startsWith(canonicalAllowed + File.separator)
+ || canonicalFilePath.equals(canonicalAllowed);
+
+ Path normalizedAllowed =
Path.of(contextRoot).toAbsolutePath().normalize();
+ Path normalizedFilePath =
file.toPath().toAbsolutePath().normalize();
+ boolean passesNormalized =
normalizedFilePath.startsWith(normalizedAllowed);
+
+ if (!passesCanonical && !passesNormalized) {
throw new GeneralException("Access to file denied: path
resolves outside of the allowed directory");
}
} catch (IOException e) {
diff --git
a/framework/security/src/main/java/org/apache/ofbiz/security/SecurityUtil.java
b/framework/security/src/main/java/org/apache/ofbiz/security/SecurityUtil.java
index ce61571985..fd0d9f38a8 100644
---
a/framework/security/src/main/java/org/apache/ofbiz/security/SecurityUtil.java
+++
b/framework/security/src/main/java/org/apache/ofbiz/security/SecurityUtil.java
@@ -174,16 +174,19 @@ public final class SecurityUtil {
}
/**
- * Checks that the given file is within the OFBiz home directory and
within one of the
- * subdirectories listed in {@code content.data.ofbiz.file.allowed.paths}
(security.properties).
+ * Checks that the given file is within one of the subdirectories listed in
+ * {@code content.data.ofbiz.file.allowed.paths} (security.properties),
relative to the OFBiz
+ * home directory. The check uses canonical paths (resolving symlinks on
both sides), so
+ * EFS/Docker volume mounts on allowed subdirectories are handled
correctly. A preliminary
+ * "must be under ofbiz.home" canonical check is intentionally absent:
when an allowed
+ * subdirectory (e.g. {@code runtime/}) is a mount point, the file's
canonical path diverges
+ * from {@code canonicalHome}, but the per-allowed-path comparison below
still passes because
+ * it resolves both sides through the mount. Path traversal via {@code
../} is still blocked.
*/
public static void checkOfbizFileAllowList(File file) throws
GeneralException {
try {
String canonicalHome = new
File(System.getProperty("ofbiz.home")).getCanonicalPath();
String canonicalFilePath = file.getCanonicalPath();
- if (!canonicalFilePath.startsWith(canonicalHome + File.separator))
{
- throw new GeneralException("Access to file denied: path
resolves outside of the OFBiz home directory");
- }
String allowedPathsStr =
UtilProperties.getPropertyValue("security",
"content.data.ofbiz.file.allowed.paths",
"applications/,themes/,plugins/,runtime/");
if (UtilValidate.isNotEmpty(allowedPathsStr)) {
diff --git
a/framework/security/src/test/java/org/apache/ofbiz/security/SecurityUtilTest.java
b/framework/security/src/test/java/org/apache/ofbiz/security/SecurityUtilTest.java
index 2dae4f7b88..13d6245fa6 100644
---
a/framework/security/src/test/java/org/apache/ofbiz/security/SecurityUtilTest.java
+++
b/framework/security/src/test/java/org/apache/ofbiz/security/SecurityUtilTest.java
@@ -20,13 +20,95 @@ package org.apache.ofbiz.security;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.Path;
import java.util.Arrays;
+import java.util.Comparator;
import java.util.List;
+import org.apache.ofbiz.base.util.GeneralException;
+import org.junit.After;
+import org.junit.Before;
import org.junit.Test;
public class SecurityUtilTest {
+
+ private Path tempHome;
+ private Path tempExternal;
+ private String previousOfbizHome;
+
+ @Before
+ public void setUpTempDirs() throws Exception {
+ tempHome = Files.createTempDirectory("ofbiz-home-test");
+ tempExternal = Files.createTempDirectory("ofbiz-ext-test");
+ previousOfbizHome = System.getProperty("ofbiz.home");
+ System.setProperty("ofbiz.home", tempHome.toString());
+ }
+
+ @After
+ public void tearDownTempDirs() throws Exception {
+ if (previousOfbizHome != null) {
+ System.setProperty("ofbiz.home", previousOfbizHome);
+ } else {
+ System.clearProperty("ofbiz.home");
+ }
+ deleteDirRecursively(tempHome);
+ deleteDirRecursively(tempExternal);
+ }
+
+ private static void deleteDirRecursively(Path dir) throws Exception {
+ if (dir != null && Files.exists(dir)) {
+
Files.walk(dir).sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete);
+ }
+ }
+
+ @Test
+ public void checkOfbizFileAllowListAcceptsFileInAllowedDir() throws
Exception {
+ Path runtimeDir = Files.createDirectory(tempHome.resolve("runtime"));
+ Path file = Files.createTempFile(runtimeDir, "upload", ".dat");
+ SecurityUtil.checkOfbizFileAllowList(file.toFile());
+ }
+
+ @Test
+ public void checkOfbizFileAllowListRejectsFileOutsideAllowedDir() throws
Exception {
+ Path forbiddenDir =
Files.createDirectory(tempHome.resolve("forbidden"));
+ Path file = Files.createTempFile(forbiddenDir, "upload", ".dat");
+ try {
+ SecurityUtil.checkOfbizFileAllowList(file.toFile());
+ fail("Expected GeneralException for file outside allowed dirs");
+ } catch (GeneralException e) {
+ assertTrue(e.getMessage().contains("not within an allowed
directory"));
+ }
+ }
+
+ @Test
+ public void checkOfbizFileAllowListAcceptsFileUnderSymlinkedAllowedDir()
throws Exception {
+ // Simulates an EFS/Docker volume mount: runtime/ is a symlink to an
external directory.
+ // Prior to the fix, the home-boundary canonical check incorrectly
rejected this because
+ // the file's canonical path diverged from canonicalHome.
+ Path runtimeLink = tempHome.resolve("runtime");
+ Files.createSymbolicLink(runtimeLink, tempExternal);
+ Path file = Files.createTempFile(tempExternal, "upload", ".dat");
+ SecurityUtil.checkOfbizFileAllowList(file.toFile());
+ }
+
+ @Test
+ public void checkOfbizFileAllowListRejectsPathTraversal() throws Exception
{
+ Path runtimeDir = Files.createDirectory(tempHome.resolve("runtime"));
+ // Construct a path that tries to escape via ".." — getCanonicalPath
resolves this.
+ File traversalFile = new File(runtimeDir.toFile(), "../../etc/passwd");
+ try {
+ SecurityUtil.checkOfbizFileAllowList(traversalFile);
+ fail("Expected GeneralException for path traversal attempt");
+ } catch (GeneralException e) {
+ assertTrue(e.getMessage().contains("not within an allowed
directory"));
+ }
+ }
+
+
@Test
public void basicAdminPermissionTesting() {
List<String> adminPermissions = Arrays.asList("PARTYMGR", "EXAMPLE",
"ACCTG_PREF");