This is an automated email from the ASF dual-hosted git repository.
ashishvijaywargiya pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/ofbiz-framework.git
The following commit(s) were added to refs/heads/trunk by this push:
new d3e9283430 Fixed: unrestricted file destination in DataServices
createFile/updateFile (#1747)
d3e9283430 is described below
commit d3e92834301129a84b199f8b3fc037af58b4c4dc
Author: Ashish Vijaywargiya <[email protected]>
AuthorDate: Thu Aug 27 11:01:35 2026 +0530
Fixed: unrestricted file destination in DataServices createFile/updateFile
(#1747)
createFileMethod and updateFileMethod build a target File from the
caller-supplied objectInfo for LOCAL_FILE and OFBIZ_FILE resource types
and write to it directly. Unlike the CONTEXT_FILE branch in the same
methods, and unlike the read path in DataResourceWorker for the same
resource types, neither branch confirmed the resolved path stayed within
the directories configured in security.properties, so a target outside
those directories was accepted and written.
Added the same SecurityUtil.checkLocalFileAllowList/
checkOfbizFileAllowList calls the read path already makes, in both
methods, matching the existing CONTEXT_FILE handling. Added regression
tests that fail against the original code and pass against the fix.
Thank you Krishna Uprit for your help.
---
.../apache/ofbiz/content/data/DataServices.java | 21 +++
.../ofbiz/content/data/DataServicesTest.java | 151 +++++++++++++++++++++
2 files changed, 172 insertions(+)
diff --git
a/applications/content/src/main/java/org/apache/ofbiz/content/data/DataServices.java
b/applications/content/src/main/java/org/apache/ofbiz/content/data/DataServices.java
index 2feed5755d..e59c89fad5 100644
---
a/applications/content/src/main/java/org/apache/ofbiz/content/data/DataServices.java
+++
b/applications/content/src/main/java/org/apache/ofbiz/content/data/DataServices.java
@@ -50,6 +50,7 @@ import org.apache.ofbiz.entity.GenericEntityException;
import org.apache.ofbiz.entity.GenericValue;
import org.apache.ofbiz.entity.util.EntityQuery;
import org.apache.ofbiz.security.SecuredUpload;
+import org.apache.ofbiz.security.SecurityUtil;
import org.apache.ofbiz.service.DispatchContext;
import org.apache.ofbiz.service.GenericServiceException;
import org.apache.ofbiz.service.ModelService;
@@ -253,12 +254,22 @@ public class DataServices {
if (!file.isAbsolute()) {
return
ServiceUtil.returnError(UtilProperties.getMessage(RESOURCE,
"ContentLocalFileDoesNotPointToAbsoluteLocation", locale));
}
+ try {
+ SecurityUtil.checkLocalFileAllowList(file);
+ } catch (GeneralException e) {
+ return ServiceUtil.returnError(e.getMessage());
+ }
} else if ("OFBIZ_FILE".equals(dataResourceTypeId) ||
"OFBIZ_FILE_BIN".equals(dataResourceTypeId)) {
prefix = System.getProperty("ofbiz.home");
if (objectInfo.indexOf('/') != 0 && prefix.lastIndexOf('/') !=
(prefix.length() - 1)) {
sep = "/";
}
file = new File(prefix + sep + objectInfo);
+ try {
+ SecurityUtil.checkOfbizFileAllowList(file);
+ } catch (GeneralException e) {
+ return ServiceUtil.returnError(e.getMessage());
+ }
} else if ("CONTEXT_FILE".equals(dataResourceTypeId) ||
"CONTEXT_FILE_BIN".equals(dataResourceTypeId)) {
prefix = (String) context.get("rootDir");
if (UtilValidate.isEmpty(prefix)) {
@@ -471,12 +482,22 @@ public class DataServices {
if (!file.isAbsolute()) {
throw new GenericServiceException("File: " + fileName + "
is not absolute.");
}
+ try {
+ SecurityUtil.checkLocalFileAllowList(file);
+ } catch (GeneralException e) {
+ return ServiceUtil.returnError(e.getMessage());
+ }
} else if (dataResourceTypeId.startsWith("OFBIZ_FILE")) {
prefix = System.getProperty("ofbiz.home");
if (objectInfo.indexOf('/') != 0 && prefix.lastIndexOf('/') !=
(prefix.length() - 1)) {
sep = "/";
}
file = new File(prefix + sep + objectInfo);
+ try {
+ SecurityUtil.checkOfbizFileAllowList(file);
+ } catch (GeneralException e) {
+ return ServiceUtil.returnError(e.getMessage());
+ }
} else if (dataResourceTypeId.startsWith("CONTEXT_FILE")) {
prefix = (String) context.get("rootDir");
if (objectInfo.indexOf('/') != 0 && prefix.lastIndexOf('/') !=
(prefix.length() - 1)) {
diff --git
a/applications/content/src/test/java/org/apache/ofbiz/content/data/DataServicesTest.java
b/applications/content/src/test/java/org/apache/ofbiz/content/data/DataServicesTest.java
new file mode 100644
index 0000000000..ba2bcc0748
--- /dev/null
+++
b/applications/content/src/test/java/org/apache/ofbiz/content/data/DataServicesTest.java
@@ -0,0 +1,151 @@
+/*******************************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+
*******************************************************************************/
+package org.apache.ofbiz.content.data;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.ofbiz.entity.Delegator;
+import org.apache.ofbiz.service.DispatchContext;
+import org.apache.ofbiz.service.ServiceUtil;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Regression tests for the LOCAL_FILE/OFBIZ_FILE allow-list checks in
+ * {@link DataServices#createFileMethod} and {@link
DataServices#updateFileMethod}.
+ * <p>Both methods build a target {@link File} from the caller-supplied {@code
objectInfo}
+ * and, for CONTEXT_FILE, already confirmed the resolved path stays under the
given
+ * context root before writing to it. The LOCAL_FILE and OFBIZ_FILE branches
did not run
+ * the equivalent {@link
org.apache.ofbiz.security.SecurityUtil#checkLocalFileAllowList}/
+ * {@link org.apache.ofbiz.security.SecurityUtil#checkOfbizFileAllowList}
check that the
+ * read path ({@link DataResourceWorker#getContentFile}) already performs for
the same
+ * resource types, so a target outside the configured directories was accepted
and written.
+ * These tests confirm both write methods now reject such a target before any
file is
+ * created, for both resource types.
+ */
+class DataServicesTest {
+
+ private Path ofbizHome;
+ private Path outsideDir;
+ private String previousOfbizHome;
+
+ @BeforeEach
+ void setUpTempDirs() throws IOException {
+ ofbizHome = Files.createTempDirectory("ofbiz-home-test");
+ outsideDir = Files.createTempDirectory("ofbiz-outside-test");
+ previousOfbizHome = System.getProperty("ofbiz.home");
+ System.setProperty("ofbiz.home", ofbizHome.toString());
+ }
+
+ @AfterEach
+ void tearDownTempDirs() throws IOException {
+ if (previousOfbizHome != null) {
+ System.setProperty("ofbiz.home", previousOfbizHome);
+ } else {
+ System.clearProperty("ofbiz.home");
+ }
+ deleteDirRecursively(ofbizHome);
+ deleteDirRecursively(outsideDir);
+ }
+
+ private static void deleteDirRecursively(Path dir) throws IOException {
+ if (dir != null && Files.exists(dir)) {
+
Files.walk(dir).sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete);
+ }
+ }
+
+ private static DispatchContext mockDispatchContext() {
+ DispatchContext dctx = mock(DispatchContext.class);
+ when(dctx.getDelegator()).thenReturn(mock(Delegator.class));
+ return dctx;
+ }
+
+ @Test
+ void createFileMethodRejectsLocalFileOutsideAllowedDirectory() throws
IOException {
+ Path targetFile = outsideDir.resolve("output.txt");
+ Map<String, Object> context = new HashMap<>();
+ context.put("dataResourceTypeId", "LOCAL_FILE");
+ context.put("objectInfo", targetFile.toString());
+ context.put("textData", "unexpected content");
+
+ Map<String, Object> result =
DataServices.createFileMethod(mockDispatchContext(), context);
+
+ assertTrue(ServiceUtil.isError(result), "A LOCAL_FILE target outside
the allowed directories must be rejected");
+ assertFalse(Files.exists(targetFile), "No file must be created for a
rejected target");
+ }
+
+ @Test
+ void createFileMethodRejectsOfbizFileOutsideAllowedDirectory() throws
IOException {
+ Path targetFile = outsideDir.resolve("output.txt");
+ // A relative reference that, once resolved against ofbiz.home, lands
in a sibling
+ // directory outside ofbiz.home entirely -- mirroring how the
OFBIZ_FILE branch
+ // resolves objectInfo relative to ofbiz.home in production.
+ String objectInfo = "../" + outsideDir.getFileName() + "/output.txt";
+ Map<String, Object> context = new HashMap<>();
+ context.put("dataResourceTypeId", "OFBIZ_FILE");
+ context.put("objectInfo", objectInfo);
+ context.put("textData", "unexpected content");
+
+ Map<String, Object> result =
DataServices.createFileMethod(mockDispatchContext(), context);
+
+ assertTrue(ServiceUtil.isError(result), "An OFBIZ_FILE target
resolving outside ofbiz.home must be rejected");
+ assertFalse(Files.exists(targetFile), "No file must be created for a
rejected target");
+ }
+
+ @Test
+ void updateFileMethodRejectsLocalFileOutsideAllowedDirectory() throws
Exception {
+ Path targetFile = outsideDir.resolve("output.txt");
+ Map<String, Object> context = new HashMap<>();
+ context.put("dataResourceTypeId", "LOCAL_FILE");
+ context.put("objectInfo", targetFile.toString());
+ context.put("textData", "overwritten content");
+
+ Map<String, Object> result =
DataServices.updateFileMethod(mockDispatchContext(), context);
+
+ assertTrue(ServiceUtil.isError(result), "A LOCAL_FILE target outside
the allowed directories must be rejected");
+ assertFalse(Files.exists(targetFile), "No file must be created for a
rejected target");
+ }
+
+ @Test
+ void updateFileMethodRejectsOfbizFileOutsideAllowedDirectory() throws
Exception {
+ Path targetFile = outsideDir.resolve("output.txt");
+ String objectInfo = "../" + outsideDir.getFileName() + "/output.txt";
+ Map<String, Object> context = new HashMap<>();
+ context.put("dataResourceTypeId", "OFBIZ_FILE");
+ context.put("objectInfo", objectInfo);
+ context.put("textData", "overwritten content");
+
+ Map<String, Object> result =
DataServices.updateFileMethod(mockDispatchContext(), context);
+
+ assertTrue(ServiceUtil.isError(result), "An OFBIZ_FILE target
resolving outside ofbiz.home must be rejected");
+ assertFalse(Files.exists(targetFile), "No file must be created for a
rejected target");
+ }
+}