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 386ced217d9c85203402bb2f9f3fd057d0001c68
Author: Andy Seaborne <[email protected]>
AuthorDate: Sat Oct 10 10:08:20 2026 +0100

    GH-4319: More checking of SHACL validation requests
---
 .../jena/fuseki/servlets/SHACL_Validation.java     | 95 +++++++++++++++++++---
 .../fuseki/main/TestFusekiShaclValidation.java     | 35 ++++++--
 .../testing/ShaclValidation/shapes-no-service.ttl  | 21 +++++
 .../testing/ShaclValidation/shapes-service.ttl     | 21 +++++
 .../main/java/org/apache/jena/shacl/lib/ShLib.java |  2 +-
 5 files changed, 154 insertions(+), 20 deletions(-)

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 536e77ddf4..673461a89c 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
@@ -24,18 +24,28 @@ package org.apache.jena.fuseki.servlets;
 import static java.lang.String.format;
 import static org.apache.jena.fuseki.servlets.GraphTarget.determineTarget;
 
+import java.util.List;
+
 import org.apache.jena.atlas.web.MediaType;
 import org.apache.jena.fuseki.DEF;
 import org.apache.jena.graph.Graph;
 import org.apache.jena.graph.Node;
 import org.apache.jena.graph.NodeFactory;
+import org.apache.jena.query.Query;
 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.ShaclException;
 import org.apache.jena.shacl.ShaclValidator;
 import org.apache.jena.shacl.Shapes;
 import org.apache.jena.shacl.ValidationReport;
+import org.apache.jena.shacl.lib.ShLib;
+import org.apache.jena.shacl.vocabulary.SHACL;
+import org.apache.jena.sparql.syntax.ElementService;
+import org.apache.jena.sparql.syntax.ElementVisitor;
+import org.apache.jena.sparql.syntax.ElementVisitorBase;
+import org.apache.jena.sparql.syntax.ElementWalker;
 import org.apache.jena.system.G;
 import org.apache.jena.vocabulary.OWL1;
 import org.apache.jena.web.HttpSC;
@@ -68,17 +78,14 @@ public class SHACL_Validation extends BaseActionREST { 
//ActionREST {
         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());
         }
 
+        checkShapes(action, shapesGraph);
+
         action.beginRead();
         try {
             GraphTarget graphTarget = determineTarget(action.getActiveDSG(), 
action);
@@ -95,13 +102,20 @@ public class SHACL_Validation extends BaseActionREST { 
//ActionREST {
                 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)
-                : ShaclValidator.get().validate(shapesGraph, data, targetNode);
+            ValidationReport report;
+            try {
+                // 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);
+                report = ( targetNode == null )
+                        ? ShaclValidator.get().validate(shapesGraph, data)
+                        : ShaclValidator.get().validate(shapesGraph, data, 
targetNode);
+            } catch (Throwable th) {
+                // General catch-all.
+                ServletOps.errorBadRequest("SHACL Validation request error: 
"+th.getMessage());
+                throw th;
+            }
 
             if ( report.conforms() )
                 action.log.info(format("[%d] shacl: conforms", action.id));
@@ -114,4 +128,61 @@ public class SHACL_Validation extends BaseActionREST { 
//ActionREST {
             action.endRead();
         }
     }
+
+    // Check the graph in the request.
+    private static void checkShapes(HttpAction action, Graph shapesGraph) {
+        try {
+            checkForOwlImports(action, shapesGraph);
+            checkForService(shapesGraph);
+        } catch (ActionErrorException ex) {
+            throw ex;
+        } catch (ShaclException ex) {
+            action.log.error(format("[%d] shacl: bad request: %s", action.id, 
ex.getMessage()));
+            ServletOps.errorBadRequest("Bad SHACL validation request: " + 
ex.getMessage());
+        }
+    }
+
+    private static void checkForOwlImports(HttpAction action, Graph 
shapesGraph) {
+        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");
+        }
+    }
+
+    // The SHACL vocabulary that is used to give executable queries.
+    static Node[] predicates = { SHACL.select, SHACL.ask, SHACL.construct };
+    public static void checkForService(Graph shapesGraph) {
+        for ( Node p : predicates ) {
+            checkForService(shapesGraph, p);
+        }
+    }
+
+    /**
+     * Check a shapes graph for use of SERVICE in SPARQ-based extensions such 
as
+     * {@code sh:target [ sh:select ... ]}.
+     * This checking is regardless of the use of the query
+     * (e.g. it is not only applying to forms that do not allow SERVICE
+     * such as pre-binding rules).
+     */
+    private static void checkForService(Graph shapesGraph, Node predicate) {
+            List<Node> queryNodes = G.listPO(shapesGraph, predicate, null);
+            G.iterSubjectsOfPredicate(shapesGraph, 
predicate).forEachRemaining(qNode->{
+                Query q = ShLib.extractSPARQLQuery(shapesGraph, qNode);
+                checkForService(q);
+            });
+    }
+
+    static void checkForService(Query query) {
+        //private static
+        ElementWalker.walk(query.getQueryPattern(), eltCheckServiceVisitor);
+    }
+
+    private static ElementVisitor eltCheckServiceVisitor = new 
ElementVisitorBase() {
+        @Override
+        public void visit(ElementService el) {
+            throw new ShaclException("SERVICE found in query");
+        }
+    };
+
 }
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 dfc01adf9e..e178b0be1e 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
@@ -177,13 +177,15 @@ public class TestFusekiShaclValidation {
         withServer((datasetURL)->{
             try ( RDFConnection conn = RDFConnection.connect(datasetURL)) {
                 conn.put(DIR+"data1.ttl");
-                try {
-                    FusekiTestLib.expect404(()->{
-                        ValidationReport report = 
validateReport(datasetURL+"/shacl?graph=urn:abc:noGraph", DIR+"shapes1.ttl");
-                    });
-                } finally {
-                    conn.update("CLEAR ALL");
-                }
+                LogCtl.withLevel(Fuseki.actionLog, "FATAL", ()->{
+                    try {
+                        FusekiTestLib.expect404(()->{
+                            ValidationReport report = 
validateReport(datasetURL+"/shacl?graph=urn:abc:noGraph", DIR+"shapes1.ttl");
+                        });
+                    } finally {
+                        conn.update("CLEAR ALL");
+                    }
+                });
             }
         });
     }
