Goobi viewer - Core: Unauthenticated Solr Streaming Expression Proxy
Description
Summary
The Goobi viewer REST endpoint POST /api/v1/index/stream accepted an arbitrary Solr streaming expression from unauthenticated network clients and forwarded it to the backend Solr server without restriction. An attacker could read the complete Solr index and, in default Solr deployments, also modify or delete indexed records.
The API endpoint has now been removed.
Impact
- Complete Solr index read without authentication. All documents indexed by the viewer including those protected by access conditions such as moving walls, licence requirements or IP restrictions - can be read in full.
- Index data modification. update() streaming expressions overwrite indexed field values. An attacker can alter metadata, change ACCESSCONDITION values, or corrupt document structure.
- Index data deletion. delete() streaming expressions permanently remove documents. A single expression can delete the entire collection, requiring a full re-index to recover.
Patches
The endpoint was removed in 326980f24c
Workarounds
Until an update can be deployed, the endpoint should be blocked by a reverse proxy or in the tomcat configuration.
For Apache httpd the following block can be used in the vhost configuration:
<LocationMatch ^.*api/v[12]/index/stream.*$>
Require all denied
</LocationMatch>
Alternatively the following security constraint can be added in tomcat via the relevant web.xml: `` <security-constraint> <web-resource-collection> <web-resource-name>blocked endpoint</web-resource-name> <url-pattern>/api/v1/index/stream</url-pattern> <url-pattern>/api/v1/index/stream/*</url-pattern> </web-resource-collection> <auth-constraint/> </security-constraint> ``
References
- Fix commit: 326980f24c
- Introducing commit: 6bfb1cbd42
- Solr Streaming Expressions reference
Contact
If you have any questions or comments about this advisory:
- Email us at support@intranda.com
Patches
1326980f24ce1refactor: remove unused /api/v1/index/stream endpoint
4 files changed · +0 −75
goobi-viewer-core/pom.xml+0 −5 modified@@ -766,11 +766,6 @@ </exclusion> </exclusions> </dependency> - <dependency> - <groupId>org.apache.solr</groupId> - <artifactId>solr-solrj-streaming</artifactId> - <version>${solr.version}</version> - </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId>
goobi-viewer-core/src/main/java/io/goobi/viewer/api/rest/v1/ApiUrls.java+0 −1 modified@@ -47,7 +47,6 @@ public class ApiUrls extends AbstractApiUrlManager { public static final String INDEX_FIELDS = "/fields"; public static final String INDEX_QUERY = "/query"; public static final String INDEX_SCHEMA_VERSION = "/schemaversion"; - public static final String INDEX_STREAM = "/stream"; public static final String INDEX_STATISTICS = "/statistics"; public static final String INDEX_SPATIAL_HEATMAP = "/spatial/heatmap/{solrField}"; public static final String INDEX_SPATIAL_SEARCH = "/spatial/search/{solrField}";
goobi-viewer-core/src/main/java/io/goobi/viewer/api/rest/v1/index/IndexResource.java+0 −68 modified@@ -27,7 +27,6 @@ import static io.goobi.viewer.api.rest.v1.ApiUrls.INDEX_SPATIAL_HEATMAP; import static io.goobi.viewer.api.rest.v1.ApiUrls.INDEX_SPATIAL_SEARCH; import static io.goobi.viewer.api.rest.v1.ApiUrls.INDEX_STATISTICS; -import static io.goobi.viewer.api.rest.v1.ApiUrls.INDEX_STREAM; import java.io.IOException; import java.util.ArrayList; @@ -47,21 +46,14 @@ import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.apache.solr.client.solrj.io.Tuple; -import org.apache.solr.client.solrj.io.stream.SolrStream; -import org.apache.solr.client.solrj.io.stream.StreamContext; -import org.apache.solr.client.solrj.io.stream.TupleStream; import org.apache.solr.client.solrj.response.FacetField; import org.apache.solr.client.solrj.response.QueryResponse; import org.apache.solr.common.SolrDocumentList; import org.apache.solr.common.SolrException; -import org.apache.solr.common.params.ModifiableSolrParams; import org.json.JSONArray; import org.json.JSONObject; import org.omnifaces.el.functions.Arrays; -import com.fasterxml.jackson.databind.ObjectMapper; - import de.unigoettingen.sub.commons.contentlib.exceptions.ContentNotFoundException; import de.unigoettingen.sub.commons.contentlib.exceptions.IllegalRequestException; import de.unigoettingen.sub.commons.contentlib.servlet.rest.CORSBinding; @@ -106,10 +98,8 @@ import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; import jakarta.ws.rs.QueryParam; -import jakarta.ws.rs.WebApplicationException; import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.StreamingOutput; /** * REST resource providing search, field information, and statistical queries against the Solr index. @@ -291,32 +281,6 @@ public String getRecordsForQuery(RecordsRequestParameters params) } } - /** - * - * @param expression raw Solr streaming expression to execute - * @return {@link StreamingOutput} - */ - @POST - @Path(INDEX_STREAM) - @Consumes({ MediaType.TEXT_PLAIN }) - @Produces({ MediaType.APPLICATION_JSON }) - @Operation( - tags = { "index" }, - summary = "Post a streaming expression to the Solr index and forward its response") - @ApiResponse(responseCode = "200", description = "Newline-delimited JSON tuples streamed from Solr") - @ApiResponse(responseCode = "400", description = "Illegal query or query parameters") - @ApiResponse(responseCode = "500", description = "Solr not available or unable to respond") - @RequestBody(required = true, content = @Content(mediaType = "text/plain")) - public StreamingOutput stream( - @Schema(description = "Raw Solr streaming expression", - example = "search(current,q=\"+ISANCHOR:*\", sort=\"YEAR asc\", fl=\"YEAR,PI,DOCTYPE\"" - + ", rows=5, qt=\"/select\")") String expression) { - String solrUrl = DataManager.getInstance().getSearchIndex().getSolrServerUrl(); - logger.trace("Call solr {}", solrUrl); - logger.trace("Streaming expression {}", expression); - return executeStreamingExpression(expression, solrUrl); - } - /** * * @return List<SolrFieldInfo> @@ -685,36 +649,4 @@ static List<SolrFieldInfo> collectFieldInfo() throws IndexUnreachableException { return ret; } - /** - * - * @param expr raw Solr streaming expression to execute - * @param solrUrl base URL of the Solr server - * @return {@link StreamingOutput} - */ - private static StreamingOutput executeStreamingExpression(String expr, String solrUrl) { - return out -> { - ObjectMapper mapper = new ObjectMapper(); - ModifiableSolrParams paramsLoc = new ModifiableSolrParams(); - paramsLoc.set("expr", expr); - paramsLoc.set("qt", "/stream"); - // Note, the "/collection" below can be an alias. - try (TupleStream solrStream = new SolrStream(solrUrl, paramsLoc)) { - StreamContext context = new StreamContext(); - solrStream.setStreamContext(context); - solrStream.open(); - Tuple tuple; - do { - tuple = solrStream.read(); - String json = mapper.writeValueAsString(tuple); - out.write((json + "\n").getBytes()); - out.flush(); - } while (!tuple.EOF); - } catch (IOException e) { - if (e.getMessage() != null && e.getMessage().contains("not a proper expression clause")) { - throw new WebApplicationException(new IllegalRequestException(e.getMessage())); - } - throw new WebApplicationException(new IndexUnreachableException(e.toString())); - } - }; - } }
goobi-viewer-core/src/main/java/io/goobi/viewer/api/rest/v2/ApiUrls.java+0 −1 modified@@ -51,7 +51,6 @@ public class ApiUrls extends AbstractApiUrlManager { public static final String INDEX = "/index"; public static final String INDEX_FIELDS = "/fields"; public static final String INDEX_QUERY = "/query"; - public static final String INDEX_STREAM = "/stream"; public static final String INDEX_STATISTICS = "/statistics"; public static final String RECORDS_RSS = "/records/rss";
Vulnerability mechanics
AI mechanics synthesis has not run for this CVE yet.
References
4News mentions
0No linked articles in our index yet.