You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by ja...@apache.org on 2021/10/11 08:51:50 UTC

[lucene-solr] branch branch_8x updated: SOLR-15678 Allow only known content types in ShowFileRequestHandler (#2589)

This is an automated email from the ASF dual-hosted git repository.

janhoy pushed a commit to branch branch_8x
in repository https://gitbox.apache.org/repos/asf/lucene-solr.git


The following commit(s) were added to refs/heads/branch_8x by this push:
     new 5bff5d6  SOLR-15678 Allow only known content types in ShowFileRequestHandler (#2589)
5bff5d6 is described below

commit 5bff5d6a8957d609f80651920677aced83f346d6
Author: Jan Høydahl <ja...@users.noreply.github.com>
AuthorDate: Mon Oct 11 10:51:30 2021 +0200

    SOLR-15678 Allow only known content types in ShowFileRequestHandler (#2589)
---
 solr/CHANGES.txt                                   |  2 +-
 .../solr/handler/admin/ShowFileRequestHandler.java | 48 ++++++++++----
 .../test-files/solr/collection1/conf/example.html  |  6 ++
 .../handler/admin/ShowFileRequestHandlerTest.java  | 74 ++++++++++++++++++++--
 .../src/rule-based-authorization-plugin.adoc       |  2 +-
 5 files changed, 112 insertions(+), 20 deletions(-)

diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index dce18a1..52b6ef4 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -32,7 +32,7 @@ Build
 
 Other Changes
 ---------------------
-(No changes)
+* SOLR-15678: Allow only known content types in ShowFileRequestHandler (janhoy, Gus Heck, Mal Aware)
 
 ======================= 8.10.1 =======================
 
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/ShowFileRequestHandler.java b/solr/core/src/java/org/apache/solr/handler/admin/ShowFileRequestHandler.java
index a000290..c3f40e8 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/ShowFileRequestHandler.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/ShowFileRequestHandler.java
@@ -16,6 +16,7 @@
  */
 package org.apache.solr.handler.admin;
 
+import com.google.common.base.Strings;
 import org.apache.solr.cloud.ZkSolrResourceLoader;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.SolrException.ErrorCode;
@@ -33,7 +34,10 @@ import org.apache.solr.handler.RequestHandlerBase;
 import org.apache.solr.request.SolrQueryRequest;
 import org.apache.solr.response.RawResponseWriter;
 import org.apache.solr.response.SolrQueryResponse;
+import org.apache.solr.security.AuthorizationContext;
+import org.apache.solr.security.PermissionNameProvider;
 import org.apache.zookeeper.KeeperException;
+import org.eclipse.jetty.http.MimeTypes;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -42,6 +46,7 @@ import java.io.IOException;
 import java.io.UnsupportedEncodingException;
 import java.lang.invoke.MethodHandles;
 import java.net.URISyntaxException;
+import java.nio.file.Path;
 import java.util.Date;
 import java.util.HashSet;
 import java.util.List;
@@ -87,7 +92,7 @@ import java.util.Set;
  *
  * @since solr 1.3
  */
-public class ShowFileRequestHandler extends RequestHandlerBase
+public class ShowFileRequestHandler extends RequestHandlerBase implements PermissionNameProvider
 {
   public static final String HIDDEN = "hidden";
   public static final String USE_CONTENT_TYPE = "contentType";
@@ -178,7 +183,7 @@ public class ShowFileRequestHandler extends RequestHandlerBase
       params.set(CommonParams.WT, "raw");
       req.setParams(params);
       ContentStreamBase content = new ContentStreamBase.ByteArrayStream(zkClient.getData(adminFile, null, null, true), adminFile);
-      content.setContentType(req.getParams().get(USE_CONTENT_TYPE));
+      content.setContentType(getSafeContentType(req.getParams().get(USE_CONTENT_TYPE)));
       
       rsp.add(RawResponseWriter.CONTENT, content);
     }
@@ -243,13 +248,33 @@ public class ShowFileRequestHandler extends RequestHandlerBase
       req.setParams(params);
 
       ContentStreamBase content = new ContentStreamBase.FileStream( adminFile );
-      content.setContentType(req.getParams().get(USE_CONTENT_TYPE));
+      content.setContentType(getSafeContentType(req.getParams().get(USE_CONTENT_TYPE)));
 
       rsp.add(RawResponseWriter.CONTENT, content);
     }
     rsp.setHttpCaching(false);
   }
 
+  /**
+   * Checks content type string and returns it if it is one of allowed types.
+   * The allowed types are all standard mime types.
+   * If an HTML type is requested, it is instead returned as text/plain
+   */
+  public static String getSafeContentType(String contentType) {
+    if (Strings.isNullOrEmpty(contentType)) {
+      log.debug("No contentType specified");
+      return null;
+    }
+    if (!MimeTypes.getKnownMimeTypes().contains(contentType)) {
+      throw new SolrException(ErrorCode.BAD_REQUEST, "Requested content type '" + contentType + "' is not supported.");
+    }
+    if (contentType.toLowerCase(Locale.ROOT).contains("html")) {
+      log.info("Using text/plain instead of {}", contentType);
+      return "text/plain";
+    }
+    return contentType;
+  }
+
   //////////////////////// Static methods //////////////////////////////
 
   public static boolean isHiddenFile(SolrQueryRequest req, SolrQueryResponse rsp, String fnameIn, boolean reportError,
@@ -336,20 +361,16 @@ public class ShowFileRequestHandler extends RequestHandlerBase
     String fname = req.getParams().get("file", null);
     if( fname == null ) {
       adminFile = configdir;
-    }
-    else {
+    } else {
       fname = fname.replace( '\\', '/' ); // normalize slashes
       if( hiddenFiles.contains( fname.toUpperCase(Locale.ROOT) ) ) {
         log.error("Can not access: {}", fname);
         rsp.setException(new SolrException( SolrException.ErrorCode.FORBIDDEN, "Can not access: "+fname ));
         return null;
       }
-      if( fname.indexOf( ".." ) >= 0 ) {
-        log.error("Invalid path: {}", fname);
-        rsp.setException(new SolrException( SolrException.ErrorCode.FORBIDDEN, "Invalid path: "+fname ));
-        return null;
-      }
-      adminFile = new File( configdir, fname );
+      Path filePath = configdir.toPath().resolve(fname);
+      req.getCore().getCoreContainer().assertPathAllowed(filePath);
+      adminFile = filePath.toFile();
     }
     return adminFile;
   }
@@ -368,4 +389,9 @@ public class ShowFileRequestHandler extends RequestHandlerBase
   public Category getCategory() {
     return Category.ADMIN;
   }
+
+  @Override
+  public Name getPermissionName(AuthorizationContext request) {
+    return Name.CONFIG_READ_PERM;
+  }
 }
