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

afs pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/jena.git

commit 00291184cbc0d6e1c53d11e090e18994a1022a8b
Author: Andy Seaborne <[email protected]>
AuthorDate: Sun Aug 9 16:12:42 2026 +0100

    GH-4150: Log error on attempted imports in harden SHACL_Validate
---
 .../apache/jena/fuseki/access/DataAccessCtl.java   |  4 ++-
 .../org/apache/jena/fuseki/servlets/ActionLib.java |  7 ++---
 .../jena/fuseki/servlets/SHACL_Validation.java     | 30 +++++++++++++++++++---
 .../apache/jena/fuseki/servlets/ServletOps.java    |  5 ++--
 .../fuseki/main/TestFusekiShaclValidation.java     | 17 +++++++++++-
 .../testing/ShaclValidation/shapes-imports.ttl     | 21 +++++++++++++++
 6 files changed, 73 insertions(+), 11 deletions(-)

diff --git 
a/jena-fuseki2/jena-fuseki-access/src/main/java/org/apache/jena/fuseki/access/DataAccessCtl.java
 
b/jena-fuseki2/jena-fuseki-access/src/main/java/org/apache/jena/fuseki/access/DataAccessCtl.java
index 0672b3cbc7..0fca12cb3a 100644
--- 
a/jena-fuseki2/jena-fuseki-access/src/main/java/org/apache/jena/fuseki/access/DataAccessCtl.java
+++ 
b/jena-fuseki2/jena-fuseki-access/src/main/java/org/apache/jena/fuseki/access/DataAccessCtl.java
@@ -103,11 +103,13 @@ public class DataAccessCtl {
      * {@link DatasetGraphAccessControl} or because it has the context 
settings.
      */
     public static boolean isAccessControlled(DatasetGraph dsg) {
+        if ( dsg == null )
+            return false;
         if ( dsg instanceof DatasetGraphAccessControl )
             return true;
 //        if ( dsg.getContext().isDefined(DataAccessCtl.symControlledAccess) )
 //            return true;
-        if ( dsg.getContext().isDefined(DataAccessCtl.symAuthorizationService) 
)
+        if ( dsg.getContext() != null && 
dsg.getContext().isDefined(DataAccessCtl.symAuthorizationService) )
             return true;
         return false;
     }
diff --git 
a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/ActionLib.java
 
b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/ActionLib.java
index d886d2fad3..e8ab085b68 100644
--- 
a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/ActionLib.java
+++ 
b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/ActionLib.java
@@ -203,13 +203,14 @@ public class ActionLib {
     /**
      * Parse RDF content from the body of the request of the action, ends the
      * request, and sends a 400 if there is a parse error.
+     * Parse errors are logged as "fatal" and become 400/{@link 
ActionErrorException}
      *
      * @throws ActionErrorException ActionErrorException
      */
     public static void parseOrError(HttpAction action, StreamRDF dest, Lang 
lang, String base) {
         try {
             parse(action, dest, lang, base);
-        } catch (RiotParseException ex) {
+        } catch (RiotException ex) {
             ActionLib.consumeBody(action);
             ServletOps.errorParseError(ex);
         }
@@ -218,7 +219,7 @@ public class ActionLib {
     /**
      * Parse RDF content. This wraps up the parse step reading from an action.
      * It includes handling compression if the {@code Content-Encoding} header 
is present
-     * @throws RiotParseException RiotParseException
+     * @throws RiotException
      */
     public static void parse(HttpAction action, StreamRDF dest, Lang lang, 
String base) {
         try {
@@ -229,7 +230,7 @@ public class ActionLib {
 
     /**
      * Parse RDF content. This wraps up the parse step reading from an input 
stream.
-     * @throws RiotParseException RiotParseException
+     * @throws RiotException RiotException
      */
     public static void parse(HttpAction action, StreamRDF dest, InputStream 
input, Lang lang, String base) {
         try {
diff --git 
a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/SHACL_Validation.java
 
b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/SHACL_Validation.java
index e901ef5459..536e77ddf4 100644
--- 
a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/SHACL_Validation.java
+++ 
b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/SHACL_Validation.java
@@ -31,10 +31,13 @@ import org.apache.jena.graph.Node;
 import org.apache.jena.graph.NodeFactory;
 import org.apache.jena.riot.Lang;
 import org.apache.jena.riot.RDFLanguages;
+import org.apache.jena.riot.RiotException;
 import org.apache.jena.riot.web.HttpNames;
 import org.apache.jena.shacl.ShaclValidator;
 import org.apache.jena.shacl.Shapes;
 import org.apache.jena.shacl.ValidationReport;
+import org.apache.jena.system.G;
+import org.apache.jena.vocabulary.OWL1;
 import org.apache.jena.web.HttpSC;
 
 /**
@@ -48,6 +51,8 @@ import org.apache.jena.web.HttpSC;
  */
 public class SHACL_Validation extends BaseActionREST { //ActionREST {
 
+    private static final Node owlImports = OWL1.imports.asNode();
+
     public SHACL_Validation() {}
 
     @Override
@@ -60,20 +65,39 @@ public class SHACL_Validation extends BaseActionREST { 
//ActionREST {
 
         String targetNodeStr = 
action.getRequestParameter(HttpNames.paramTarget);
 
+        Graph shapesGraph;
+        try {
+            shapesGraph = ActionLib.readFromRequest(action, Lang.TTL);
+            if ( G.contains(shapesGraph, null, owlImports, null) ) {
+                action.log.error(format("[%d] shacl: owl:imports not supported 
for remote validation", action.id));
+                // Does not return.
+                ServletOps.errorBadRequest("owl:imports not allowed");
+            }
+        } catch (RiotException ex) {
+            shapesGraph = null;
+            // Does not return.
+            ServletOps.errorBadRequest(ex.getMessage());
+        }
+
         action.beginRead();
         try {
             GraphTarget graphTarget = determineTarget(action.getActiveDSG(), 
action);
-            if ( ! graphTarget.exists() )
+            if ( ! graphTarget.exists() ) {
+                action.log.error(format("[%d] shacl: No data graph", 
action.id));
+                // Does not return.
                 ServletOps.errorNotFound("No data graph: 
"+graphTarget.label());
-            Graph data = graphTarget.graph();
-            Graph shapesGraph = ActionLib.readFromRequest(action, Lang.TTL);
+            }
 
+            Graph data = graphTarget.graph();
             Node targetNode = null;
             if ( targetNodeStr != null ) {
                 String x = data.getPrefixMapping().expandPrefix(targetNodeStr);
                 targetNode = NodeFactory.createURI(x);
             }
 
+            // This does not resolve owl:imports.
+            // Doing so would lead to SSRF (server-side request forgery)
+            // with the server making a URL access on the users behalf.
             Shapes shapes = Shapes.parse(shapesGraph);
             ValidationReport report = ( targetNode == null )
                 ? ShaclValidator.get().validate(shapesGraph, data)
diff --git 
a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/ServletOps.java
 
b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/ServletOps.java
index 4fbeaa43a8..8e630af428 100644
--- 
a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/ServletOps.java
+++ 
b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/ServletOps.java
@@ -28,7 +28,6 @@ import java.io.PrintWriter;
 
 import jakarta.servlet.ServletOutputStream;
 import jakarta.servlet.http.HttpServletResponse;
-
 import org.apache.jena.atlas.RuntimeIOException;
 import org.apache.jena.atlas.io.IO;
 import org.apache.jena.atlas.json.JSON;
@@ -40,7 +39,7 @@ import org.apache.jena.atlas.web.MediaType;
 import org.apache.jena.fuseki.system.ConNeg;
 import org.apache.jena.fuseki.system.UploadDetails;
 import org.apache.jena.fuseki.system.UploadDetails.PreState;
-import org.apache.jena.riot.RiotParseException;
+import org.apache.jena.riot.RiotException;
 import org.apache.jena.riot.WebContent;
 import org.apache.jena.riot.web.HttpNames;
 import org.apache.jena.web.HttpSC;
@@ -211,7 +210,7 @@ public class ServletOps {
         action.log.warn(string, thorwable);
     }
 
-    public static void errorParseError(RiotParseException ex) {
+    public static void errorParseError(RiotException ex) {
         error(HttpSC.BAD_REQUEST_400, "Parse Error: "+ex.getMessage());
     }
 
diff --git 
a/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TestFusekiShaclValidation.java
 
b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TestFusekiShaclValidation.java
index 33472e66ef..dfc01adf9e 100644
--- 
a/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TestFusekiShaclValidation.java
+++ 
b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TestFusekiShaclValidation.java
@@ -24,8 +24,12 @@ package org.apache.jena.fuseki.main;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 
-import org.junit.jupiter.api.*;
+import org.junit.jupiter.api.MethodOrderer;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestMethodOrder;
 
+import org.apache.jena.atlas.logging.LogCtl;
+import org.apache.jena.fuseki.Fuseki;
 import org.apache.jena.graph.Graph;
 import org.apache.jena.http.HttpRDF;
 import org.apache.jena.rdfconnection.RDFConnection;
@@ -184,6 +188,17 @@ public class TestFusekiShaclValidation {
         });
     }
 
+    @Test
+    public void shacl_imports() {
+        LogCtl.withLevel(Fuseki.actionLog, "FATAL", ()->{
+            withServer((datasetURL)->{
+                FusekiTestLib.expect400(()->{
+                    validateReport(datasetURL+"/shacl?default", 
DIR+"shapes-imports.ttl");
+                });
+            });
+        });
+    }
+
     private static ValidationReport validateReport(String url, String 
shapesFile) {
         Graph shapesGraph = RDFDataMgr.loadGraph(shapesFile);
         Graph responseGraph = HttpRDF.httpPostGraphRtn(url, shapesGraph);
diff --git 
a/jena-fuseki2/jena-fuseki-main/testing/ShaclValidation/shapes-imports.ttl 
b/jena-fuseki2/jena-fuseki-main/testing/ShaclValidation/shapes-imports.ttl
new file mode 100644
index 0000000000..067cc98fe2
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/testing/ShaclValidation/shapes-imports.ttl
@@ -0,0 +1,21 @@
+PREFIX rdf:     <http://www.w3.org/1999/02/22-rdf-syntax-ns#> 
+PREFIX rdfs:    <http://www.w3.org/2000/01/rdf-schema#>
+PREFIX owl:     <http://www.w3.org/2002/07/owl#>
+PREFIX xsd:     <http://www.w3.org/2001/XMLSchema#>
+PREFIX sh:         <http://www.w3.org/ns/shacl#>
+
+PREFIX :        <urn:sh:ex:>
+PREFIX ex:      <http://example/>
+PREFIX ns:      <http://example/ns#>
+
+[] rdf:type owl:Ontology;
+   owl:imports <.> .
+
+:nodeShape2 a sh:NodeShape ;
+    sh:targetSubjectsOf ns:p ;
+    sh:property [
+       sh:path ns:p;
+       sh:datatype xsd:string;
+       sh:maxCount 1 ;
+       sh:minCount 1 ;
+    ] .

Reply via email to