CVE-2024-24565 (Java, published 2024-01-30T17:15Z)
Description: 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.
Severity: nan (CVSS v3 base score: 6.5)
Weakness: CWE-22 \u2014 Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
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.
Repository: https://github.com/crate/crate
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 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.


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");
        }
    }