diff --git a/solr/core/src/test-files/solr/collection1/conf/example.html b/solr/core/src/test-files/solr/collection1/conf/example.html
new file mode 100644
index 0000000..bb8f277
--- /dev/null
+++ b/solr/core/src/test-files/solr/collection1/conf/example.html
@@ -0,0 +1,6 @@
+<html>
+<head>
+  <title>Welcome to Solr</title>
+</head>
+<body>Hello</body>
+</html>
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/ShowFileRequestHandlerTest.java b/solr/core/src/test/org/apache/solr/handler/admin/ShowFileRequestHandlerTest.java
index ee8695b..0e78329 100644
--- a/solr/core/src/test/org/apache/solr/handler/admin/ShowFileRequestHandlerTest.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/ShowFileRequestHandlerTest.java
@@ -16,23 +16,28 @@
  */
 package org.apache.solr.handler.admin;
 
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.Reader;
-import java.util.concurrent.atomic.AtomicBoolean;
-
 import org.apache.solr.SolrJettyTestBase;
 import org.apache.solr.client.solrj.ResponseParser;
 import org.apache.solr.client.solrj.SolrClient;
 import org.apache.solr.client.solrj.SolrServerException;
+import org.apache.solr.client.solrj.impl.NoOpResponseParser;
 import org.apache.solr.client.solrj.request.QueryRequest;
 import org.apache.solr.client.solrj.response.QueryResponse;
 import org.apache.solr.common.SolrException;
