From e33832f33150d4dde2a7f11de3e2698bed0e71e2 Mon Sep 17 00:00:00 2001 From: Andy Seaborne Date: Mon, 24 Aug 2026 19:15:14 +0100 Subject: [PATCH 1/2] Rename exclusivity lock --- .../dboe/storage/system/DatasetGraphTxnCtl.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/jena-db/jena-dboe-storage/src/main/java/org/apache/jena/dboe/storage/system/DatasetGraphTxnCtl.java b/jena-db/jena-dboe-storage/src/main/java/org/apache/jena/dboe/storage/system/DatasetGraphTxnCtl.java index 85ec0bc0979..b38608f6123 100644 --- a/jena-db/jena-dboe-storage/src/main/java/org/apache/jena/dboe/storage/system/DatasetGraphTxnCtl.java +++ b/jena-db/jena-dboe-storage/src/main/java/org/apache/jena/dboe/storage/system/DatasetGraphTxnCtl.java @@ -58,7 +58,7 @@ public class DatasetGraphTxnCtl extends DatasetGraphWrapper implements Transacti * Use {@link #beginMultiMode}/{@link #endMultiMode} (normal * operation), {@link #beingSingleMode}/{@link #endSingleMode} (exclusive mode) **/ - private ReadWriteLock exclusivitylock = new ReentrantReadWriteLock(); + private ReadWriteLock exclusivityLock = new ReentrantReadWriteLock(); /** * Lock to guarantee only readers are present. Writers and promote transaction @@ -257,19 +257,19 @@ protected void finishPossibleWriter() { @Override public void startNonExclusiveMode() { - boolean b = beginMultiMode(exclusivitylock, true); + boolean b = beginMultiMode(exclusivityLock, true); if ( !b ) throw new TransactionException("Can't start transaction at the moment."); } @Override public boolean tryNonExclusiveMode(boolean canBlock) { - return beginMultiMode(exclusivitylock, canBlock); + return beginMultiMode(exclusivityLock, canBlock); } @Override public void finishNonExclusiveMode() { - endMultiMode(exclusivitylock); + endMultiMode(exclusivityLock); } /** @@ -295,10 +295,10 @@ public void startExclusiveMode() { */ private boolean startExclusiveMode(boolean canBlock) { if ( canBlock ) { - exclusivitylock.writeLock().lock(); + exclusivityLock.writeLock().lock(); return true; } - return exclusivitylock.writeLock().tryLock(); + return exclusivityLock.writeLock().tryLock(); } /** @@ -336,7 +336,7 @@ public boolean tryExclusiveMode(boolean canBlock) { */ @Override public void finishExclusiveMode() { - exclusivitylock.writeLock().unlock(); + exclusivityLock.writeLock().unlock(); } // Lock abstraction. From 82978f2438935d16cb0ee53144adaaa5b7b05eb3 Mon Sep 17 00:00:00 2001 From: Andy Seaborne Date: Sat, 10 Oct 2026 10:08:20 +0100 Subject: [PATCH 2/2] GH-4319: More checking of SHACL validation requests --- .../fuseki/servlets/SHACL_Validation.java | 95 ++++++++++++++++--- .../main/TestFusekiShaclValidation.java | 35 +++++-- .../ShaclValidation/shapes-no-service.ttl | 21 ++++ .../ShaclValidation/shapes-service.ttl | 21 ++++ .../java/org/apache/jena/shacl/lib/ShLib.java | 2 +- 5 files changed, 154 insertions(+), 20 deletions(-) create mode 100644 jena-fuseki2/jena-fuseki-main/testing/ShaclValidation/shapes-no-service.ttl create mode 100644 jena-fuseki2/jena-fuseki-main/testing/ShaclValidation/shapes-service.ttl 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 536e77ddf4e..673461a89c8 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 @@ 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 @@ protected void doPost(HttpAction action) { 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 @@ protected void doPost(HttpAction action) { 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 @@ protected void doPost(HttpAction action) { 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 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 dfc01adf9ec..e178b0be1e6 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 void shacl_no_data_graph() { 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 void shacl_imports() { }); } + @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 00000000000..d443443414e --- /dev/null +++ b/jena-fuseki2/jena-fuseki-main/testing/ShaclValidation/shapes-no-service.ttl @@ -0,0 +1,21 @@ +PREFIX rdf: +PREFIX rdfs: +PREFIX owl: +PREFIX xsd: +PREFIX sh: + +PREFIX : + +: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 00000000000..19bbe645a49 --- /dev/null +++ b/jena-fuseki2/jena-fuseki-main/testing/ShaclValidation/shapes-service.ttl @@ -0,0 +1,21 @@ +PREFIX rdf: +PREFIX rdfs: +PREFIX owl: +PREFIX xsd: +PREFIX sh: + +PREFIX : + +: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 585e6c705ad..db6523012a7 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 static String prefixes(Graph shapesGraph, Node sparqlNode) { 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;