This is an automated email from the ASF dual-hosted git repository.
rzo1 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/storm.git
The following commit(s) were added to refs/heads/master by this push:
new 76a59d2a4 Reject blobstore map local names that resolve outside the
topology and worker directories
76a59d2a4 is described below
commit 76a59d2a43465424b6e34f269bd188fb4279896b
Author: Richard Zowalla <[email protected]>
AuthorDate: Wed Aug 19 13:20:30 2026 +0200
Reject blobstore map local names that resolve outside the topology and
worker directories
---
.../apache/storm/daemon/supervisor/Container.java | 6 +-
.../org/apache/storm/localizer/AsyncLocalizer.java | 4 +-
.../java/org/apache/storm/utils/ServerUtils.java | 39 ++++++++++++
.../storm/daemon/supervisor/ContainerTest.java | 43 +++++++++++++
.../apache/storm/localizer/AsyncLocalizerTest.java | 72 ++++++++++++++++++++++
.../org/apache/storm/utils/ServerUtilsTest.java | 21 +++++++
6 files changed, 182 insertions(+), 3 deletions(-)
diff --git
a/storm-server/src/main/java/org/apache/storm/daemon/supervisor/Container.java
b/storm-server/src/main/java/org/apache/storm/daemon/supervisor/Container.java
index 8b1275b37..d45f1a639 100644
---
a/storm-server/src/main/java/org/apache/storm/daemon/supervisor/Container.java
+++
b/storm-server/src/main/java/org/apache/storm/daemon/supervisor/Container.java
@@ -50,6 +50,7 @@ import org.apache.storm.utils.ConfigUtils;
import org.apache.storm.utils.LocalState;
import org.apache.storm.utils.ObjectReader;
import org.apache.storm.utils.ServerConfigUtils;
+import org.apache.storm.utils.ServerUtils;
import org.apache.storm.utils.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -410,8 +411,9 @@ public abstract class Container implements Killable {
targetResourcesDir.toString());
}
for (String fileName : blobFileNames) {
- ops.createSymlink(new File(workerRoot, fileName),
- new File(stormRoot, fileName));
+ // the localname may come from the topology conf, it must not
point outside of the worker/dist dirs
+
ops.createSymlink(ServerUtils.resolveTopologyConfSuppliedName(new
File(workerRoot), fileName),
+ ServerUtils.resolveTopologyConfSuppliedName(new
File(stormRoot), fileName));
}
} else if (blobFileNames.size() > 0) {
LOG.warn("Symlinks are disabled, no symlinks created for blobs
{}", blobFileNames);
diff --git
a/storm-server/src/main/java/org/apache/storm/localizer/AsyncLocalizer.java
b/storm-server/src/main/java/org/apache/storm/localizer/AsyncLocalizer.java
index c9ad9e1c3..c06e38f3d 100644
--- a/storm-server/src/main/java/org/apache/storm/localizer/AsyncLocalizer.java
+++ b/storm-server/src/main/java/org/apache/storm/localizer/AsyncLocalizer.java
@@ -743,7 +743,9 @@ public class AsyncLocalizer implements AutoCloseable {
// all things are from dependencies
symlinkName = keyName;
}
- fsOps.createSymlink(new File(stormroot,
symlinkName), rsrcFilePath);
+ // the localname may come from the topology conf,
it must not point outside of stormroot
+
fsOps.createSymlink(ServerUtils.resolveTopologyConfSuppliedName(new
File(stormroot), symlinkName),
+ rsrcFilePath);
}
}
}
diff --git a/storm-server/src/main/java/org/apache/storm/utils/ServerUtils.java
b/storm-server/src/main/java/org/apache/storm/utils/ServerUtils.java
index f8d81c9b8..066334c83 100644
--- a/storm-server/src/main/java/org/apache/storm/utils/ServerUtils.java
+++ b/storm-server/src/main/java/org/apache/storm/utils/ServerUtils.java
@@ -36,6 +36,7 @@ import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystems;
import java.nio.file.Files;
+import java.nio.file.InvalidPathException;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.Paths;
@@ -757,6 +758,44 @@ public class ServerUtils {
return sub;
}
+ /**
+ * Resolve a name that came from the topology conf (for example the
"localname" of a topology.blobstore.map entry)
+ * against a base directory. The name is only allowed to point at
something strictly inside the base directory, so
+ * that a topology cannot make the supervisor create a symlink, and force
delete whatever was there before, outside
+ * of the directories the supervisor manages for that topology.
+ *
+ * @param baseDir the directory the name has to resolve inside of
+ * @param name the name from the topology conf
+ * @return the resolved file
+ * @throws IOException if the name is empty, is absolute, contains a ".."
component, or resolves outside of baseDir
+ */
+ public static File resolveTopologyConfSuppliedName(File baseDir, String
name) throws IOException {
+ if (StringUtils.isEmpty(name)) {
+ throw new IOException("Invalid local name, it can't be null or
empty string");
+ }
+ Path namePath;
+ try {
+ namePath = Paths.get(name);
+ } catch (InvalidPathException e) {
+ throw new IOException("Invalid local name '" + name + "'", e);
+ }
+ if (namePath.isAbsolute()) {
+ throw new IOException("Invalid local name '" + name + "', it must
be relative");
+ }
+ for (Path part : namePath) {
+ if ("..".equals(part.toString())) {
+ throw new IOException("Invalid local name '" + name + "', it
can't contain \"..\"");
+ }
+ }
+ File ret = new File(baseDir, name);
+ Path base = baseDir.toPath().toAbsolutePath().normalize();
+ Path resolved = ret.toPath().toAbsolutePath().normalize();
+ if (resolved.equals(base) || !resolved.startsWith(base)) {
+ throw new IOException("Invalid local name '" + name + "', it does
not resolve inside of " + baseDir);
+ }
+ return ret;
+ }
+
// Non-static impl methods exist for mocking purposes.
public String currentClasspathImpl() {
return System.getProperty("java.class.path");
diff --git
a/storm-server/src/test/java/org/apache/storm/daemon/supervisor/ContainerTest.java
b/storm-server/src/test/java/org/apache/storm/daemon/supervisor/ContainerTest.java
index 25c3302c5..bf17ea2a1 100644
---
a/storm-server/src/test/java/org/apache/storm/daemon/supervisor/ContainerTest.java
+++
b/storm-server/src/test/java/org/apache/storm/daemon/supervisor/ContainerTest.java
@@ -37,7 +37,9 @@ import org.junit.jupiter.api.Test;
import org.yaml.snakeyaml.Yaml;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.fail;
+import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
@@ -172,6 +174,47 @@ public class ContainerTest {
verify(ops, never()).createSymlink(new File(workerRoot, "resources"),
new File(distRoot, "resources"));
}
+ @Test
+ public void testCreateBlobstoreLinks() throws Exception {
+ final int port = 8080;
+ final String topoId = "test_topology";
+ final String workerId = "worker_id";
+ final String stormLocal = asAbsPath("tmp", "testing");
+ final File workerRoot = asAbsFile(stormLocal, "workers", workerId);
+ final File distRoot = asAbsFile(stormLocal, "supervisor", "stormdist",
topoId);
+
+ final Map<String, Object> superConf = new HashMap<>();
+ superConf.put(Config.STORM_LOCAL_DIR, stormLocal);
+ superConf.put(Config.STORM_WORKERS_ARTIFACTS_DIR, stormLocal);
+
+ final Map<String, Object> topoConf = new HashMap<>();
+ Map<String, Object> blobInfo = new HashMap<>();
+ blobInfo.put("localname", "simple.txt");
+ topoConf.put(Config.TOPOLOGY_BLOBSTORE_MAP,
Collections.singletonMap("simple", blobInfo));
+
+ AdvancedFSOps ops = mock(AdvancedFSOps.class);
+ when(ops.doRequiredTopoFilesExist(superConf, topoId)).thenReturn(true);
+
+ LocalAssignment la = new LocalAssignment();
+ la.set_topology_id(topoId);
+ ResourceIsolationInterface iso =
mock(ResourceIsolationInterface.class);
+ MockContainer mc = new MockContainer(ContainerType.LAUNCH, superConf,
+ "SUPERVISOR", 6628, port, la,
iso, workerId, topoConf, ops, new StormMetricsRegistry());
+
+ mc.createBlobstoreLinks();
+ verify(ops).createSymlink(new File(workerRoot, "simple.txt"), new
File(distRoot, "simple.txt"));
+
+ //a localname that points outside of the worker root must not result
in any link
+ AdvancedFSOps badOps = mock(AdvancedFSOps.class);
+ when(badOps.doRequiredTopoFilesExist(superConf,
topoId)).thenReturn(true);
+ blobInfo.put("localname", asPath("..", "..", "escaped.txt"));
+ MockContainer badMc = new MockContainer(ContainerType.LAUNCH,
superConf,
+ "SUPERVISOR", 6628, port, la,
iso, workerId, topoConf, badOps, new StormMetricsRegistry());
+
+ assertThrows(IOException.class, () -> badMc.createBlobstoreLinks());
+ verify(badOps, never()).createSymlink(any(File.class),
any(File.class));
+ }
+
@Test
public void testCleanup() throws Exception {
final int supervisorPort = 6628;
diff --git
a/storm-server/src/test/java/org/apache/storm/localizer/AsyncLocalizerTest.java
b/storm-server/src/test/java/org/apache/storm/localizer/AsyncLocalizerTest.java
index be67e0b34..30cfdc4f9 100644
---
a/storm-server/src/test/java/org/apache/storm/localizer/AsyncLocalizerTest.java
+++
b/storm-server/src/test/java/org/apache/storm/localizer/AsyncLocalizerTest.java
@@ -19,6 +19,7 @@ import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
+import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
@@ -30,6 +31,7 @@ import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
@@ -241,6 +243,76 @@ public class AsyncLocalizerTest {
}
+ @Test
+ public void
testRequestDownloadTopologyBlobsWithLocalNameOutsideOfStormRoot() throws
Exception {
+ ConfigUtils mockedConfigUtils = mock(ConfigUtils.class);
+ ConfigUtils previousConfigUtils =
ConfigUtils.setInstance(mockedConfigUtils);
+
+ AsyncLocalizer victim = null;
+
+ try (TmpPath stormLocal = new TmpPath(); TmpPath localizerRoot = new
TmpPath()) {
+
+ Map<String, Object> conf = new HashMap<>();
+ conf.put(Config.STORM_LOCAL_DIR, stormLocal.getPath());
+
+ AdvancedFSOps ops = AdvancedFSOps.make(conf);
+ StormMetricsRegistry metricsRegistry = new StormMetricsRegistry();
+
+ victim = spy(new AsyncLocalizer(conf, ops,
localizerRoot.getPath(), metricsRegistry));
+
+ final String topoId = "TOPO-12345";
+ final String user = "user";
+
+ final Path userDir = Paths.get(stormLocal.getPath(), user);
+ final Path topologyDirRoot = Paths.get(stormLocal.getPath(),
topoId);
+
+ // the localname comes from the topology conf and tries to point
outside of the topology's dist dir
+ final String escapingLocalName =
Joiner.on(File.separator).join("..", "escaped.txt");
+ final String simpleKey = "simple";
+ Map<String, Map<String, Object>> topoBlobMap = new HashMap<>();
+ Map<String, Object> simple = new HashMap<>();
+ simple.put("localname", escapingLocalName);
+ simple.put("uncompress", false);
+ topoBlobMap.put(simpleKey, simple);
+
+ final int port = 8080;
+
+ Map<String, Object> topoConf = new HashMap<>(conf);
+ topoConf.put(Config.TOPOLOGY_BLOBSTORE_MAP, topoBlobMap);
+ topoConf.put(Config.TOPOLOGY_NAME, "TOPO");
+
+ List<LocalizedResource> localizedList = new ArrayList<>();
+ LocalizedResource simpleLocal = new LocalizedResource(simpleKey,
localizerRoot.getFile().toPath(), false, ops, conf, user,
+ metricsRegistry);
+ localizedList.add(simpleLocal);
+
+ when(mockedConfigUtils.supervisorStormDistRootImpl(conf,
topoId)).thenReturn(topologyDirRoot.toString());
+ when(mockedConfigUtils.readSupervisorStormConfImpl(conf,
topoId)).thenReturn(topoConf);
+ when(mockedConfigUtils.readSupervisorTopologyImpl(conf, topoId,
ops)).thenReturn(constructEmptyStormTopology());
+
+ //Write the mocking backwards so the actual method is not called
on the spy object
+ doReturn(CompletableFuture.supplyAsync(() -> null)).when(victim)
+ .requestDownloadBaseTopologyBlobs(any(), eq(null));
+
+ Files.createDirectories(topologyDirRoot);
+
+
doReturn(userDir.toFile()).when(victim).getLocalUserFileCacheDir(user);
+ doReturn(localizedList).when(victim).getBlobs(any(List.class),
any(), any());
+
+ Future<Void> f =
victim.requestDownloadTopologyBlobs(constructLocalAssignment(topoId, user),
port, null);
+ assertThrows(ExecutionException.class, () -> f.get(20,
TimeUnit.SECONDS));
+
+ // nothing was created outside of the topology's dist dir
+
assertFalse(Files.exists(topologyDirRoot.getParent().resolve("escaped.txt"),
LinkOption.NOFOLLOW_LINKS));
+
+ } finally {
+ ConfigUtils.setInstance(previousConfigUtils);
+ if (victim != null) {
+ victim.close();
+ }
+ }
+ }
+
@Test
public void testRequestDownloadTopologyBlobsLocalMode() throws Exception {
// tests download of topology blobs in local mode on a topology
without resources folder
diff --git
a/storm-server/src/test/java/org/apache/storm/utils/ServerUtilsTest.java
b/storm-server/src/test/java/org/apache/storm/utils/ServerUtilsTest.java
index bf4189dc7..10ed0890d 100644
--- a/storm-server/src/test/java/org/apache/storm/utils/ServerUtilsTest.java
+++ b/storm-server/src/test/java/org/apache/storm/utils/ServerUtilsTest.java
@@ -23,6 +23,7 @@ import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
@@ -397,4 +398,24 @@ public class ServerUtilsTest {
}
return false;
}
+
+ @Test
+ public void testResolveTopologyConfSuppliedName() throws Exception {
+ File baseDir = new File(System.getProperty("java.io.tmpdir"),
"stormdist/topo-1-1");
+
+ assertEquals(new File(baseDir, "myblob"),
ServerUtils.resolveTopologyConfSuppliedName(baseDir, "myblob"));
+ assertEquals(new File(baseDir, "resources/myblob"),
ServerUtils.resolveTopologyConfSuppliedName(baseDir, "resources/myblob"));
+
+ assertThrows(IOException.class, () ->
ServerUtils.resolveTopologyConfSuppliedName(baseDir, ".."));
+ assertThrows(IOException.class, () ->
ServerUtils.resolveTopologyConfSuppliedName(baseDir,
"../other-topo/stormjar.jar"));
+ assertThrows(IOException.class, () ->
ServerUtils.resolveTopologyConfSuppliedName(baseDir, "a/../../../etc/passwd"));
+ assertThrows(IOException.class, () ->
ServerUtils.resolveTopologyConfSuppliedName(baseDir, "a/../b/../.."));
+ //a sibling directory whose name only starts with the base directory
name is not inside it
+ assertThrows(IOException.class, () ->
ServerUtils.resolveTopologyConfSuppliedName(baseDir,
"../topo-1-1-evil/stormjar.jar"));
+ assertThrows(IOException.class, () ->
ServerUtils.resolveTopologyConfSuppliedName(baseDir, "."));
+ assertThrows(IOException.class,
+ () -> ServerUtils.resolveTopologyConfSuppliedName(baseDir, new
File("etc", "passwd").getAbsolutePath()));
+ assertThrows(IOException.class, () ->
ServerUtils.resolveTopologyConfSuppliedName(baseDir, ""));
+ assertThrows(IOException.class, () ->
ServerUtils.resolveTopologyConfSuppliedName(baseDir, null));
+ }
}