+import org.apache.solr.common.util.ContentStreamBase;
 import org.apache.solr.common.util.NamedList;
 import org.apache.solr.core.SolrCore;
+import org.apache.solr.request.LocalSolrQueryRequest;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.request.SolrRequestHandler;
 import org.apache.solr.response.SolrQueryResponse;
 import org.junit.BeforeClass;
 
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+import java.util.concurrent.atomic.AtomicBoolean;
+
 /**
  * Extend SolrJettyTestBase because the SOLR-2535 bug only manifested itself when
  * the {@link org.apache.solr.servlet.SolrDispatchFilter} is used, which isn't for embedded Solr use.
@@ -41,6 +46,7 @@ public class ShowFileRequestHandlerTest extends SolrJettyTestBase {
 
   @BeforeClass
   public static void beforeTest() throws Exception {
+    initCore("solrconfig.xml", "schema.xml");
     createAndStartJetty(legacyExampleCollection1SolrHome());
   }
 
@@ -54,10 +60,8 @@ public class ShowFileRequestHandlerTest extends SolrJettyTestBase {
   }
 
   public void test404Locally() throws Exception {
-
     // we need to test that executing the handler directly does not 
     // throw an exception, just sets the exception on the response.
-    initCore("solrconfig.xml", "schema.xml");
 
     // bypass TestHarness since it will throw any exception found in the
     // response.
@@ -114,4 +118,60 @@ public class ShowFileRequestHandlerTest extends SolrJettyTestBase {
     //request.process(client); but we don't have a NamedList response
     assertTrue(readFile.get());
   }
+
+  public void testContentTypeHtmlBecomesTextPlain() throws Exception {
+    SolrRequestHandler handler = h.getCore().getRequestHandler("/admin/file");
+    SolrQueryRequest req = new LocalSolrQueryRequest(h.getCore(), params("file", "schema.xml", "contentType", "text/html"));
+    SolrQueryResponse rsp = new SolrQueryResponse();
+    handler.handleRequest(req, rsp);
+    ContentStreamBase.FileStream content = (ContentStreamBase.FileStream) rsp.getValues().get("content");
+    assertEquals("text/plain", content.getContentType());
+  }
+
+  public void testContentTypeHtmlDefault() throws Exception {
+    SolrRequestHandler handler = h.getCore().getRequestHandler("/admin/file");
+    SolrQueryRequest req = new LocalSolrQueryRequest(h.getCore(), params("file", "example.html"));
+    SolrQueryResponse rsp = new SolrQueryResponse();
+    handler.handleRequest(req, rsp);
+    ContentStreamBase.FileStream content = (ContentStreamBase.FileStream) rsp.getValues().get("content");
+    // System attempts to guess content type, but will only return XML, JSON, CSV, never HTML
+    assertEquals("application/xml", content.getContentType());
+  }
+
+  public void testIllegalContentType() {
+    SolrClient client = getSolrClient();
+    QueryRequest request = new QueryRequest(params("file", "managed-schema", "contentType", "not/known"));
+    request.setPath("/admin/file");
+    request.setResponseParser(new NoOpResponseParser());
+    expectThrows(SolrException.class, () -> client.request(request));
+  }
+
+  public void testAbsoluteFilename() {
+    SolrClient client = getSolrClient();
+    final QueryRequest request = new QueryRequest(params("file", "/etc/passwd"));
+    request.setPath("/admin/file"); // absolute path not allowed
+    request.setResponseParser(new NoOpResponseParser());
+    expectThrows(SolrException.class, () -> client.request(request));
+  }
+
+  public void testPathTraversalFilename() {
+    SolrClient client = getSolrClient();
+    final QueryRequest request = new QueryRequest(params("file", "../../../../../../etc/passwd"));
+    request.setPath("/admin/file");
+    request.setResponseParser(new NoOpResponseParser());
+    expectThrows(SolrException.class, () -> client.request(request));
+  }
+
+  public void testGetSafeContentType() {
+    // Valid content types are returned as is
+    assertEquals("application/json", ShowFileRequestHandler.getSafeContentType("application/json"));
+    assertEquals("text/csv", ShowFileRequestHandler.getSafeContentType("text/csv"));
+
+    // HTML gets rewritten as "safe" plaintext
+    assertEquals("text/plain", ShowFileRequestHandler.getSafeContentType("text/html"));
+    assertEquals("text/plain", ShowFileRequestHandler.getSafeContentType("application/xhtml+xml"));
+
+    // Non-known content types are rejected with 400 error
+    expectThrows(SolrException.class, () -> ShowFileRequestHandler.getSafeContentType("foo/bar"));
+  }
 }
diff --git a/solr/solr-ref-guide/src/rule-based-authorization-plugin.adoc b/solr/solr-ref-guide/src/rule-based-authorization-plugin.adoc
index 4dc9401..fcb108d 100644
--- a/solr/solr-ref-guide/src/rule-based-authorization-plugin.adoc
+++ b/solr/solr-ref-guide/src/rule-based-authorization-plugin.adoc
@@ -207,7 +207,7 @@ The predefined permission names (and their effects) are:
 * *schema-edit*: this permission is allowed to edit a collection's schema using the <<schema-api.adoc#,Schema API>>. Note that this allows schema edit permissions for _all_ collections. If edit permissions should only be applied to specific collections, a custom permission would need to be created.
 * *schema-read*: this permission is allowed to read a collection's schema using the <<schema-api.adoc#,Schema API>>. Note that this allows schema read permissions for _all_ collections. If read permissions should only be applied to specific collections, a custom permission would need to be created.
 * *config-edit*: this permission is allowed to edit a collection's configuration using the <<config-api.adoc#,Config API>>, the <<request-parameters-api.adoc#,Request Parameters API>>, and other APIs which modify `configoverlay.json`. Note that this allows configuration edit permissions for _all_ collections. If edit permissions should only be applied to specific collections, a custom permission would need to be created.
-* *config-read*: this permission is allowed to read a collection's configuration using the <<config-api.adoc#,Config API>>, the <<request-parameters-api.adoc#,Request Parameters API>>, and other APIs which modify `configoverlay.json`. Note that this allows configuration read permissions for _all_ collections. If read permissions should only be applied to specific collections, a custom permission would need to be created.
+* *config-read*: this permission is allowed to read a collection's configuration using the <<config-api.adoc#,Config API>>, the <<request-parameters-api.adoc#,Request Parameters API>>, <<configsets-api.adoc#configsets-list,Configsets API>>, the Admin UI's <<files-screen.adoc#,Files Screen>>, and other APIs accessing configuration. Note that this allows configuration read permissions for _all_ collections. If read permissions should only be applied to specific collections, a custom permis [...]
 * *metrics-read*: this permission allows access to Solr's <<metrics-reporting.adoc#metrics-api,Metrics API>>
 * *metrics-history-read*: this permission allows access to Solr's <<metrics-history.adoc#metrics-history-api,Metrics History API>>, which provides long-term history for a select set of key Solr metrics.
 * *autoscaling-read*: this permission allows users to read Solr's <<solrcloud-autoscaling-api.adoc#read-api,autoscaling>> configuration.  This covers all read-only autoscaling APIs, including: