From f5ea5089d23a95ebc50412d133986b782664a53e Mon Sep 17 00:00:00 2001 From: Andy Seaborne Date: Thu, 13 Aug 2026 13:34:32 +0100 Subject: [PATCH 1/3] GH-4150: Remove unused jena-shex dependency in Fuseki --- jena-fuseki2/jena-fuseki-core/pom.xml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/jena-fuseki2/jena-fuseki-core/pom.xml b/jena-fuseki2/jena-fuseki-core/pom.xml index 205ce65a417..82a825ee8a7 100644 --- a/jena-fuseki2/jena-fuseki-core/pom.xml +++ b/jena-fuseki2/jena-fuseki-core/pom.xml @@ -52,12 +52,6 @@ 6.3.0-SNAPSHOT - - org.apache.jena - jena-shex - 6.3.0-SNAPSHOT - - org.apache.jena jena-tdb1 From f65efe9591a39b85bc834dda7bdf8ddda7f36c6a Mon Sep 17 00:00:00 2001 From: Andy Seaborne Date: Sun, 9 Aug 2026 16:12:42 +0100 Subject: [PATCH 2/3] GH-4150: Log error on attempted imports in harden SHACL_Validate --- .../jena/fuseki/access/DataAccessCtl.java | 4 ++- .../jena/fuseki/servlets/ActionLib.java | 7 +++-- .../fuseki/servlets/SHACL_Validation.java | 30 +++++++++++++++++-- .../jena/fuseki/servlets/ServletOps.java | 5 ++-- .../main/TestFusekiShaclValidation.java | 17 ++++++++++- .../ShaclValidation/shapes-imports.ttl | 21 +++++++++++++ 6 files changed, 73 insertions(+), 11 deletions(-) create mode 100644 jena-fuseki2/jena-fuseki-main/testing/ShaclValidation/shapes-imports.ttl 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 0672b3cbc77..0fca12cb3a0 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 static DatasetGraph controlledDataset(DatasetGraph dsgBase, Authorization * {@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 d886d2fad3f..e8ab085b68a 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 static boolean splitContains(String[] elts, String str) { /** * 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 static void parseOrError(HttpAction action, StreamRDF dest, Lang lang, St /** * 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 static void parse(HttpAction action, StreamRDF dest, Lang lang, String ba /** * 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 e901ef54592..536e77ddf4e 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.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 @@ */ public class SHACL_Validation extends BaseActionREST { //ActionREST { + private static final Node owlImports = OWL1.imports.asNode(); + public SHACL_Validation() {} @Override @@ -60,20 +65,39 @@ protected void doPost(HttpAction action) { 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 4fbeaa43a84..8e630af428c 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 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.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 static void warning(HttpAction action, String string, Throwable thorwable 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 33472e66ef7..dfc01adf9ec 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 @@ 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 void shacl_no_data_graph() { }); } + @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 00000000000..067cc98fe2c --- /dev/null +++ b/jena-fuseki2/jena-fuseki-main/testing/ShaclValidation/shapes-imports.ttl @@ -0,0 +1,21 @@ +PREFIX rdf: +PREFIX rdfs: +PREFIX owl: +PREFIX xsd: +PREFIX sh: + +PREFIX : +PREFIX ex: +PREFIX 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 ; + ] . From 7aebe21208ef64908fa3a3346e075fb0fbf4a09c Mon Sep 17 00:00:00 2001 From: Andy Seaborne Date: Mon, 17 Aug 2026 13:59:41 +0100 Subject: [PATCH 3/3] GH-4150: THREAT Model: Remove jena-shex as network reachable; note no imports for SHACL --- THREAT_MODEL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/THREAT_MODEL.md b/THREAT_MODEL.md index 150f42c601a..2b304268a9a 100644 --- a/THREAT_MODEL.md +++ b/THREAT_MODEL.md @@ -47,7 +47,7 @@ limitations under the License. | Stores + text index | `jena-tdb1`, `jena-tdb2`; **`jena-text` (Lucene)** | filesystem | **In.** On-disk store is operator-trusted and private to the owning process *(maintainer)*; the Lucene text index is reachable from SPARQL via `text:query` — an in-model query surface *(maintainer — afs flagged jena-text)* | | IRI / langtag | `jena-iri3986`, `jena-langtag`, `jena-base` | none | **In (input parsing)** *(inferred)* | | Extensions | `jena-geosparql`, `jena-serviceenhancer` | SERVICE | **In (reachable from queries)** *(inferred)* | -| Validations | `jena-shacl`, `jena-shex` | HTTP GET requests (imports) | **In (import-fetch = SSRF surface)** *(maintainer — afs)* | +| Validations | `jena-shacl` | HTTP GET requests | **In (no imports)** *(maintainer — afs)* | | Client/API helpers | `jena-rdfconnection`, `jena-querybuilder`, `jena-rdfpatch`, `jena-commonsrdf`, `jena-ontapi` | none | **In as libraries (memory/correctness)** *(inferred)* | | CLI tools | `jena-cmds` | filesystem | **In iff fed untrusted input; usually operator-run** *(inferred)* | | Examples / tests / benchmarks | `jena-examples`, `jena-integration-tests`, `jena-benchmarks` | n/a | **Out** *(see §3)* |