From a99738a79664e2d2b1c40ec10891b9e25c793174 Mon Sep 17 00:00:00 2001 From: Naveed Date: Sat, 24 Oct 2026 12:49:08 +0530 Subject: [PATCH 1/2] write the file config store with owner-only permissions FileBasedConfigStore wrote config-store.json through a sibling .tmp file which carry resolved credentials, were readable by every local user under the default 022 umask. The .tmp is now created owner-only on POSIX filesystems before it is written and atomically renamed into place. --- .../core/config/FileBasedConfigStore.java | 20 ++++++++++++++-- .../pipes/core/config/ConfigStoreTest.java | 23 +++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/config/FileBasedConfigStore.java b/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/config/FileBasedConfigStore.java index ee0de07fd56..a74660ac5f3 100644 --- a/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/config/FileBasedConfigStore.java +++ b/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/config/FileBasedConfigStore.java @@ -17,10 +17,14 @@ package org.apache.tika.pipes.core.config; import java.io.IOException; +import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.FileAttribute; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; +import java.util.EnumSet; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -41,6 +45,9 @@ public class FileBasedConfigStore implements ConfigStore { private static final Logger LOG = LoggerFactory.getLogger(FileBasedConfigStore.class); private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + // stored configs carry fetcher/emitter credentials; keep them from other local users + private static final Set OWNER_ONLY = + EnumSet.of(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE); private final Path configFile; private final ConcurrentHashMap cache = new ConcurrentHashMap<>(); @@ -152,7 +159,9 @@ private synchronized void loadFromFile() throws IOException { private synchronized void saveToFile() { try { // Write to temp file first, then atomic rename - Path tempFile = Paths.get(configFile.toString() + ".tmp"); + Path tempFile = configFile.resolveSibling(configFile.getFileName() + ".tmp"); + Files.deleteIfExists(tempFile); + Files.createFile(tempFile, ownerOnly()); OBJECT_MAPPER.writerWithDefaultPrettyPrinter() .writeValue(tempFile.toFile(), cache); Files.move(tempFile, configFile, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); @@ -162,4 +171,11 @@ private synchronized void saveToFile() { throw new RuntimeException("Failed to save config store", e); } } + + private static FileAttribute[] ownerOnly() { + if (FileSystems.getDefault().supportedFileAttributeViews().contains("posix")) { + return new FileAttribute[]{PosixFilePermissions.asFileAttribute(OWNER_ONLY)}; + } + return new FileAttribute[0]; + } } diff --git a/tika-pipes/tika-pipes-core/src/test/java/org/apache/tika/pipes/core/config/ConfigStoreTest.java b/tika-pipes/tika-pipes-core/src/test/java/org/apache/tika/pipes/core/config/ConfigStoreTest.java index dbffab352fe..13e58409543 100644 --- a/tika-pipes/tika-pipes-core/src/test/java/org/apache/tika/pipes/core/config/ConfigStoreTest.java +++ b/tika-pipes/tika-pipes-core/src/test/java/org/apache/tika/pipes/core/config/ConfigStoreTest.java @@ -21,8 +21,16 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.PosixFilePermission; +import java.util.Set; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; import org.apache.tika.plugins.ExtensionConfig; @@ -112,4 +120,19 @@ public void testLoggingConfigStore() { assertNull(store.get("nonexistent")); } + + @Test + public void testFileBasedConfigStoreIsOwnerOnly(@TempDir Path tmp) throws Exception { + assumeTrue(FileSystems.getDefault().supportedFileAttributeViews().contains("posix")); + Path file = tmp.resolve("store").resolve("config-store.json"); + FileBasedConfigStore store = new FileBasedConfigStore(file); + store.init(); + store.put("s3", new ExtensionConfig("s3", "s3-fetcher", "{\"secretKey\":\"hunter2\"}")); + store.put("http", new ExtensionConfig("http", "http-fetcher", "{\"password\":\"x\"}")); + + Set perms = Files.getPosixFilePermissions(file); + assertEquals(Set.of(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE), perms); + assertFalse(Files.exists(file.resolveSibling("config-store.json.tmp"))); + assertEquals("s3-fetcher", new FileBasedConfigStore(file).get("s3").name()); + } } From 62b05d1aacc0903a2a135eead420dd4470880fbb Mon Sep 17 00:00:00 2001 From: Naveed Date: Sun, 25 Oct 2026 23:04:18 +0530 Subject: [PATCH 2/2] use a unique owner-only temp file per config store save --- .../core/config/FileBasedConfigStore.java | 18 ++++++++++++++---- .../pipes/core/config/ConfigStoreTest.java | 7 ++++++- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/config/FileBasedConfigStore.java b/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/config/FileBasedConfigStore.java index a74660ac5f3..b43b1bf2cd6 100644 --- a/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/config/FileBasedConfigStore.java +++ b/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/config/FileBasedConfigStore.java @@ -157,18 +157,28 @@ private synchronized void loadFromFile() throws IOException { } private synchronized void saveToFile() { + Path tempFile = null; try { - // Write to temp file first, then atomic rename - Path tempFile = configFile.resolveSibling(configFile.getFileName() + ".tmp"); - Files.deleteIfExists(tempFile); - Files.createFile(tempFile, ownerOnly()); + // Write to a unique temp file first, then atomic rename; a unique name keeps + // other instances/processes from deleting or recreating it mid-write + tempFile = Files.createTempFile(configFile.toAbsolutePath().getParent(), + configFile.getFileName() + ".", ".tmp", ownerOnly()); OBJECT_MAPPER.writerWithDefaultPrettyPrinter() .writeValue(tempFile.toFile(), cache); Files.move(tempFile, configFile, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); + tempFile = null; LOG.debug("Saved {} configurations to {}", cache.size(), configFile); } catch (IOException e) { LOG.error("Failed to save configurations to {}", configFile, e); throw new RuntimeException("Failed to save config store", e); + } finally { + if (tempFile != null) { + try { + Files.deleteIfExists(tempFile); + } catch (IOException e) { + LOG.warn("Failed to delete temp file {}", tempFile, e); + } + } } } diff --git a/tika-pipes/tika-pipes-core/src/test/java/org/apache/tika/pipes/core/config/ConfigStoreTest.java b/tika-pipes/tika-pipes-core/src/test/java/org/apache/tika/pipes/core/config/ConfigStoreTest.java index 13e58409543..dbaab85dc8e 100644 --- a/tika-pipes/tika-pipes-core/src/test/java/org/apache/tika/pipes/core/config/ConfigStoreTest.java +++ b/tika-pipes/tika-pipes-core/src/test/java/org/apache/tika/pipes/core/config/ConfigStoreTest.java @@ -27,7 +27,10 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.attribute.PosixFilePermission; +import java.util.List; import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -132,7 +135,9 @@ public void testFileBasedConfigStoreIsOwnerOnly(@TempDir Path tmp) throws Except Set perms = Files.getPosixFilePermissions(file); assertEquals(Set.of(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE), perms); - assertFalse(Files.exists(file.resolveSibling("config-store.json.tmp"))); + try (Stream siblings = Files.list(file.getParent())) { + assertEquals(List.of(file), siblings.collect(Collectors.toList())); + } assertEquals("s3-fetcher", new FileBasedConfigStore(file).get("s3").name()); } }