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 ; + ] .
