Skip to content

Commit

Permalink
SOLR-16949: Restrict certain file types from being uploaded to or dow…
Browse files Browse the repository at this point in the history
…nloaded from Config Sets

(cherry picked from commit 1553475)
  • Loading branch information
janhoy committed Dec 13, 2023
1 parent 36759d5 commit 644dd3a
Show file tree
Hide file tree
Showing 23 changed files with 522 additions and 56 deletions.
2 changes: 2 additions & 0 deletions solr/CHANGES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ Other Changes
* SOLR-17091: dev tools script cloud.sh became broken after changes in 9.3 added a new -slim.tgz file it was not expecting
cloud.sh has been updated to ignore the -slim.tgz version of the tarball.

* SOLR-16949: Restrict certain file types from being uploaded to or downloaded from Config Sets (janhoy, Houston Putman)

================== 9.4.0 ==================
New Features
---------------------
Expand Down
2 changes: 2 additions & 0 deletions solr/core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,8 @@ dependencies {

compileOnly 'com.github.stephenc.jcip:jcip-annotations'

implementation 'com.j256.simplemagic:simplemagic'

// -- Test Dependencies

testRuntimeOnly 'org.slf4j:jcl-over-slf4j'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import org.apache.solr.common.cloud.SolrZkClient;
import org.apache.solr.common.cloud.ZkMaintenanceUtils;
import org.apache.solr.core.ConfigSetService;
import org.apache.solr.util.FileTypeMagicUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand Down Expand Up @@ -100,6 +101,7 @@ public void runImpl(CommandLine cli) throws Exception {
+ cli.getOptionValue("confname")
+ " to ZooKeeper at "
+ zkHost);
FileTypeMagicUtil.assertConfigSetFolderLegal(confPath);
ZkMaintenanceUtils.uploadToZK(
zkClient,
confPath,
Expand Down
21 changes: 20 additions & 1 deletion solr/core/src/java/org/apache/solr/cloud/ZkConfigSetService.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import org.apache.solr.client.solrj.cloud.SolrCloudManager;
Expand All @@ -39,6 +40,7 @@
import org.apache.solr.core.CoreDescriptor;
import org.apache.solr.core.SolrConfig;
import org.apache.solr.core.SolrResourceLoader;
import org.apache.solr.util.FileTypeMagicUtil;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.data.Stat;
Expand Down Expand Up @@ -199,6 +201,15 @@ public void uploadFileToConfig(
try {
if (ZkMaintenanceUtils.isFileForbiddenInConfigSets(fileName)) {
log.warn("Not including uploading file to config, as it is a forbidden type: {}", fileName);
} else if (FileTypeMagicUtil.isFileForbiddenInConfigset(data)) {
String mimeType = FileTypeMagicUtil.INSTANCE.guessMimeType(data);
throw new SolrException(
SolrException.ErrorCode.BAD_REQUEST,
String.format(
Locale.ROOT,
"Not uploading file %s to config, as it matched the MAGIC signature of a forbidden mime type %s",
fileName,
mimeType));
} else {
// if overwriteOnExists is true then zkClient#makePath failOnExists is set to false
zkClient.makePath(filePath, data, CreateMode.PERSISTENT, null, !overwriteOnExists, true);
Expand Down Expand Up @@ -340,7 +351,15 @@ private void copyData(String fromZkFilePath, String toZkFilePath)
} else {
log.debug("Copying zk node {} to {}", fromZkFilePath, toZkFilePath);
byte[] data = zkClient.getData(fromZkFilePath, null, null, true);
zkClient.makePath(toZkFilePath, data, true);
if (!FileTypeMagicUtil.isFileForbiddenInConfigset(data)) {
zkClient.makePath(toZkFilePath, data, true);
} else {
String mimeType = FileTypeMagicUtil.INSTANCE.guessMimeType(data);
log.warn(
"Skipping copy of file {} in ZK, as it matched the MAGIC signature of a forbidden mime type {}",
fromZkFilePath,
mimeType);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import org.apache.solr.common.SolrException;
import org.apache.solr.common.cloud.ZkMaintenanceUtils;
import org.apache.solr.common.util.Utils;
import org.apache.solr.util.FileTypeMagicUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand Down Expand Up @@ -150,9 +151,17 @@ public void uploadFileToConfig(
if (ZkMaintenanceUtils.isFileForbiddenInConfigSets(fileName)) {
log.warn("Not including uploading file to config, as it is a forbidden type: {}", fileName);
} else {
Path filePath = getConfigDir(configName).resolve(normalizePathToOsSeparator(fileName));
if (!Files.exists(filePath) || overwriteOnExists) {
Files.write(filePath, data);
if (!FileTypeMagicUtil.isFileForbiddenInConfigset(data)) {
Path filePath = getConfigDir(configName).resolve(normalizePathToOsSeparator(fileName));
if (!Files.exists(filePath) || overwriteOnExists) {
Files.write(filePath, data);
}
} else {
String mimeType = FileTypeMagicUtil.INSTANCE.guessMimeType(data);
log.warn(
"Not including uploading file {}, as it matched the MAGIC signature of a forbidden mime type {}",
fileName,
mimeType);
}
}
}
Expand Down Expand Up @@ -205,8 +214,17 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
"Not including uploading file to config, as it is a forbidden type: {}",
file.getFileName());
} else {
Files.copy(
file, target.resolve(source.relativize(file).toString()), REPLACE_EXISTING);
if (!FileTypeMagicUtil.isFileForbiddenInConfigset(Files.newInputStream(file))) {
Files.copy(
file, target.resolve(source.relativize(file).toString()), REPLACE_EXISTING);
} else {
String mimeType =
FileTypeMagicUtil.INSTANCE.guessMimeType(Files.newInputStream(file));
log.warn(
"Not copying file {}, as it matched the MAGIC signature of a forbidden mime type {}",
file.getFileName(),
mimeType);
}
}
return FileVisitResult.CONTINUE;
}
Expand Down
23 changes: 20 additions & 3 deletions solr/core/src/java/org/apache/solr/core/backup/BackupManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
import org.apache.solr.common.util.Utils;
import org.apache.solr.core.ConfigSetService;
import org.apache.solr.core.backup.repository.BackupRepository;
import org.apache.solr.util.FileTypeMagicUtil;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.slf4j.Logger;
Expand Down Expand Up @@ -349,8 +350,16 @@ private void downloadConfigToRepo(ConfigSetService configSetService, String conf
if (data == null) {
data = new byte[0];
}
try (OutputStream os = repository.createOutput(uri)) {
os.write(data);
if (!FileTypeMagicUtil.isFileForbiddenInConfigset(data)) {
try (OutputStream os = repository.createOutput(uri)) {
os.write(data);
}
} else {
String mimeType = FileTypeMagicUtil.INSTANCE.guessMimeType(data);
log.warn(
"Not including zookeeper file {} in backup, as it matched the MAGIC signature of a forbidden mime type {}",
filePath,
mimeType);
}
}
} else {
Expand Down Expand Up @@ -379,7 +388,15 @@ private void uploadConfigToSolrCloud(
// probably ok since the config file should be small.
byte[] arr = new byte[(int) is.length()];
is.readBytes(arr, 0, (int) is.length());
configSetService.uploadFileToConfig(configName, filePath, arr, false);
if (!FileTypeMagicUtil.isFileForbiddenInConfigset(arr)) {
configSetService.uploadFileToConfig(configName, filePath, arr, false);
} else {
String mimeType = FileTypeMagicUtil.INSTANCE.guessMimeType(arr);
log.warn(
"Not including zookeeper file {} in restore, as it matched the MAGIC signature of a forbidden mime type {}",
filePath,
mimeType);
}
}
}
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import org.apache.solr.core.CoreContainer;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.response.SolrQueryResponse;
import org.apache.solr.util.FileTypeMagicUtil;

/**
* V2 API for adding or updating a single file within a configset.
Expand Down Expand Up @@ -67,11 +68,13 @@ public void updateConfigSetFile(SolrQueryRequest req, SolrQueryResponse rsp) thr
if (fixedSingleFilePath.charAt(0) == '/') {
fixedSingleFilePath = fixedSingleFilePath.substring(1);
}
byte[] data = inputStream.readAllBytes();
if (fixedSingleFilePath.isEmpty()) {
throw new SolrException(
SolrException.ErrorCode.BAD_REQUEST,
"The file path provided for upload, '" + singleFilePath + "', is not valid.");
} else if (ZkMaintenanceUtils.isFileForbiddenInConfigSets(fixedSingleFilePath)) {
} else if (ZkMaintenanceUtils.isFileForbiddenInConfigSets(fixedSingleFilePath)
|| FileTypeMagicUtil.isFileForbiddenInConfigset(data)) {
throw new SolrException(
SolrException.ErrorCode.BAD_REQUEST,
"The file type provided for upload, '"
Expand All @@ -87,8 +90,7 @@ public void updateConfigSetFile(SolrQueryRequest req, SolrQueryResponse rsp) thr
// For creating the baseNode, the cleanup parameter is only allowed to be true when
// singleFilePath is not passed.
createBaseNode(configSetService, overwritesExisting, requestIsTrusted, configSetName);
configSetService.uploadFileToConfig(
configSetName, fixedSingleFilePath, inputStream.readAllBytes(), allowOverwrite);
configSetService.uploadFileToConfig(configSetName, fixedSingleFilePath, data, allowOverwrite);
}
}
}
166 changes: 166 additions & 0 deletions solr/core/src/java/org/apache/solr/util/FileTypeMagicUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
/*
* 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.solr.util;

import com.j256.simplemagic.ContentInfo;
import com.j256.simplemagic.ContentInfoUtil;
import com.j256.simplemagic.ContentType;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
import org.apache.solr.common.SolrException;

/** Utility class to guess the mime type of file based on its magic number. */
public class FileTypeMagicUtil implements ContentInfoUtil.ErrorCallBack {
private final ContentInfoUtil util;
private static final Set<String> SKIP_FOLDERS = new HashSet<>(Arrays.asList(".", ".."));

public static FileTypeMagicUtil INSTANCE = new FileTypeMagicUtil();

FileTypeMagicUtil() {
try {
util = new ContentInfoUtil("/magic/executables", this);
} catch (IOException e) {
throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Error parsing magic file", e);
}
}

/**
* Asserts that an entire configset folder is legal to upload.
*
* @param confPath the path to the folder
* @throws SolrException if an illegal file is found in the folder structure
*/
public static void assertConfigSetFolderLegal(Path confPath) throws IOException {
Files.walkFileTree(
confPath,
new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
throws IOException {
// Read first 100 bytes of the file to determine the mime type
try (InputStream fileStream = Files.newInputStream(file)) {
byte[] bytes = new byte[100];
fileStream.read(bytes);
if (FileTypeMagicUtil.isFileForbiddenInConfigset(bytes)) {
throw new SolrException(
SolrException.ErrorCode.BAD_REQUEST,
String.format(
Locale.ROOT,
"Not uploading file %s to configset, as it matched the MAGIC signature of a forbidden mime type %s",
file,
FileTypeMagicUtil.INSTANCE.guessMimeType(bytes)));
}
return FileVisitResult.CONTINUE;
}
}

@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
throws IOException {
if (SKIP_FOLDERS.contains(dir.getFileName().toString()))
return FileVisitResult.SKIP_SUBTREE;

return FileVisitResult.CONTINUE;
}
});
}

/**
* Guess the mime type of file based on its magic number.
*
* @param stream input stream of the file
* @return string with content-type or "application/octet-stream" if unknown
*/
public String guessMimeType(InputStream stream) {
try {
ContentInfo info = util.findMatch(stream);
if (info == null) {
return ContentType.OTHER.getMimeType();
}
return info.getContentType().getMimeType();
} catch (IOException e) {
throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, e);
}
}

/**
* Guess the mime type of file bytes based on its magic number.
*
* @param bytes the first bytes at start of the file
* @return string with content-type or "application/octet-stream" if unknown
*/
public String guessMimeType(byte[] bytes) {
return guessMimeType(new ByteArrayInputStream(bytes));
}

@Override
public void error(String line, String details, Exception e) {
throw new SolrException(
SolrException.ErrorCode.SERVER_ERROR,
String.format(Locale.ROOT, "%s: %s", line, details),
e);
}

/**
* Determine forbidden file type based on magic bytes matching of the file itself. Forbidden types
* are:
*
* <ul>
* <li><code>application/x-java-applet</code>: java class file
* <li><code>application/zip</code>: jar or zip archives
* <li><code>application/x-tar</code>: tar archives
* <li><code>text/x-shellscript</code>: shell or bash script
* </ul>
*
* @param fileStream stream from the file content
* @return true if file is among the forbidden mime-types
*/
public static boolean isFileForbiddenInConfigset(InputStream fileStream) {
return forbiddenTypes.contains(FileTypeMagicUtil.INSTANCE.guessMimeType(fileStream));
}

/**
* Determine forbidden file type based on magic bytes matching of the first bytes of the file.
*
* @param bytes byte array of the file content
* @return true if file is among the forbidden mime-types
*/
public static boolean isFileForbiddenInConfigset(byte[] bytes) {
if (bytes == null || bytes.length == 0)
return false; // A ZK znode may be a folder with no content
return isFileForbiddenInConfigset(new ByteArrayInputStream(bytes));
}

private static final Set<String> forbiddenTypes =
new HashSet<>(
Arrays.asList(
System.getProperty(
"solr.configset.upload.mimetypes.forbidden",
"application/x-java-applet,application/zip,application/x-tar,text/x-shellscript")
.split(",")));
}
Loading

0 comments on commit 644dd3a

Please sign in to comment.