VYPR
Critical severity9.8NVD Advisory· Published May 13, 2026· Updated May 13, 2026

Goobi viewer - Core: Unauthenticated Solr Streaming Expression Proxy

CVE-2026-45083

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

Contact

If you have any questions or comments about this advisory:

Patches

1
326980f24ce1

refactor: 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

4

News mentions

0

No linked articles in our index yet.