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;
