CVE-2024-24565: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal') (CWE-22)
value: CrateDB is a distributed SQL database that makes it simple to store and analyze massive amounts of data in real-time. There is a COPY FROM function in the CrateDB database that is used to import file data into database tables. This function has a flaw, and authenticated attackers can use the COPY FROM function to import arbitrary file content into database tables, resulting in information leakage. This vulnerability is patched in 5.3.9, 5.4.8, 5.5.4, and 5.6.1.
CWE description: The product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory.
CVSS v3 base score: 6.5
Published: 2024-01-30T17:15Z
Repository: https://github.com/crate/crate
Language: Java
Files changed: docs/appendices/release-notes/5.5.4.rst, docs/appendices/release-notes/5.6.1.rst, docs/sql/statements/copy-from.rst, plugins/cr8-copy-s3/src/test/java/io/crate/copy/s3/S3FileReadingCollectorTest.java, server/src/main/java/io/crate/exceptions/UnauthorizedException.java, server/src/main/java/io/crate/execution/engine/collect/files/FileReadingIterator.java, server/src/main/java/io/crate/execution/engine/collect/sources/FileCollectSource.java, server/src/main/java/org/elasticsearch/ElasticsearchException.java, server/src/test/java/io/crate/execution/engine/collect/MapSideDataCollectOperationTest.java, server/src/test/java/io/crate/execution/engine/collect/files/FileReadingCollectorTest.java, server/src/test/java/io/crate/execution/engine/collect/files/FileReadingIteratorTest.java, server/src/test/java/io/crate/execution/engine/collect/sources/FileCollectSourceTest.java, server/src/test/java/io/crate/integrationtests/CopyIntegrationTest.java
Commit date: 2024-01-29 14:13:34 +0100
Commit message: Restrict `COPY FROM` using local files to superuser

Fixing a security issue where any user could read/import content
of any file on the host system, the CrateDB process user has read
access to.

Diff: diff --git a/docs/appendices/release-notes/5.5.4.rst b/docs/appendices/release-notes/5.5.4.rst
index d6f37dd6ba..6bfb7e8fc0 100644
--- a/docs/appendices/release-notes/5.5.4.rst
+++ b/docs/appendices/release-notes/5.5.4.rst
@@ -42,10 +42,18 @@ Version 5.5.4 - Unreleased
    :local:
 
 See the :ref:`version_5.5.0` release notes for a full list of changes in the
 5.5 series.
 
+Security Fixes
+==============
+
+- Fixed a security issue where any CrateDB user could read/import the content of
+  any file on the host system, the CrateDB process user has read access to, by
+  using the ``COPY FROM`` command with a file URI. This access is now restricted
+  to the ``crate`` superuser only.
+
 Fixes
 =====
 
 - Fixed an issue that caused ``SELECT`` statements with ``WHERE``
   clause having an equality condition on a primary key to return ``NULL`` when
diff --git a/docs/appendices/release-notes/5.6.1.rst b/docs/appendices/release-notes/5.6.1.rst
index 267b93636c..f3d781560d 100644
--- a/docs/appendices/release-notes/5.6.1.rst
+++ b/docs/appendices/release-notes/5.6.1.rst
@@ -41,10 +41,17 @@ Version 5.6.1 - Unreleased
    :local:
 
 See the :ref:`version_5.6.0` release notes for a full list of changes in the
 5.6 series.
 
+Security Fixes
+==============
+
+- Fixed a security issue where any CrateDB user could read/import the content of
+  any file on the host system, the CrateDB process user has read access to, by
+  using the ``COPY FROM`` command with a file URI. This access is now restricted
+  to the ``crate`` superuser only.
 
 Fixes
 =====
 
 - Added a workaround for a change in JDK 21.0.2 which caused many operations to
diff --git a/docs/sql/statements/copy-from.rst b/docs/sql/statements/copy-from.rst
index 27f8512209..705f4661e8 100644
--- a/docs/sql/statements/copy-from.rst
+++ b/docs/sql/statements/copy-from.rst
@@ -209,10 +209,12 @@ For example:
 
     file:///path/to/dir
 
 The files must be accessible on at least one node and the system user running
 the ``crate`` process must have read access to every file specified.