@@ -199,6 +201,25 @@ public class TestFusekiShaclValidation {
         });
     }
 
+    @Test
+    public void shacl_SERVICE_not_included() {
+        LogCtl.withLevel(Fuseki.actionLog, "FATAL", ()->{
+            withServer((datasetURL)->{
+                FusekiTestLib.expect400(()->{
+                    validateReport(datasetURL+"/shacl?default", 
DIR+"shapes-service.ttl");
+                });
+            });
+        });
+    }
+
+    @Test
+    public void shacl_no_SERVICE() {
+        // allowed
+        withServer((datasetURL)->{
+                validateReport(datasetURL+"/shacl?default", 
DIR+"shapes-no-service.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-no-service.ttl 
b/jena-fuseki2/jena-fuseki-main/testing/ShaclValidation/shapes-no-service.ttl
new file mode 100644
index 0000000000..d443443414
--- /dev/null
+++ 
b/jena-fuseki2/jena-fuseki-main/testing/ShaclValidation/shapes-no-service.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 :        <http://example/>
+
+:NS
+     sh:declare [
+     sh:prefix "" ;
+     sh:namespace "http://example/"^^xsd:anyURI;
+] .
+
+[ ] rdf:type sh:NodeShape ;
+    sh:target [
+        a sh:SPARQLTarget ;
+        sh:prefixes :NS ;
+        sh:select "SELECT ?this { BIND(:x as ?this) }"
+    ];
+    .
diff --git 
a/jena-fuseki2/jena-fuseki-main/testing/ShaclValidation/shapes-service.ttl 
b/jena-fuseki2/jena-fuseki-main/testing/ShaclValidation/shapes-service.ttl
new file mode 100644
index 0000000000..19bbe645a4
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/testing/ShaclValidation/shapes-service.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 :        <http://example/>
+
+:NS
+     sh:declare [
+     sh:prefix "" ;
+     sh:namespace "http://example/"^^xsd:anyURI;
+] .
+
+[ ] rdf:type sh:NodeShape ;
+    sh:target [
+        a sh:SPARQLTarget ;
+        sh:prefixes :NS ;
+        sh:select "SELECT ?this { SERVICE :remote { } }"
+    ];
+    .
diff --git a/jena-shacl/src/main/java/org/apache/jena/shacl/lib/ShLib.java 
b/jena-shacl/src/main/java/org/apache/jena/shacl/lib/ShLib.java
index 585e6c705a..db6523012a 100644
--- a/jena-shacl/src/main/java/org/apache/jena/shacl/lib/ShLib.java
+++ b/jena-shacl/src/main/java/org/apache/jena/shacl/lib/ShLib.java
@@ -338,7 +338,7 @@ public class ShLib {
             else if ( nNamespace.isURI() )
                 ns = nNamespace.getURI();
             else
-                throw new ShaclParseException("sh:namespace is not  a literal 
or URI");
+                throw new ShaclParseException("sh:namespace is not a literal 
or URI");
             if ( seen.containsKey(prefix) ) {
                 if ( seen.get(prefix).equals(ns) )
                     continue;

Reply via email to