+Additionally, only the ``crate`` superuser is allowed to use the ``file://``
+scheme.
 
 By default, every node will attempt to import every file. If the file is
 accessible on multiple nodes, you can set the `shared`_ option to true in order
 to avoid importing duplicates.
 
diff --git a/plugins/cr8-copy-s3/src/test/java/io/crate/copy/s3/S3FileReadingCollectorTest.java b/plugins/cr8-copy-s3/src/test/java/io/crate/copy/s3/S3FileReadingCollectorTest.java
index 36ca60acec..67b5df0c2b 100644
--- a/plugins/cr8-copy-s3/src/test/java/io/crate/copy/s3/S3FileReadingCollectorTest.java
+++ b/plugins/cr8-copy-s3/src/test/java/io/crate/copy/s3/S3FileReadingCollectorTest.java
@@ -104,11 +104,11 @@ public class S3FileReadingCollectorTest extends ESTestCase {
 
 
     private FileReadingIterator createBatchIterator(S3ObjectInputStream inputStream, String ... fileUris) {
         String compression = null;
         return new FileReadingIterator(
-            Arrays.asList(fileUris),
+            Arrays.stream(fileUris).map(FileReadingIterator::toURI).toList(),
             compression,
             Map.of(
                 S3FileInputFactory.NAME,
                 (uri, withClauseOptions) -> new S3FileInput(new S3ClientHelper() {
                     @Override
diff --git a/server/src/main/java/io/crate/exceptions/UnauthorizedException.java b/server/src/main/java/io/crate/exceptions/UnauthorizedException.java
index c30d44fdcc..a052ec955c 100644
--- a/server/src/main/java/io/crate/exceptions/UnauthorizedException.java
+++ b/server/src/main/java/io/crate/exceptions/UnauthorizedException.java
@@ -19,12 +19,20 @@
  * software solely pursuant to the terms of the relevant commercial agreement.
  */
 
 package io.crate.exceptions;
 
-public class UnauthorizedException extends RuntimeException implements UnscopedException {
+import java.io.IOException;
+
+import org.elasticsearch.ElasticsearchException;
+import org.elasticsearch.common.io.stream.StreamInput;
+
+public class UnauthorizedException extends ElasticsearchException implements UnscopedException {
 
     public UnauthorizedException(String message) {
         super(message);
     }
 
+    public UnauthorizedException(StreamInput in) throws IOException {
+        super(in);
+    }
 }
diff --git a/server/src/main/java/io/crate/execution/engine/collect/files/FileReadingIterator.java b/server/src/main/java/io/crate/execution/engine/collect/files/FileReadingIterator.java
index 7df571fcc0..cf3e3e7cb1 100644
--- a/server/src/main/java/io/crate/execution/engine/collect/files/FileReadingIterator.java
+++ b/server/src/main/java/io/crate/execution/engine/collect/files/FileReadingIterator.java
@@ -173,11 +173,11 @@ public class FileReadingIterator implements BatchIterator<FileReadingIterator.Li
                 && Objects.equals(line, other.line)
                 && Objects.equals(failure, other.failure);
         }
     }
 
-    public FileReadingIterator(Collection<String> fileUris,
+    public FileReadingIterator(Collection<URI> fileUris,
                                String compression,
                                Map<String, FileInputFactory> fileInputFactories,
                                Boolean shared,
                                int numReaders,
                                int readerNumber,
@@ -396,12 +396,11 @@ public class FileReadingIterator implements BatchIterator<FileReadingIterator.Li
             return uri;
         }
     }
 
     @Nullable
-    private FileInput toFileInput(String fileUri, Settings withClauseOptions) {
-        URI uri = toURI(fileUri);
+    private FileInput toFileInput(URI uri, Settings withClauseOptions) {
         FileInputFactory fileInputFactory = fileInputFactories.get(uri.getScheme());
         if (fileInputFactory != null) {
             try {
                 return fileInputFactory.create(uri, withClauseOptions);
             } catch (IOException e) {
diff --git a/server/src/main/java/io/crate/execution/engine/collect/sources/FileCollectSource.java b/server/src/main/java/io/crate/execution/engine/collect/sources/FileCollectSource.java
index 887e4be2cf..2657c9690b 100644
--- a/server/src/main/java/io/crate/execution/engine/collect/sources/FileCollectSource.java
+++ b/server/src/main/java/io/crate/execution/engine/collect/sources/FileCollectSource.java
@@ -19,10 +19,13 @@
  * software solely pursuant to the terms of the relevant commercial agreement.
  */
 
 package io.crate.execution.engine.collect.sources;
 
+import static java.util.Objects.requireNonNull;
+
+import java.net.URI;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
@@ -38,10 +41,11 @@ import io.crate.analyze.CopyFromParserProperties;
 import io.crate.analyze.SymbolEvaluator;
 import io.crate.common.annotations.VisibleForTesting;
 import io.crate.data.BatchIterator;
 import io.crate.data.Row;
 import io.crate.data.SkippingBatchIterator;
+import io.crate.exceptions.UnauthorizedException;
 import io.crate.execution.dsl.phases.CollectPhase;
 import io.crate.execution.dsl.phases.FileUriCollectPhase;
 import io.crate.execution.engine.collect.CollectTask;
 import io.crate.execution.engine.collect.files.FileInputFactory;
 import io.crate.execution.engine.collect.files.FileReadingIterator;
@@ -51,31 +55,36 @@ import io.crate.expression.InputFactory;
 import io.crate.expression.reference.file.FileLineReferenceResolver;
 import io.crate.expression.symbol.Symbol;
 import io.crate.metadata.NodeContext;
 import io.crate.metadata.TransactionContext;
 import io.crate.planner.operators.SubQueryResults;
+import io.crate.role.Role;
+import io.crate.role.Roles;
 import io.crate.types.DataTypes;
 
 @Singleton
 public class FileCollectSource implements CollectSource {
 
     private final ClusterService clusterService;
     private final Map<String, FileInputFactory> fileInputFactoryMap;
     private final InputFactory inputFactory;
     private final NodeContext nodeCtx;
     private final ThreadPool threadPool;
+    private final Roles roles;
 
     @Inject
     public FileCollectSource(NodeContext nodeCtx,
                              ClusterService clusterService,
                              Map<String, FileInputFactory> fileInputFactoryMap,
-                             ThreadPool threadPool) {
+                             ThreadPool threadPool,
+                             Roles roles) {
         this.fileInputFactoryMap = fileInputFactoryMap;
         this.nodeCtx = nodeCtx;
         this.inputFactory = new InputFactory(nodeCtx);
         this.clusterService = clusterService;
         this.threadPool = threadPool;
+        this.roles = roles;
     }
 
     @Override
     public CompletableFuture<BatchIterator<Row>> getIterator(TransactionContext txnCtx,
                                                              CollectPhase collectPhase,
@@ -84,11 +93,20 @@ public class FileCollectSource implements CollectSource {
         FileUriCollectPhase fileUriCollectPhase = (FileUriCollectPhase) collectPhase;
         InputFactory.Context<LineCollectorExpression<?>> ctx =
             inputFactory.ctxForRefs(txnCtx, FileLineReferenceResolver::getImplementation);
         ctx.add(collectPhase.toCollect());
 
-        List<String> fileUris = targetUriToStringList(txnCtx, nodeCtx, fileUriCollectPhase.targetUri());
+        Role user = requireNonNull(roles.findUser(txnCtx.sessionSettings().userName()), "User who invoked a statement must exist");
+        List<URI> fileUris = targetUriToStringList(txnCtx, nodeCtx, fileUriCollectPhase.targetUri()).stream()
+            .map(s -> {
+                var uri = FileReadingIterator.toURI(s);
+                if (uri.getScheme().equals("file") && user.isSuperUser() == false) {
+                    throw new UnauthorizedException("Only a superuser can read from the local file system");
+                }
+                return uri;
+            })
+            .toList();
         FileReadingIterator fileReadingIterator = new FileReadingIterator(
             fileUris,
             fileUriCollectPhase.compression(),
             fileInputFactoryMap,
             fileUriCollectPhase.sharedStorage(),
diff --git a/server/src/main/java/org/elasticsearch/ElasticsearchException.java b/server/src/main/java/org/elasticsearch/ElasticsearchException.java
index 38fb3a0a48..2f422aff90 100644
--- a/server/src/main/java/org/elasticsearch/ElasticsearchException.java
+++ b/server/src/main/java/org/elasticsearch/ElasticsearchException.java
@@ -971,11 +971,16 @@ public class ElasticsearchException extends RuntimeException implements ToXConte
             Version.V_5_2_0),
         OPERATION_ON_INACCESSIBLE_RELATION_EXCEPTION(
             io.crate.exceptions.OperationOnInaccessibleRelationException.class,
             io.crate.exceptions.OperationOnInaccessibleRelationException::new,
             176,
-            Version.V_5_6_0);
+            Version.V_5_6_0),
+        UNAUTHORIZED_EXCEPTION(
+            io.crate.exceptions.UnauthorizedException.class,
+            io.crate.exceptions.UnauthorizedException::new,
+            177,
+            Version.V_5_7_0);
 
         final Class<? extends ElasticsearchException> exceptionClass;
         final CheckedFunction<StreamInput, ? extends ElasticsearchException, IOException> constructor;
         final int id;
         final Version versionAdded;
diff --git a/server/src/test/java/io/crate/execution/engine/collect/MapSideDataCollectOperationTest.java b/server/src/test/java/io/crate/execution/engine/collect/MapSideDataCollectOperationTest.java
index 009c26b9e3..6d40a834f2 100644
--- a/server/src/test/java/io/crate/execution/engine/collect/MapSideDataCollectOperationTest.java
+++ b/server/src/test/java/io/crate/execution/engine/collect/MapSideDataCollectOperationTest.java
@@ -52,10 +52,11 @@ import io.crate.data.testing.TestingRowConsumer;
 import io.crate.execution.dsl.phases.FileUriCollectPhase;
 import io.crate.execution.engine.collect.sources.FileCollectSource;
 import io.crate.expression.symbol.Literal;
 import io.crate.metadata.ColumnIdent;
 import io.crate.metadata.CoordinatorTxnCtx;
+import io.crate.role.Role;
 import io.crate.test.integration.CrateDummyClusterServiceUnitTest;
 import io.crate.types.DataTypes;
 
 
 public class MapSideDataCollectOperationTest extends CrateDummyClusterServiceUnitTest {
@@ -67,11 +68,12 @@ public class MapSideDataCollectOperationTest extends CrateDummyClusterServiceUni
     public void testFileUriCollect() throws Exception {
         FileCollectSource fileCollectSource = new FileCollectSource(
             createNodeContext(),
             clusterService,
             Collections.emptyMap(),
-            THREAD_POOL
+            THREAD_POOL,
+            () -> List.of(Role.CRATE_USER)
             );
 
         File tmpFile = temporaryFolder.newFile("fileUriCollectOperation.json");
         try (OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(tmpFile), StandardCharsets.UTF_8)) {
             writer.write("{\"name\": \"Arthur\", \"id\": 4, \"details\": {\"age\": 38}}\n");
diff --git a/server/src/test/java/io/crate/execution/engine/collect/files/FileReadingCollectorTest.java b/server/src/test/java/io/crate/execution/engine/collect/files/FileReadingCollectorTest.java
index 9d93b6820a..f03e25eb6f 100644
--- a/server/src/test/java/io/crate/execution/engine/collect/files/FileReadingCollectorTest.java
+++ b/server/src/test/java/io/crate/execution/engine/collect/files/FileReadingCollectorTest.java
@@ -198,11 +198,11 @@ public class FileReadingCollectorTest extends ESTestCase {
         return it(Arrays.asList(fileUris), null);
     }
 
     private static FileReadingIterator it(Collection<String> fileUris, String compression) {
         return new FileReadingIterator(
-            fileUris,
+            fileUris.stream().map(FileReadingIterator::toURI).toList(),
             compression,
             Map.of(LocalFsFileInputFactory.NAME, new LocalFsFileInputFactory()),
             false,
             1,
             0,
diff --git a/server/src/test/java/io/crate/execution/engine/collect/files/FileReadingIteratorTest.java b/server/src/test/java/io/crate/execution/engine/collect/files/FileReadingIteratorTest.java
index 1fcddb22a1..c75349625a 100644
--- a/server/src/test/java/io/crate/execution/engine/collect/files/FileReadingIteratorTest.java
+++ b/server/src/test/java/io/crate/execution/engine/collect/files/FileReadingIteratorTest.java
@@ -32,19 +32,21 @@ import static org.mockito.Mockito.verify;
 import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.net.SocketTimeoutException;
+import java.net.URI;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
 import java.util.function.Supplier;
+import java.util.stream.Stream;
 
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.test.ESTestCase;
 import org.elasticsearch.threadpool.TestThreadPool;
 import org.elasticsearch.threadpool.ThreadPool;
@@ -85,11 +87,13 @@ public class FileReadingIteratorTest extends ESTestCase {
         );
         Files.write(tempFile1, lines1);
         Path tempFile2 = createTempFile("tempfile2", ".csv");
         List<String> lines2 = List.of("name,id,age", "Trillian,5,33");
         Files.write(tempFile2, lines2);
-        List<String> fileUris = List.of(tempFile1.toUri().toString(), tempFile2.toUri().toString());
+        List<URI> fileUris = Stream.of(tempFile1.toUri().toString(), tempFile2.toUri().toString())
+            .map(FileReadingIterator::toURI)
+            .toList();
 
         Supplier<BatchIterator<LineCursor>> batchIteratorSupplier =
             () -> new FileReadingIterator(
                 fileUris,
                 null,
@@ -137,11 +141,12 @@ public class FileReadingIteratorTest extends ESTestCase {
     @Test
     public void test_consecutive_retries_will_not_result_in_duplicate_reads() throws Exception {
         Path tempFile = createTempFile("tempfile1", ".csv");
         List<String> lines = List.of("id", "1", "2", "3", "4", "5");
         Files.write(tempFile, lines);
-        List<String> fileUris = List.of(tempFile.toUri().toString());
+        List<URI> fileUris = Stream.of(tempFile.toUri().toString())
+            .map(FileReadingIterator::toURI).toList();
 
         Supplier<BatchIterator<LineCursor>> batchIteratorSupplier =
             () -> new FileReadingIterator(
                 fileUris,
                 null,
@@ -211,11 +216,12 @@ public class FileReadingIteratorTest extends ESTestCase {
     public void test_retry_from_one_uri_does_not_affect_reading_next_uri() throws Exception {
         Path tempFile = createTempFile("tempfile1", ".csv");
         Files.write(tempFile, List.of("1", "2", "3"));
         Path tempFile2 = createTempFile("tempfile2", ".csv");
         Files.write(tempFile2, List.of("4", "5", "6"));
-        List<String> fileUris = List.of(tempFile.toUri().toString(), tempFile2.toUri().toString());
+        List<URI> fileUris = Stream.of(tempFile.toUri().toString(), tempFile2.toUri().toString())
+            .map(FileReadingIterator::toURI).toList();
 
         var fi = new FileReadingIterator(
             fileUris,
             null,
             Map.of(LocalFsFileInputFactory.NAME, new LocalFsFileInputFactory()),
diff --git a/server/src/test/java/io/crate/execution/engine/collect/sources/FileCollectSourceTest.java b/server/src/test/java/io/crate/execution/engine/collect/sources/FileCollectSourceTest.java
index 3a0e808598..35b0a71a0d 100644
--- a/server/src/test/java/io/crate/execution/engine/collect/sources/FileCollectSourceTest.java
+++ b/server/src/test/java/io/crate/execution/engine/collect/sources/FileCollectSourceTest.java
@@ -90,11 +90,12 @@ public class FileCollectSourceTest extends CrateDummyClusterServiceUnitTest {
         Roles roles = () -> List.of(Role.CRATE_USER);
         FileCollectSource fileCollectSource = new FileCollectSource(
             new NodeContext(new Functions(Map.of()), roles),
             clusterService,
             Map.of(),
-            THREAD_POOL
+            THREAD_POOL,
+            () -> List.of(Role.CRATE_USER)
         );
 
         CompletableFuture<BatchIterator<Row>> iterator = fileCollectSource.getIterator(
             CoordinatorTxnCtx.systemTransactionContext(),
             fileUriCollectPhase,
diff --git a/server/src/test/java/io/crate/integrationtests/CopyIntegrationTest.java b/server/src/test/java/io/crate/integrationtests/CopyIntegrationTest.java
index 5601aa4cff..a67325f627 100644
--- a/server/src/test/java/io/crate/integrationtests/CopyIntegrationTest.java
+++ b/server/src/test/java/io/crate/integrationtests/CopyIntegrationTest.java
@@ -24,10 +24,11 @@ package io.crate.integrationtests;
 import static com.carrotsearch.randomizedtesting.RandomizedTest.newTempDir;
 import static io.crate.protocols.postgres.PGErrorStatus.INTERNAL_ERROR;
 import static io.crate.testing.Asserts.assertThat;
 import static io.crate.testing.TestingHelpers.printedTable;
 import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
 import java.io.File;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.OutputStreamWriter;
@@ -51,14 +52,19 @@ import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.TemporaryFolder;
 
 import com.carrotsearch.randomizedtesting.LifecycleScope;
 
+import io.crate.action.sql.Sessions;
+import io.crate.exceptions.UnauthorizedException;
+import io.crate.role.Role;
+import io.crate.role.Roles;
 import io.crate.testing.Asserts;
 import io.crate.testing.SQLResponse;
 import io.crate.testing.UseJdbc;
 import io.crate.testing.UseNewCluster;
+import io.crate.testing.UseRandomizedSchema;
 
 @IntegTestCase.ClusterScope(numDataNodes = 2)
 public class CopyIntegrationTest extends SQLHttpIntegrationTest {
 
     private final String copyFilePath =
@@ -1194,6 +1200,24 @@ public class CopyIntegrationTest extends SQLHttpIntegrationTest {
         assertThat(response).hasRows(
             "1| 38392| apple safari| 151155",
             "2| 31123| apple safari| 23073"
         );
     }
+
+    @UseRandomizedSchema(random = false)
+    @Test
+    public void test_copy_from_local_file_is_only_allowed_for_superusers() {
+        execute("CREATE TABLE quotes (id INT PRIMARY KEY, " +
+            "quote STRING INDEX USING FULLTEXT) WITH (number_of_replicas = 0)");
+        execute("CREATE USER test_user");
+        execute("GRANT ALL TO test_user");
+
+        var roles = cluster().getInstance(Roles.class);
+        Role user = roles.findUser("test_user");
+        Sessions sqlOperations = cluster().getInstance(Sessions.class);
+        try (var session = sqlOperations.newSession(null, user)) {
+            assertThatThrownBy(() -> execute("COPY quotes FROM ?", new Object[]{copyFilePath + "test_copy_from.json"}, session))
+                .isExactlyInstanceOf(UnauthorizedException.class)
+                .hasMessage("Only a superuser can read from the local file system");
+        }
+    }
 }

Vulnerable code:             Arrays.asList(fileUris),
public class UnauthorizedException extends RuntimeException implements UnscopedException {
    public FileReadingIterator(Collection<String> fileUris,
    private FileInput toFileInput(String fileUri, Settings withClauseOptions) {
        URI uri = toURI(fileUri);
                             ThreadPool threadPool) {
        List<String> fileUris = targetUriToStringList(txnCtx, nodeCtx, fileUriCollectPhase.targetUri());
            Version.V_5_6_0);
            THREAD_POOL
            fileUris,
        List<String> fileUris = List.of(tempFile1.toUri().toString(), tempFile2.toUri().toString());
        List<String> fileUris = List.of(tempFile.toUri().toString());
        List<String> fileUris = List.of(tempFile.toUri().toString(), tempFile2.toUri().toString());
            THREAD_POOL
Fixed code: Security Fixes
==============

- Fixed a security issue where any CrateDB user could read/import the content of
  any file on the host system, the CrateDB process user has read access to, by
  using the ``COPY FROM`` command with a file URI. This access is now restricted
  to the ``crate`` superuser only.

Security Fixes
==============

- Fixed a security issue where any CrateDB user could read/import the content of
  any file on the host system, the CrateDB process user has read access to, by
  using the ``COPY FROM`` command with a file URI. This access is now restricted
  to the ``crate`` superuser only.
Additionally, only the ``crate`` superuser is allowed to use the ``file://``
scheme.
            Arrays.stream(fileUris).map(FileReadingIterator::toURI).toList(),
import java.io.IOException;

import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.common.io.stream.StreamInput;

public class UnauthorizedException extends ElasticsearchException implements UnscopedException {
    public UnauthorizedException(StreamInput in) throws IOException {
        super(in);
    }
    public FileReadingIterator(Collection<URI> fileUris,
    private FileInput toFileInput(URI uri, Settings withClauseOptions) {
import static java.util.Objects.requireNonNull;

import java.net.URI;
import io.crate.exceptions.UnauthorizedException;
import io.crate.role.Role;
import io.crate.role.Roles;
    private final Roles roles;
                             ThreadPool threadPool,
                             Roles roles) {
        this.roles = roles;
        Role user = requireNonNull(roles.findUser(txnCtx.sessionSettings().userName()), "User who invoked a statement must exist");
        List<URI> fileUris = targetUriToStringList(txnCtx, nodeCtx, fileUriCollectPhase.targetUri()).stream()
            .map(s -> {
                var uri = FileReadingIterator.toURI(s);
                if (uri.getScheme().equals("file") && user.isSuperUser() == false) {
                    throw new UnauthorizedException("Only a superuser can read from the local file system");
                }
                return uri;
            })
            .toList();
            Version.V_5_6_0),
        UNAUTHORIZED_EXCEPTION(
            io.crate.exceptions.UnauthorizedException.class,
            io.crate.exceptions.UnauthorizedException::new,
            177,
            Version.V_5_7_0);
import io.crate.role.Role;
            THREAD_POOL,
            () -> List.of(Role.CRATE_USER)
            fileUris.stream().map(FileReadingIterator::toURI).toList(),
import java.net.URI;
import java.util.stream.Stream;
        List<URI> fileUris = Stream.of(tempFile1.toUri().toString(), tempFile2.toUri().toString())
            .map(FileReadingIterator::toURI)
            .toList();
        List<URI> fileUris = Stream.of(tempFile.toUri().toString())
            .map(FileReadingIterator::toURI).toList();
        List<URI> fileUris = Stream.of(tempFile.toUri().toString(), tempFile2.toUri().toString())
            .map(FileReadingIterator::toURI).toList();
            THREAD_POOL,
            () -> List.of(Role.CRATE_USER)
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import io.crate.action.sql.Sessions;
import io.crate.exceptions.UnauthorizedException;
import io.crate.role.Role;
import io.crate.role.Roles;
import io.crate.testing.UseRandomizedSchema;

    @UseRandomizedSchema(random = false)
    @Test
    public void test_copy_from_local_file_is_only_allowed_for_superusers() {
        execute("CREATE TABLE quotes (id INT PRIMARY KEY, " +
            "quote STRING INDEX USING FULLTEXT) WITH (number_of_replicas = 0)");
        execute("CREATE USER test_user");
        execute("GRANT ALL TO test_user");

        var roles = cluster().getInstance(Roles.class);
        Role user = roles.findUser("test_user");
        Sessions sqlOperations = cluster().getInstance(Sessions.class);
        try (var session = sqlOperations.newSession(null, user)) {
            assertThatThrownBy(() -> execute("COPY quotes FROM ?", new Object[]{copyFilePath + "test_copy_from.json"}, session))
                .isExactlyInstanceOf(UnauthorizedException.class)
                .hasMessage("Only a superuser can read from the local file system");
        }
    }