From 909f7e64fea6532a00b3eb8904bf620fc3cae6d1 Mon Sep 17 00:00:00 2001 From: dotasek Date: Thu, 9 Mar 2023 14:30:53 -0500 Subject: [PATCH] Additional zip-slip tests (#1162) * Additional zip-slip tests * Fix windows path test --- .../fhir/r4b/context/SimpleWorkerContext.java | 10 +- .../r4b/renderers/QuestionnaireRenderer.java | 41 ++- .../TerminologyCacheManager.java | 12 +- .../hl7/fhir/r4b/test/utils/ToolsHelper.java | 113 ++---- .../hl7/fhir/r4b/utils/IntegrityChecker.java | 341 ------------------ .../r4b/context/SimpleWorkerContextTests.java | 44 +++ .../TerminologyCacheManagerTests.java | 50 +-- .../zip-normal.zip | Bin .../zip-slip-2.zip | Bin .../test/resources/zip-slip/zip-slip-peer.zip | Bin 0 -> 157 bytes .../zip-slip-win.zip | Bin .../zip-slip.zip | Bin .../fhir/r5/context/SimpleWorkerContext.java | 10 +- .../r5/renderers/QuestionnaireRenderer.java | 41 ++- .../TerminologyCacheManager.java | 11 +- .../hl7/fhir/r5/test/utils/ToolsHelper.java | 113 ++---- .../r5/context/SimpleWorkerContextTests.java | 39 +- .../TerminologyCacheManagerTests.java | 51 +-- .../zip-normal.zip | Bin .../zip-slip-2.zip | Bin .../test/resources/zip-slip/zip-slip-peer.zip | Bin 0 -> 157 bytes .../zip-slip-win.zip | Bin .../zip-slip.zip | Bin .../org/hl7/fhir/utilities/Utilities.java | 71 ++-- .../org/hl7/fhir/utilities/UtilitiesTest.java | 233 ++++++++++-- .../org/hl7/fhir/validation/IgLoader.java | 12 +- .../conversion/tests/UtilitiesXTests.java | 15 +- .../hl7/fhir/validation/IgLoaderTests.java | 46 ++- .../org/hl7/fhir/validation/ScannerTest.java | 55 ++- .../{scanner => zip-slip}/zip-normal.zip | Bin .../{scanner => zip-slip}/zip-slip-2.zip | Bin .../test/resources/zip-slip/zip-slip-peer.zip | Bin 0 -> 157 bytes .../{scanner => zip-slip}/zip-slip-win.zip | Bin .../{scanner => zip-slip}/zip-slip.zip | Bin 34 files changed, 576 insertions(+), 732 deletions(-) delete mode 100644 org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/utils/IntegrityChecker.java create mode 100644 org.hl7.fhir.r4b/src/test/java/org/hl7/fhir/r4b/context/SimpleWorkerContextTests.java rename org.hl7.fhir.r4b/src/test/resources/{terminologyCacheManager => zip-slip}/zip-normal.zip (100%) rename org.hl7.fhir.r4b/src/test/resources/{terminologyCacheManager => zip-slip}/zip-slip-2.zip (100%) create mode 100755 org.hl7.fhir.r4b/src/test/resources/zip-slip/zip-slip-peer.zip rename org.hl7.fhir.r4b/src/test/resources/{terminologyCacheManager => zip-slip}/zip-slip-win.zip (100%) rename org.hl7.fhir.r4b/src/test/resources/{terminologyCacheManager => zip-slip}/zip-slip.zip (100%) rename org.hl7.fhir.r5/src/test/resources/{terminologyCacheManager => zip-slip}/zip-normal.zip (100%) rename org.hl7.fhir.r5/src/test/resources/{terminologyCacheManager => zip-slip}/zip-slip-2.zip (100%) create mode 100755 org.hl7.fhir.r5/src/test/resources/zip-slip/zip-slip-peer.zip rename org.hl7.fhir.r5/src/test/resources/{terminologyCacheManager => zip-slip}/zip-slip-win.zip (100%) rename org.hl7.fhir.r5/src/test/resources/{terminologyCacheManager => zip-slip}/zip-slip.zip (100%) rename org.hl7.fhir.validation/src/test/resources/{scanner => zip-slip}/zip-normal.zip (100%) rename org.hl7.fhir.validation/src/test/resources/{scanner => zip-slip}/zip-slip-2.zip (100%) create mode 100755 org.hl7.fhir.validation/src/test/resources/zip-slip/zip-slip-peer.zip rename org.hl7.fhir.validation/src/test/resources/{scanner => zip-slip}/zip-slip-win.zip (100%) rename org.hl7.fhir.validation/src/test/resources/{scanner => zip-slip}/zip-slip.zip (100%) diff --git a/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/context/SimpleWorkerContext.java b/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/context/SimpleWorkerContext.java index 6dedc80284..a0df53d0e0 100644 --- a/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/context/SimpleWorkerContext.java +++ b/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/context/SimpleWorkerContext.java @@ -471,9 +471,13 @@ public void loadFromFile(String file, IContextResourceLoader loader) throws IOEx private void loadFromStream(InputStream stream, IContextResourceLoader loader) throws IOException, FHIRException { ZipInputStream zip = new ZipInputStream(stream); - ZipEntry ze; - while ((ze = zip.getNextEntry()) != null) { - loadDefinitionItem(ze.getName(), zip, loader, null, null); + ZipEntry zipEntry; + while ((zipEntry = zip.getNextEntry()) != null) { + String entryName = zipEntry.getName(); + if (entryName.contains("..")) { + throw new RuntimeException("Entry with an illegal path: " + entryName); + } + loadDefinitionItem(entryName, zip, loader, null, null); zip.closeEntry(); } zip.close(); diff --git a/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/renderers/QuestionnaireRenderer.java b/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/renderers/QuestionnaireRenderer.java index 1ec71df584..99098c8bd1 100644 --- a/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/renderers/QuestionnaireRenderer.java +++ b/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/renderers/QuestionnaireRenderer.java @@ -37,6 +37,8 @@ import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator.TableModel; import org.hl7.fhir.utilities.xhtml.XhtmlNode; +import javax.annotation.Nonnull; + public class QuestionnaireRenderer extends TerminologyRenderer { public static final String EXT_QUESTIONNAIRE_ITEM_TYPE_ORIGINAL = "http://hl7.org/fhir/tools/StructureDefinition/original-item-type"; @@ -251,28 +253,28 @@ private boolean renderTreeItem(HierarchicalTableGenerator gen, List rows, Q Cell flags = gen.new Cell(); r.getCells().add(flags); if (i.getReadOnly()) { - flags.addPiece(gen.new Piece(Utilities.pathURL(context.getSpecificationLink(), "questionnaire-definitions.html#Questionnaire.item.readOnly"), null, "Is Readonly").addHtml(new XhtmlNode(NodeType.Element, "img").attribute("src", Utilities.path(context.getLocalPrefix(), "icon-qi-readonly.png")))); + flags.addPiece(gen.new Piece(Utilities.pathURL(context.getSpecificationLink(), "questionnaire-definitions.html#Questionnaire.item.readOnly"), null, "Is Readonly").addHtml(new XhtmlNode(NodeType.Element, "img").attribute("src", getImgPath("icon-qi-readonly.png")))); } if (ToolingExtensions.readBoolExtension(i, "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-isSubject")) { - flags.addPiece(gen.new Piece(getSDCLink("StructureDefinition-sdc-questionnaire-isSubject.html"), null, "Can change the subject of the questionnaire").addHtml(new XhtmlNode(NodeType.Element, "img").attribute("src", Utilities.path(context.getLocalPrefix(), "icon-qi-subject.png")))); + flags.addPiece(gen.new Piece(getSDCLink("StructureDefinition-sdc-questionnaire-isSubject.html"), null, "Can change the subject of the questionnaire").addHtml(new XhtmlNode(NodeType.Element, "img").attribute("src", getImgPath("icon-qi-subject.png")))); } if (ToolingExtensions.readBoolExtension(i, "http://hl7.org/fhir/StructureDefinition/questionnaire-hidden")) { - flags.addPiece(gen.new Piece(getSpecLink("extension-questionnaire-hidden.html"), null, "Is a hidden item").addHtml(new XhtmlNode(NodeType.Element, "img").attribute("src", Utilities.path(context.getLocalPrefix(), "icon-qi-hidden.png")))); + flags.addPiece(gen.new Piece(getSpecLink("extension-questionnaire-hidden.html"), null, "Is a hidden item").addHtml(new XhtmlNode(NodeType.Element, "img").attribute("src", getImgPath("icon-qi-hidden.png")))); } if (ToolingExtensions.readBoolExtension(i, "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-optionalDisplay")) { - flags.addPiece(gen.new Piece(getSDCLink("StructureDefinition-sdc-questionnaire-optionalDisplay.html"), null, "Is optional to display").addHtml(new XhtmlNode(NodeType.Element, "img").attribute("src", Utilities.path(context.getLocalPrefix(), "icon-qi-optional.png")))); + flags.addPiece(gen.new Piece(getSDCLink("StructureDefinition-sdc-questionnaire-optionalDisplay.html"), null, "Is optional to display").addHtml(new XhtmlNode(NodeType.Element, "img").attribute("src", getImgPath("icon-qi-optional.png")))); } if (i.hasExtension("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-observationLinkPeriod")) { - flags.addPiece(gen.new Piece(getSDCLink("StructureDefinition-sdc-questionnaire-observationLinkPeriod.html"), null, "Is linked to an observation").addHtml(new XhtmlNode(NodeType.Element, "img").attribute("src", Utilities.path(context.getLocalPrefix(), "icon-qi-observation.png")))); + flags.addPiece(gen.new Piece(getSDCLink("StructureDefinition-sdc-questionnaire-observationLinkPeriod.html"), null, "Is linked to an observation").addHtml(new XhtmlNode(NodeType.Element, "img").attribute("src", getImgPath("icon-qi-observation.png")))); } if (i.hasExtension("http://hl7.org/fhir/StructureDefinition/questionnaire-choiceOrientation")) { String code = ToolingExtensions.readStringExtension(i, "http://hl7.org/fhir/StructureDefinition/questionnaire-choiceOrientation"); - flags.addPiece(gen.new Piece(getSpecLink("extension-questionnaire-choiceorientation.html"), null, "Orientation: "+code).addHtml(new XhtmlNode(NodeType.Element, "img").attribute("src", Utilities.path(context.getLocalPrefix(), "icon-qi-"+code+".png")))); + flags.addPiece(gen.new Piece(getSpecLink("extension-questionnaire-choiceorientation.html"), null, "Orientation: "+code).addHtml(new XhtmlNode(NodeType.Element, "img").attribute("src", getImgPath("icon-qi-" + code + ".png")))); } if (i.hasExtension("http://hl7.org/fhir/StructureDefinition/questionnaire-displayCategory")) { CodeableConcept cc = i.getExtensionByUrl("http://hl7.org/fhir/StructureDefinition/questionnaire-displayCategory").getValueCodeableConcept(); String code = cc.getCode("http://hl7.org/fhir/questionnaire-display-category"); - flags.addPiece(gen.new Piece(getSDCLink("StructureDefinition-sdc-questionnaire-displayCategory.html"), null, "Category: "+code).addHtml(new XhtmlNode(NodeType.Element, "img").attribute("src", Utilities.path(context.getLocalPrefix(), "icon-qi-"+code+".png")))); + flags.addPiece(gen.new Piece(getSDCLink("StructureDefinition-sdc-questionnaire-displayCategory.html"), null, "Category: "+code).addHtml(new XhtmlNode(NodeType.Element, "img").attribute("src", getImgPath("icon-qi-" + code + ".png")))); } } Cell defn = gen.new Cell(); @@ -687,26 +689,26 @@ private boolean renderFormItem(XhtmlNode x, Questionnaire q, QuestionnaireItemCo if (ToolingExtensions.readBoolExtension(i, "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-isSubject")) { hasFlag = true; - flags.ah(getSDCLink("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-isSubject"), "Can change the subject of the questionnaire").img(Utilities.path(context.getLocalPrefix(), "icon-qi-subject.png"), "icon"); + flags.ah(getSDCLink("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-isSubject"), "Can change the subject of the questionnaire").img(getImgPath("icon-qi-subject.png"), "icon"); } if (ToolingExtensions.readBoolExtension(i, "http://hl7.org/fhir/StructureDefinition/questionnaire-hidden")) { hasFlag = true; - flags.ah(Utilities.pathURL(context.getSpecificationLink(), "extension-questionnaire-hidden.html"), "Is a hidden item").img(Utilities.path(context.getLocalPrefix(), "icon-qi-hidden.png"), "icon"); + flags.ah(Utilities.pathURL(context.getSpecificationLink(), "extension-questionnaire-hidden.html"), "Is a hidden item").img(getImgPath("icon-qi-hidden.png"), "icon"); d.style("background-color: #eeeeee"); } if (ToolingExtensions.readBoolExtension(i, "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-optionalDisplay")) { hasFlag = true; - flags.ah(getSDCLink("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-optionalDisplay"), "Is optional to display").img(Utilities.path(context.getLocalPrefix(), "icon-qi-optional.png"), "icon"); + flags.ah(getSDCLink("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-optionalDisplay"), "Is optional to display").img(getImgPath("icon-qi-optional.png"), "icon"); } if (i.hasExtension("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-observationLinkPeriod")) { hasFlag = true; - flags.ah(getSDCLink("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-observationLinkPeriod"), "Is linked to an observation").img(Utilities.path(context.getLocalPrefix(), "icon-qi-observation.png"), "icon"); + flags.ah(getSDCLink("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-observationLinkPeriod"), "Is linked to an observation").img(getImgPath("icon-qi-observation.png"), "icon"); } if (i.hasExtension("http://hl7.org/fhir/StructureDefinition/questionnaire-displayCategory")) { CodeableConcept cc = i.getExtensionByUrl("http://hl7.org/fhir/StructureDefinition/questionnaire-displayCategory").getValueCodeableConcept(); String code = cc.getCode("http://hl7.org/fhir/questionnaire-display-category"); hasFlag = true; - flags.ah(getSDCLink("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-displayCategory"), "Category: "+code).img(Utilities.path(context.getLocalPrefix(), "icon-qi-"+code+".png"), "icon"); + flags.ah(getSDCLink("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-displayCategory"), "Category: "+code).img(getImgPath("icon-qi-" + code + ".png"), "icon"); } if (i.hasMaxLength()) { @@ -788,6 +790,13 @@ private boolean renderFormItem(XhtmlNode x, Questionnaire q, QuestionnaireItemCo return hasExt; } + @Nonnull + private String getImgPath(String code) throws IOException { + return context.getLocalPrefix().length() > 0 + ? Utilities.path(context.getLocalPrefix(), code) + : Utilities.path(code); + } + private void item(XhtmlNode ul, String name, String value, String valueLink) { if (!Utilities.noString(value)) { ul.li().style("font-size: 10px").ah(valueLink).tx(name+": "+value); @@ -862,7 +871,7 @@ private boolean renderRootDefinition(XhtmlNode tbl, Questionnaire q, List "); } - td.img(Utilities.path(context.getLocalPrefix(), "icon_q_item.png"), "icon"); + td.img(getImgPath("icon_q_item.png"), "icon"); td.tx(" Item "); td.b().tx(qi.getLinkId()); @@ -1103,4 +1112,4 @@ private void defn(XhtmlNode tbl, String name, boolean value, boolean ifFalse) { } } -} \ No newline at end of file +} diff --git a/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/terminologies/TerminologyCacheManager.java b/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/terminologies/TerminologyCacheManager.java index ecd7e95e6a..1e1f8308cf 100644 --- a/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/terminologies/TerminologyCacheManager.java +++ b/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/terminologies/TerminologyCacheManager.java @@ -97,17 +97,17 @@ private void fillCache(String source) throws IOException { public static void unzip(InputStream is, String targetDir) throws IOException { try (ZipInputStream zipIn = new ZipInputStream(is)) { for (ZipEntry ze; (ze = zipIn.getNextEntry()) != null; ) { - String path = Path.of(Utilities.path(targetDir, ze.getName())).normalize().toFile().getAbsolutePath(); - - if (!path.startsWith(targetDir)) { + Path path = Path.of(Utilities.path(targetDir, ze.getName())).normalize(); + String pathString = path.toFile().getAbsolutePath(); + if (!path.startsWith(Path.of(targetDir).normalize())) { // see: https://snyk.io/research/zip-slip-vulnerability throw new RuntimeException("Entry with an illegal path: " + ze.getName()); } if (ze.isDirectory()) { - Utilities.createDirectory(path); + Utilities.createDirectory(pathString); } else { - Utilities.createDirectory(Utilities.getDirectoryForFile(path)); - TextFile.streamToFileNoClose(zipIn, path); + Utilities.createDirectory(Utilities.getDirectoryForFile(pathString)); + TextFile.streamToFileNoClose(zipIn, pathString); } } } diff --git a/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/test/utils/ToolsHelper.java b/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/test/utils/ToolsHelper.java index a9395e8285..dafbf55ff1 100644 --- a/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/test/utils/ToolsHelper.java +++ b/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/test/utils/ToolsHelper.java @@ -1,33 +1,33 @@ package org.hl7.fhir.r4b.test.utils; -/* - Copyright (c) 2011+, HL7, Inc. - All rights reserved. - - Redistribution and use in source and binary forms, with or without modification, - are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - * Neither the name of HL7 nor the names of its contributors may be used to - endorse or promote products derived from this software without specific - prior written permission. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, - INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - POSSIBILITY OF SUCH DAMAGE. - - */ +/* + Copyright (c) 2011+, HL7, Inc. + All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of HL7 nor the names of its contributors may be used to + endorse or promote products derived from this software without specific + prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. + + */ @@ -146,61 +146,6 @@ private void generateSnapshots(String[] args) throws IOException, FHIRException // } } - private Map getDefinitions(String definitions) throws IOException, FHIRException { - Map results = new HashMap(); - readDefinitions(results, loadDefinitions(definitions)); - return results; - } - - private void readDefinitions(Map map, byte[] defn) throws IOException { - ZipInputStream zip = new ZipInputStream(new ByteArrayInputStream(defn)); - ZipEntry ze; - while ((ze = zip.getNextEntry()) != null) { - if (!ze.getName().endsWith(".zip") && !ze.getName().endsWith(".jar") ) { // skip saxon .zip - String name = ze.getName(); - InputStream in = zip; - ByteArrayOutputStream b = new ByteArrayOutputStream(); - int n; - byte[] buf = new byte[1024]; - while ((n = in.read(buf, 0, 1024)) > -1) { - b.write(buf, 0, n); - } - map.put(name, b.toByteArray()); - } - zip.closeEntry(); - } - zip.close(); - } - - private byte[] loadDefinitions(String definitions) throws FHIRException, IOException { - byte[] defn; - // if (Utilities.noString(definitions)) { - // defn = loadFromUrl(MASTER_SOURCE); - // } else - if (definitions.startsWith("https:") || definitions.startsWith("http:")) { - defn = loadFromUrl(definitions); - } else if (new File(definitions).exists()) { - defn = loadFromFile(definitions); - } else - throw new FHIRException("Unable to find FHIR validation Pack (source = "+definitions+")"); - return defn; - } - - private byte[] loadFromUrl(String src) throws IOException { - URL url = new URL(src); - byte[] str = IOUtils.toByteArray(url.openStream()); - return str; - } - - private byte[] loadFromFile(String src) throws IOException { - FileInputStream in = new FileInputStream(src); - byte[] b = new byte[in.available()]; - in.read(b); - in.close(); - return b; - } - - protected XmlPullParser loadXml(InputStream stream) throws XmlPullParserException, IOException { BufferedInputStream input = new BufferedInputStream(stream); XmlPullParserFactory factory = XmlPullParserFactory.newInstance(System.getProperty(XmlPullParserFactory.PROPERTY_NAME), null); @@ -438,4 +383,4 @@ private void executeTest(String[] args) throws Throwable { } -} \ No newline at end of file +} diff --git a/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/utils/IntegrityChecker.java b/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/utils/IntegrityChecker.java deleted file mode 100644 index 34731ad342..0000000000 --- a/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/utils/IntegrityChecker.java +++ /dev/null @@ -1,341 +0,0 @@ -package org.hl7.fhir.r4b.utils; - -import java.io.BufferedOutputStream; -import java.io.ByteArrayOutputStream; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.FileWriter; -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.zip.ZipEntry; -import java.util.zip.ZipInputStream; - -import org.hl7.fhir.r4b.model.CodeType; -import org.hl7.fhir.r4b.model.SearchParameter; -import org.hl7.fhir.r4b.utils.IntegrityChecker.SearchParameterNode; -import org.hl7.fhir.r4b.utils.IntegrityChecker.SearchParameterNodeSorter; -import org.hl7.fhir.r4b.utils.IntegrityChecker.SearchParameterParamNode; -import org.hl7.fhir.r4b.utils.IntegrityChecker.SearchParameterParamNodeSorter; -import org.hl7.fhir.exceptions.FHIRFormatError; -import org.hl7.fhir.r4b.model.ElementDefinition; -import org.hl7.fhir.r4b.formats.JsonParser; -import org.hl7.fhir.r4b.formats.XmlParser; -import org.hl7.fhir.r4b.model.StructureDefinition; -import org.hl7.fhir.r4b.model.StructureDefinition.TypeDerivationRule; -import org.hl7.fhir.r4b.utils.IntegrityChecker.StructureDefinitionNode; -import org.hl7.fhir.r4b.utils.IntegrityChecker.StructureDefinitionNodeComparer; -import org.hl7.fhir.utilities.Utilities; -import org.hl7.fhir.utilities.npm.NpmPackage; - -public class IntegrityChecker { - - public class SearchParameterNodeSorter implements Comparator { - - @Override - public int compare(SearchParameterNode o1, SearchParameterNode o2) { - return o1.name.compareTo(o2.name); - } - - } - - public class SearchParameterParamNodeSorter implements Comparator { - - @Override - public int compare(SearchParameterParamNode o1, SearchParameterParamNode o2) { - return o1.sp.getCode().compareTo(o2.sp.getCode()); - } - - } - - public class SearchParameterParamNode { - SearchParameter sp; - boolean only; - public SearchParameterParamNode(SearchParameter sp, boolean only) { - super(); - this.sp = sp; - this.only = only; - } - - - } - - public class SearchParameterNode { - - private String name; - private List params = new ArrayList<>(); - - public SearchParameterNode(String name) { - this.name = name; - } - - } - - public class StructureDefinitionNodeComparer implements Comparator { - - @Override - public int compare(StructureDefinitionNode arg0, StructureDefinitionNode arg1) { - if ( arg0.sd.getType().equals(arg1.sd.getType())) { - return arg0.sd.getName().compareTo(arg1.sd.getName()); - } else { - return arg0.sd.getType().compareTo(arg1.sd.getType()); - } - } - - } - - public class StructureDefinitionNode { - StructureDefinition sd; - List children = new ArrayList<>(); - - public StructureDefinitionNode(StructureDefinition sd) { - this.sd = sd; - } - - } - - private NpmPackage npm; - - public static void main(String[] args) throws Exception { - IntegrityChecker check = new IntegrityChecker(); - check.load(args[0]); - check.check(args[1]); - } - - private void check(String dst) throws IOException { - dumpSD(new FileWriter("/Users/grahamegrieve/temp/r4b-dump.txt")); -// checkSD(); -// checkSP(); -// checkExamplesXml(dst); -// checkExamplesJson(dst); - } - - - private void dumpSD(FileWriter w) throws FHIRFormatError, IOException { - Map map = new HashMap<>(); - for (String sdn : npm.listResources("StructureDefinition")) { - InputStream s = npm.load(sdn); - StructureDefinition sd = (StructureDefinition) new JsonParser().parse(s); - map.put(sd.getUrl(), sd); - } - msg("Loaded "+map.size()+" Structures"); - List structures = new ArrayList<>(); - for (StructureDefinition sd : map.values()) { - structures.add(sd.getUrl()); - } - Collections.sort(structures); - - for (String sdn : structures) { - dumpSD(map.get(sdn), map, w); - } - } - - private void dumpSD(StructureDefinition sd, Map map, FileWriter w) throws IOException { - if (sd.getDerivation() == TypeDerivationRule.SPECIALIZATION) { - StructureDefinition base = sd.hasBaseDefinition() ? map.get(sd.getBaseDefinition()) : null; - System.out.println(sd.getType()+(base == null ? "" : " : "+base.getType())); - w.append(sd.getType()+(base == null ? "" : " : "+base.getType())+"\r\n"); - for (ElementDefinition ed : sd.getSnapshot().getElement()) { - w.append(" "+Utilities.padLeft("", ' ', Utilities.charCount(ed.getPath(), '.'))+tail(ed.getPath())+" : "+ed.typeSummary()+" ["+ed.getMin()+".."+ed.getMax()+"]"+"\r\n"); - } - } - } - - - private String tail(String path) { - return path.contains(".") ? path.substring(path.lastIndexOf('.')+1) : path; - } - - private Map loadZip(InputStream stream) throws IOException { - Map res = new HashMap(); - ZipInputStream zip = new ZipInputStream(stream); - ZipEntry ze; - while ((ze = zip.getNextEntry()) != null) { - int size; - byte[] buffer = new byte[2048]; - - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - BufferedOutputStream bos = new BufferedOutputStream(bytes, buffer.length); - - while ((size = zip.read(buffer, 0, buffer.length)) != -1) { - bos.write(buffer, 0, size); - } - bos.flush(); - bos.close(); - res.put(ze.getName(), bytes.toByteArray()); - - zip.closeEntry(); - } - zip.close(); - return res; - } - - private void checkExamplesJson(String dst) throws FileNotFoundException, IOException { - Map files = loadZip(new FileInputStream(Utilities.path(dst, "examples-json.zip"))); - for (Entry t : files.entrySet()) { - try { - new JsonParser().parse(t.getValue()); - System.out.print("."); - } catch (Exception e) { - System.out.println(""); - System.out.println("Error parsing "+t.getKey()+": "+e.getMessage()); - } - } - } - -private void checkExamplesXml(String dst) throws FileNotFoundException, IOException { - Map files = loadZip(new FileInputStream(Utilities.path(dst, "examples.zip"))); - for (Entry t : files.entrySet()) { - try { - new XmlParser().parse(t.getValue()); - System.out.print("."); - } catch (Exception e) { - System.out.println(""); - System.out.println("Error parsing "+t.getKey()+": "+e.getMessage()); - } - } - } - - private void checkSP() throws IOException { - List list = new ArrayList<>(); - for (String sdn : npm.listResources("SearchParameter")) { - InputStream s = npm.load(sdn); - SearchParameter sp = (SearchParameter) new JsonParser().parse(s); - list.add(sp); - } - msg("Loaded "+list.size()+" resources"); - Map map = new HashMap<>(); - for (SearchParameter sp : list) { - for (CodeType c : sp.getBase()) { - String s = c.primitiveValue(); - if (!map.containsKey(s)) { - map.put(s, new SearchParameterNode(s)); - } - addNode(sp, sp.getBase().size() == 1, map.get(s)); - } - } - for (SearchParameterNode node : sort(map.values())) { - dump(node); - } - } - - - private void dump(SearchParameterNode node) { - msg(node.name); - for (SearchParameterParamNode p : sortP(node.params)) { - String exp = p.sp.getExperimental() ? " **exp!" : ""; - if (p.only) { - msg(" "+p.sp.getCode()+exp); - } else { - msg(" *"+p.sp.getCode()+exp); - } - } - - } - - private List sortP(List params) { - List res = new ArrayList<>(); - res.addAll(params); - Collections.sort(res, new SearchParameterParamNodeSorter()); - return res; - } - - private List sort(Collection values) { - List res = new ArrayList<>(); - res.addAll(values); - Collections.sort(res, new SearchParameterNodeSorter()); - return res; - } - - private void addNode(SearchParameter sp, boolean b, SearchParameterNode node) { - node.params.add(new SearchParameterParamNode(sp, b)); - } - - private void checkSD() throws IOException { - Map map = new HashMap<>(); - for (String sdn : npm.listResources("StructureDefinition")) { - InputStream s = npm.load(sdn); - StructureDefinition sd = (StructureDefinition) new JsonParser().parse(s); - map.put(sd.getUrl(), sd); - } - msg("Loaded "+map.size()+" resources"); - List roots = new ArrayList<>(); - for (StructureDefinition sd : map.values()) { - if (sd.getBaseDefinition() == null || !map.containsKey(sd.getBaseDefinition())) { - StructureDefinitionNode root = new StructureDefinitionNode(sd); - roots.add(root); - analyse(root, map); - } - } - sort(roots); - for (StructureDefinitionNode root : roots) { - describe(root, 0); - } - } - - private void sort(List list) { - Collections.sort(list, new StructureDefinitionNodeComparer()); - - } - - private void analyse(StructureDefinitionNode node, Map map) { - for (StructureDefinition sd : map.values()) { - if (node.sd.getUrl().equals(sd.getBaseDefinition())) { - StructureDefinitionNode c = new StructureDefinitionNode(sd); - node.children.add(c); - analyse(c, map); - } - } - sort(node.children); - } - - private void describe(StructureDefinitionNode node, int level) { - describe(node.sd, level); - for (StructureDefinitionNode c : node.children) { - describe(c, level+1); - } - } - - private void describe(StructureDefinition sd, int level) { - String exp = sd.getExperimental() ? " **exp!" : ""; - if (sd.getDerivation() == TypeDerivationRule.CONSTRAINT) { - msg(Utilities.padLeft("", ' ', level)+sd.getType()+" / "+sd.getName()+" ("+sd.getUrl()+")"+exp); - } else { - msg(Utilities.padLeft("", ' ', level)+sd.getType()+" : "+sd.getKind()+exp); - } - } - -// private int analyse(Map map, List list, StructureDefinition sd) { -// if (!list.contains(sd)) { -// int level = 0; -// if (sd.hasBaseDefinition()) { -// StructureDefinition p = map.get(sd.getBaseDefinition()); -// if (p == null) { -// msg("Can't find parent "+sd.getBaseDefinition()+" for "+sd.getUrl()); -// } else { -// level = analyse(map, list, p) + 1; -// } -// } -// list.add(sd); -// sd.setUserData("level", level); -// } -// } - - private void msg(String string) { - System.out.println(string); - } - - private void load(String folder) throws IOException { - msg("Loading resources from "+folder); - npm = NpmPackage.fromFolder(folder); - } - -} \ No newline at end of file diff --git a/org.hl7.fhir.r4b/src/test/java/org/hl7/fhir/r4b/context/SimpleWorkerContextTests.java b/org.hl7.fhir.r4b/src/test/java/org/hl7/fhir/r4b/context/SimpleWorkerContextTests.java new file mode 100644 index 0000000000..0c97c6b70c --- /dev/null +++ b/org.hl7.fhir.r4b/src/test/java/org/hl7/fhir/r4b/context/SimpleWorkerContextTests.java @@ -0,0 +1,44 @@ +package org.hl7.fhir.r4b.context; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class SimpleWorkerContextTests { + public static Stream zipSlipData() { + + return Stream.of( + Arguments.of("zip-slip/zip-slip.zip", "Entry with an illegal path: ../evil.txt"), + Arguments.of("zip-slip/zip-slip-2.zip", "Entry with an illegal path: child/../../evil.txt"), + Arguments.of("zip-slip/zip-slip-peer.zip", "Entry with an illegal path: ../childpeer/evil.txt"), + Arguments.of("zip-slip/zip-slip-win.zip", "Entry with an illegal path: ../evil.txt") + ); + } + + @ParameterizedTest(name = "{index}: file {0}") + @MethodSource("zipSlipData") + public void testLoadFromClasspathZipSlip(String classPath, String expectedMessage) { + RuntimeException thrown = Assertions.assertThrows(RuntimeException.class, () -> {SimpleWorkerContext.fromClassPath(classPath);}); + assertNotNull(thrown); + assertEquals(expectedMessage, thrown.getMessage()); + } + + @Test + public void testLoadFromClasspathBinaries() throws IOException { + SimpleWorkerContext simpleWorkerContext = SimpleWorkerContext.fromClassPath("zip-slip/zip-normal.zip"); + + final String testPath = "zip-normal/depth1/test.txt"; + assertTrue(simpleWorkerContext.binaries.containsKey(testPath)); + String testFileContent = new String(simpleWorkerContext.binaries.get(testPath), StandardCharsets.UTF_8); + assertEquals("dummy file content", testFileContent); + } +} diff --git a/org.hl7.fhir.r4b/src/test/java/org/hl7/fhir/r4b/terminologies/TerminologyCacheManagerTests.java b/org.hl7.fhir.r4b/src/test/java/org/hl7/fhir/r4b/terminologies/TerminologyCacheManagerTests.java index 22f4cf7749..6c542be03b 100644 --- a/org.hl7.fhir.r4b/src/test/java/org/hl7/fhir/r4b/terminologies/TerminologyCacheManagerTests.java +++ b/org.hl7.fhir.r4b/src/test/java/org/hl7/fhir/r4b/terminologies/TerminologyCacheManagerTests.java @@ -5,36 +5,33 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; +import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @TestInstance(TestInstance.Lifecycle.PER_CLASS) public class TerminologyCacheManagerTests implements ResourceLoaderTests { - - public static final String ZIP_NORMAL_ZIP = "zip-normal.zip"; - public static final String ZIP_SLIP_ZIP = "zip-slip.zip"; - - public static final String ZIP_SLIP_2_ZIP = "zip-slip-2.zip"; - - public static final String ZIP_SLIP_WIN_ZIP = "zip-slip-win.zip"; + Path tempDir; @BeforeAll public void beforeAll() throws IOException { tempDir = Files.createTempDirectory("terminology-cache-manager"); tempDir.resolve("child").toFile().mkdir(); - getResourceAsInputStream("terminologyCacheManager", ZIP_SLIP_ZIP); } @Test public void testNormalZip() throws IOException { - InputStream normalInputStream = getResourceAsInputStream( "terminologyCacheManager", ZIP_NORMAL_ZIP); + InputStream normalInputStream = getResourceAsInputStream("zip-slip", "zip-normal.zip"); TerminologyCacheManager.unzip( normalInputStream, tempDir.toFile().getAbsolutePath()); Path expectedFilePath = tempDir.resolve("zip-normal").resolve("depth1").resolve("test.txt"); @@ -42,36 +39,25 @@ public void testNormalZip() throws IOException { assertEquals("dummy file content", actualContent); } - @Test - public void testSlipZip() throws IOException { - RuntimeException thrown = Assertions.assertThrows(RuntimeException.class, () -> { - InputStream slipInputStream = getResourceAsInputStream( "terminologyCacheManager", ZIP_SLIP_ZIP); - TerminologyCacheManager.unzip( slipInputStream, tempDir.toFile().getAbsolutePath()); - //Code under test - }); - assertNotNull(thrown); - assertEquals("Entry with an illegal path: ../evil.txt", thrown.getMessage()); - } + public static Stream zipSlipData() { - @Test - public void testSlip2Zip() throws IOException { - RuntimeException thrown = Assertions.assertThrows(RuntimeException.class, () -> { - InputStream slipInputStream = getResourceAsInputStream( "terminologyCacheManager", ZIP_SLIP_2_ZIP); - TerminologyCacheManager.unzip( slipInputStream, tempDir.toFile().getAbsolutePath()); - //Code under test - }); - assertNotNull(thrown); - assertEquals("Entry with an illegal path: child/../../evil.txt", thrown.getMessage()); + return Stream.of( + Arguments.of("zip-slip.zip", "../evil.txt"), + Arguments.of("zip-slip-2.zip", "child/../../evil.txt"), + Arguments.of("zip-slip-peer.zip", "../childpeer/evil.txt"), + Arguments.of("zip-slip-win.zip", "../evil.txt") + ); } - @Test - public void testSlipZipWin() throws IOException { + @ParameterizedTest(name = "{index}: file {0}") + @MethodSource("zipSlipData") + public void testLoadFromClasspathZipSlip(String fileName, String expectedMessage) { RuntimeException thrown = Assertions.assertThrows(RuntimeException.class, () -> { - InputStream slipInputStream = getResourceAsInputStream( "terminologyCacheManager", ZIP_SLIP_WIN_ZIP); + InputStream slipInputStream = getResourceAsInputStream("zip-slip", fileName); TerminologyCacheManager.unzip( slipInputStream, tempDir.toFile().getAbsolutePath()); //Code under test }); assertNotNull(thrown); - assertEquals("Entry with an illegal path: ../evil.txt", thrown.getMessage()); + Assertions.assertTrue(thrown.getMessage().endsWith(expectedMessage)); } } diff --git a/org.hl7.fhir.r4b/src/test/resources/terminologyCacheManager/zip-normal.zip b/org.hl7.fhir.r4b/src/test/resources/zip-slip/zip-normal.zip similarity index 100% rename from org.hl7.fhir.r4b/src/test/resources/terminologyCacheManager/zip-normal.zip rename to org.hl7.fhir.r4b/src/test/resources/zip-slip/zip-normal.zip diff --git a/org.hl7.fhir.r4b/src/test/resources/terminologyCacheManager/zip-slip-2.zip b/org.hl7.fhir.r4b/src/test/resources/zip-slip/zip-slip-2.zip similarity index 100% rename from org.hl7.fhir.r4b/src/test/resources/terminologyCacheManager/zip-slip-2.zip rename to org.hl7.fhir.r4b/src/test/resources/zip-slip/zip-slip-2.zip diff --git a/org.hl7.fhir.r4b/src/test/resources/zip-slip/zip-slip-peer.zip b/org.hl7.fhir.r4b/src/test/resources/zip-slip/zip-slip-peer.zip new file mode 100755 index 0000000000000000000000000000000000000000..1b1779ab96d56a581dd23fa7007fd4be328af6f5 GIT binary patch literal 157 zcmWIWW@Zs#0D%=%#$k2Q{vLurHVBIXv7VlOaz rows, Q Cell flags = gen.new Cell(); r.getCells().add(flags); if (i.getReadOnly()) { - flags.addPiece(gen.new Piece(Utilities.pathURL(context.getLink(KnownLinkType.SPEC), "questionnaire-definitions.html#Questionnaire.item.readOnly"), null, "Is Readonly").addHtml(new XhtmlNode(NodeType.Element, "img").attribute("alt", "icon").attribute("src", Utilities.path(context.getLocalPrefix(), "icon-qi-readonly.png")))); + flags.addPiece(gen.new Piece(Utilities.pathURL(context.getLink(KnownLinkType.SPEC), "questionnaire-definitions.html#Questionnaire.item.readOnly"), null, "Is Readonly").addHtml(new XhtmlNode(NodeType.Element, "img").attribute("alt", "icon").attribute("src", getImgPath("icon-qi-readonly.png")))); } if (ToolingExtensions.readBoolExtension(i, "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-isSubject")) { - flags.addPiece(gen.new Piece(getSDCLink("StructureDefinition-sdc-questionnaire-isSubject.html"), null, "Can change the subject of the questionnaire").addHtml(new XhtmlNode(NodeType.Element, "img").attribute("alt", "icon").attribute("src", Utilities.path(context.getLocalPrefix(), "icon-qi-subject.png")))); + flags.addPiece(gen.new Piece(getSDCLink("StructureDefinition-sdc-questionnaire-isSubject.html"), null, "Can change the subject of the questionnaire").addHtml(new XhtmlNode(NodeType.Element, "img").attribute("alt", "icon").attribute("src", getImgPath("icon-qi-subject.png")))); } if (ToolingExtensions.readBoolExtension(i, ToolingExtensions.EXT_Q_HIDDEN)) { - flags.addPiece(gen.new Piece(getSpecLink("extension-questionnaire-hidden.html"), null, "Is a hidden item").addHtml(new XhtmlNode(NodeType.Element, "img").attribute("alt", "icon").attribute("src", Utilities.path(context.getLocalPrefix(), "icon-qi-hidden.png")))); + flags.addPiece(gen.new Piece(getSpecLink("extension-questionnaire-hidden.html"), null, "Is a hidden item").addHtml(new XhtmlNode(NodeType.Element, "img").attribute("alt", "icon").attribute("src", getImgPath("icon-qi-hidden.png")))); } if (ToolingExtensions.readBoolExtension(i, ToolingExtensions.EXT_Q_OTP_DISP)) { - flags.addPiece(gen.new Piece(getSDCLink("StructureDefinition-sdc-questionnaire-optionalDisplay.html"), null, "Is optional to display").addHtml(new XhtmlNode(NodeType.Element, "img").attribute("alt", "icon").attribute("src", Utilities.path(context.getLocalPrefix(), "icon-qi-optional.png")))); + flags.addPiece(gen.new Piece(getSDCLink("StructureDefinition-sdc-questionnaire-optionalDisplay.html"), null, "Is optional to display").addHtml(new XhtmlNode(NodeType.Element, "img").attribute("alt", "icon").attribute("src", getImgPath("icon-qi-optional.png")))); } if (i.hasExtension("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-observationLinkPeriod")) { - flags.addPiece(gen.new Piece(getSDCLink("StructureDefinition-sdc-questionnaire-observationLinkPeriod.html"), null, "Is linked to an observation").addHtml(new XhtmlNode(NodeType.Element, "img").attribute("alt", "icon").attribute("src", Utilities.path(context.getLocalPrefix(), "icon-qi-observation.png")))); + flags.addPiece(gen.new Piece(getSDCLink("StructureDefinition-sdc-questionnaire-observationLinkPeriod.html"), null, "Is linked to an observation").addHtml(new XhtmlNode(NodeType.Element, "img").attribute("alt", "icon").attribute("src", getImgPath("icon-qi-observation.png")))); } if (i.hasExtension(ToolingExtensions.EXT_Q_CHOICE_ORIENT)) { String code = ToolingExtensions.readStringExtension(i, ToolingExtensions.EXT_Q_CHOICE_ORIENT); - flags.addPiece(gen.new Piece(getSpecLink("extension-questionnaire-choiceorientation.html"), null, "Orientation: "+code).addHtml(new XhtmlNode(NodeType.Element, "img").attribute("alt", "icon").attribute("src", Utilities.path(context.getLocalPrefix(), "icon-qi-"+code+".png")))); + flags.addPiece(gen.new Piece(getSpecLink("extension-questionnaire-choiceorientation.html"), null, "Orientation: "+code).addHtml(new XhtmlNode(NodeType.Element, "img").attribute("alt", "icon").attribute("src", getImgPath("icon-qi-" + code + ".png")))); } if (i.hasExtension(ToolingExtensions.EXT_Q_DISPLAY_CAT)) { CodeableConcept cc = i.getExtensionByUrl(ToolingExtensions.EXT_Q_DISPLAY_CAT).getValueCodeableConcept(); String code = cc.getCode("http://hl7.org/fhir/questionnaire-display-category"); - flags.addPiece(gen.new Piece(getSDCLink("StructureDefinition-sdc-questionnaire-displayCategory.html"), null, "Category: "+code).addHtml(new XhtmlNode(NodeType.Element, "img").attribute("alt", "icon").attribute("src", Utilities.path(context.getLocalPrefix(), "icon-qi-"+code+".png")))); + flags.addPiece(gen.new Piece(getSDCLink("StructureDefinition-sdc-questionnaire-displayCategory.html"), null, "Category: "+code).addHtml(new XhtmlNode(NodeType.Element, "img").attribute("alt", "icon").attribute("src", getImgPath("icon-qi-" + code + ".png")))); } } Cell defn = gen.new Cell(); @@ -690,26 +692,26 @@ private boolean renderFormItem(XhtmlNode x, Questionnaire q, QuestionnaireItemCo if (ToolingExtensions.readBoolExtension(i, "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-isSubject")) { hasFlag = true; - flags.ah(getSDCLink("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-isSubject"), "Can change the subject of the questionnaire").img(Utilities.path(context.getLocalPrefix(), "icon-qi-subject.png"), "icon"); + flags.ah(getSDCLink("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-isSubject"), "Can change the subject of the questionnaire").img(getImgPath("icon-qi-subject.png"), "icon"); } if (ToolingExtensions.readBoolExtension(i, ToolingExtensions.EXT_Q_HIDDEN)) { hasFlag = true; - flags.ah(Utilities.pathURL(context.getLink(KnownLinkType.SPEC), "extension-questionnaire-hidden.html"), "Is a hidden item").img(Utilities.path(context.getLocalPrefix(), "icon-qi-hidden.png"), "icon"); + flags.ah(Utilities.pathURL(context.getLink(KnownLinkType.SPEC), "extension-questionnaire-hidden.html"), "Is a hidden item").img(getImgPath("icon-qi-hidden.png"), "icon"); d.style("background-color: #eeeeee"); } if (ToolingExtensions.readBoolExtension(i, ToolingExtensions.EXT_Q_OTP_DISP)) { hasFlag = true; - flags.ah(getSDCLink(ToolingExtensions.EXT_Q_OTP_DISP), "Is optional to display").img(Utilities.path(context.getLocalPrefix(), "icon-qi-optional.png"), "icon"); + flags.ah(getSDCLink(ToolingExtensions.EXT_Q_OTP_DISP), "Is optional to display").img(getImgPath("icon-qi-optional.png"), "icon"); } if (i.hasExtension("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-observationLinkPeriod")) { hasFlag = true; - flags.ah(getSDCLink("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-observationLinkPeriod"), "Is linked to an observation").img(Utilities.path(context.getLocalPrefix(), "icon-qi-observation.png"), "icon"); + flags.ah(getSDCLink("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-observationLinkPeriod"), "Is linked to an observation").img(getImgPath("icon-qi-observation.png"), "icon"); } if (i.hasExtension(ToolingExtensions.EXT_Q_DISPLAY_CAT)) { CodeableConcept cc = i.getExtensionByUrl(ToolingExtensions.EXT_Q_DISPLAY_CAT).getValueCodeableConcept(); String code = cc.getCode("http://hl7.org/fhir/questionnaire-display-category"); hasFlag = true; - flags.ah(getSDCLink("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-displayCategory"), "Category: "+code).img(Utilities.path(context.getLocalPrefix(), "icon-qi-"+code+".png"), "icon"); + flags.ah(getSDCLink("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-displayCategory"), "Category: "+code).img(getImgPath("icon-qi-" + code + ".png"), "icon"); } if (i.hasMaxLength()) { @@ -791,6 +793,13 @@ private boolean renderFormItem(XhtmlNode x, Questionnaire q, QuestionnaireItemCo return hasExt; } + @Nonnull + private String getImgPath(String code) throws IOException { + return context.getLocalPrefix().length() > 0 + ? Utilities.path(context.getLocalPrefix(), code) + : Utilities.path(code); + } + private void item(XhtmlNode ul, String name, String value, String valueLink) { if (!Utilities.noString(value)) { ul.li().style("font-size: 10px").ah(valueLink).tx(name+": "+value); @@ -865,7 +874,7 @@ private boolean renderRootDefinition(XhtmlNode tbl, Questionnaire q, List "); } - td.img(Utilities.path(context.getLocalPrefix(), "icon_q_item.png"), "icon"); + td.img(getImgPath("icon_q_item.png"), "icon"); td.tx(" Item "); td.b().tx(qi.getLinkId()); @@ -1106,4 +1115,4 @@ private void defn(XhtmlNode tbl, String name, boolean value, boolean ifFalse) { } } -} \ No newline at end of file +} diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/TerminologyCacheManager.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/TerminologyCacheManager.java index 28783f4d09..60fa1cf2d5 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/TerminologyCacheManager.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/TerminologyCacheManager.java @@ -97,16 +97,17 @@ private void fillCache(String source) throws IOException { public static void unzip(InputStream is, String targetDir) throws IOException { try (ZipInputStream zipIn = new ZipInputStream(is)) { for (ZipEntry ze; (ze = zipIn.getNextEntry()) != null; ) { - String path = Path.of(Utilities.path(targetDir, ze.getName())).normalize().toFile().getAbsolutePath(); - if (!path.startsWith(targetDir)) { + Path path = Path.of(Utilities.path(targetDir, ze.getName())).normalize(); + String pathString = path.toFile().getAbsolutePath(); + if (!path.startsWith(Path.of(targetDir).normalize())) { // see: https://snyk.io/research/zip-slip-vulnerability throw new RuntimeException("Entry with an illegal path: " + ze.getName()); } if (ze.isDirectory()) { - Utilities.createDirectory(path); + Utilities.createDirectory(pathString); } else { - Utilities.createDirectory(Utilities.getDirectoryForFile(path)); - TextFile.streamToFileNoClose(zipIn, path); + Utilities.createDirectory(Utilities.getDirectoryForFile(pathString)); + TextFile.streamToFileNoClose(zipIn, pathString); } } } diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/test/utils/ToolsHelper.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/test/utils/ToolsHelper.java index 28336762ed..2e2cf3aba9 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/test/utils/ToolsHelper.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/test/utils/ToolsHelper.java @@ -1,33 +1,33 @@ package org.hl7.fhir.r5.test.utils; -/* - Copyright (c) 2011+, HL7, Inc. - All rights reserved. - - Redistribution and use in source and binary forms, with or without modification, - are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - * Neither the name of HL7 nor the names of its contributors may be used to - endorse or promote products derived from this software without specific - prior written permission. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, - INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - POSSIBILITY OF SUCH DAMAGE. - - */ +/* + Copyright (c) 2011+, HL7, Inc. + All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of HL7 nor the names of its contributors may be used to + endorse or promote products derived from this software without specific + prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. + + */ @@ -146,61 +146,6 @@ private void generateSnapshots(String[] args) throws IOException, FHIRException // } } - private Map getDefinitions(String definitions) throws IOException, FHIRException { - Map results = new HashMap(); - readDefinitions(results, loadDefinitions(definitions)); - return results; - } - - private void readDefinitions(Map map, byte[] defn) throws IOException { - ZipInputStream zip = new ZipInputStream(new ByteArrayInputStream(defn)); - ZipEntry ze; - while ((ze = zip.getNextEntry()) != null) { - if (!ze.getName().endsWith(".zip") && !ze.getName().endsWith(".jar") ) { // skip saxon .zip - String name = ze.getName(); - InputStream in = zip; - ByteArrayOutputStream b = new ByteArrayOutputStream(); - int n; - byte[] buf = new byte[1024]; - while ((n = in.read(buf, 0, 1024)) > -1) { - b.write(buf, 0, n); - } - map.put(name, b.toByteArray()); - } - zip.closeEntry(); - } - zip.close(); - } - - private byte[] loadDefinitions(String definitions) throws FHIRException, IOException { - byte[] defn; - // if (Utilities.noString(definitions)) { - // defn = loadFromUrl(MASTER_SOURCE); - // } else - if (definitions.startsWith("https:") || definitions.startsWith("http:")) { - defn = loadFromUrl(definitions); - } else if (new File(definitions).exists()) { - defn = loadFromFile(definitions); - } else - throw new FHIRException("Unable to find FHIR validation Pack (source = "+definitions+")"); - return defn; - } - - private byte[] loadFromUrl(String src) throws IOException { - URL url = new URL(src); - byte[] str = IOUtils.toByteArray(url.openStream()); - return str; - } - - private byte[] loadFromFile(String src) throws IOException { - FileInputStream in = new FileInputStream(src); - byte[] b = new byte[in.available()]; - in.read(b); - in.close(); - return b; - } - - protected XmlPullParser loadXml(InputStream stream) throws XmlPullParserException, IOException { BufferedInputStream input = new BufferedInputStream(stream); XmlPullParserFactory factory = XmlPullParserFactory.newInstance(System.getProperty(XmlPullParserFactory.PROPERTY_NAME), null); @@ -438,4 +383,4 @@ private void executeTest(String[] args) throws Throwable { } -} \ No newline at end of file +} diff --git a/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/context/SimpleWorkerContextTests.java b/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/context/SimpleWorkerContextTests.java index 78d5dfa4d0..44aff0053c 100644 --- a/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/context/SimpleWorkerContextTests.java +++ b/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/context/SimpleWorkerContextTests.java @@ -1,6 +1,6 @@ package org.hl7.fhir.r5.context; -import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.argThat; @@ -9,9 +9,9 @@ import static org.mockito.Mockito.times; import java.io.IOException; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.stream.Stream; import org.hl7.fhir.r5.model.CapabilityStatement; import org.hl7.fhir.r5.model.CodeableConcept; @@ -27,9 +27,13 @@ import org.hl7.fhir.r5.utils.validation.ValidationContextCarrier; import org.hl7.fhir.utilities.ToolingClientLogger; import org.hl7.fhir.utilities.validation.ValidationOptions; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.mockito.ArgumentMatcher; import org.mockito.Mock; import org.mockito.Mockito; @@ -423,4 +427,31 @@ public void testInitializationWithClient() { Mockito.verify(context).setTxCaps(terminologyCapabilities); } + public static Stream zipSlipData() { + + return Stream.of( + Arguments.of("zip-slip/zip-slip.zip", "Entry with an illegal path: ../evil.txt"), + Arguments.of("zip-slip/zip-slip-2.zip", "Entry with an illegal path: child/../../evil.txt"), + Arguments.of("zip-slip/zip-slip-peer.zip", "Entry with an illegal path: ../childpeer/evil.txt"), + Arguments.of("zip-slip/zip-slip-win.zip", "Entry with an illegal path: ../evil.txt") + ); + } + + @ParameterizedTest(name = "{index}: file {0}") + @MethodSource("zipSlipData") + public void testLoadFromClasspathZipSlip(String classPath, String expectedMessage) { + RuntimeException thrown = Assertions.assertThrows(RuntimeException.class, () -> {new SimpleWorkerContext.SimpleWorkerContextBuilder().fromClassPath(classPath);}); + assertNotNull(thrown); + assertEquals(expectedMessage, thrown.getMessage()); + } + + @Test + public void testLoadFromClasspathBinaries() throws IOException { + SimpleWorkerContext simpleWorkerContext = new SimpleWorkerContext.SimpleWorkerContextBuilder().fromClassPath("zip-slip/zip-normal.zip"); + + final String testPath = "zip-normal/depth1/test.txt"; + assertTrue(simpleWorkerContext.getBinaryKeysAsSet().contains(testPath)); + String testFileContent = new String(simpleWorkerContext.getBinaryForKey(testPath), StandardCharsets.UTF_8); + assertEquals("dummy file content", testFileContent); + } } diff --git a/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/terminologies/TerminologyCacheManagerTests.java b/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/terminologies/TerminologyCacheManagerTests.java index cc5f052e4a..aeef930757 100644 --- a/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/terminologies/TerminologyCacheManagerTests.java +++ b/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/terminologies/TerminologyCacheManagerTests.java @@ -5,37 +5,34 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; +import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @TestInstance(TestInstance.Lifecycle.PER_CLASS) public class TerminologyCacheManagerTests implements ResourceLoaderTests { - - public static final String ZIP_NORMAL_ZIP = "zip-normal.zip"; - public static final String ZIP_SLIP_ZIP = "zip-slip.zip"; - - public static final String ZIP_SLIP_2_ZIP = "zip-slip-2.zip"; - - public static final String ZIP_SLIP_WIN_ZIP = "zip-slip-win.zip"; + Path tempDir; @BeforeAll public void beforeAll() throws IOException { tempDir = Files.createTempDirectory("terminology-cache-manager"); tempDir.resolve("child").toFile().mkdir(); - getResourceAsInputStream("terminologyCacheManager", ZIP_SLIP_ZIP); } @Test public void testNormalZip() throws IOException { - InputStream normalInputStream = getResourceAsInputStream( "terminologyCacheManager", ZIP_NORMAL_ZIP); + InputStream normalInputStream = getResourceAsInputStream( "zip-slip", "zip-normal.zip"); TerminologyCacheManager.unzip( normalInputStream, tempDir.toFile().getAbsolutePath()); Path expectedFilePath = tempDir.resolve("zip-normal").resolve("depth1").resolve("test.txt"); @@ -43,36 +40,26 @@ public void testNormalZip() throws IOException { assertEquals("dummy file content", actualContent); } - @Test - public void testSlipZip() throws IOException { - RuntimeException thrown = Assertions.assertThrows(RuntimeException.class, () -> { - InputStream slipInputStream = getResourceAsInputStream( "terminologyCacheManager", ZIP_SLIP_ZIP); - TerminologyCacheManager.unzip( slipInputStream, tempDir.toFile().getAbsolutePath()); - //Code under test - }); - assertNotNull(thrown); - assertEquals("Entry with an illegal path: ../evil.txt", thrown.getMessage()); - } + public static Stream zipSlipData() { - @Test - public void testSlip2Zip() throws IOException { - RuntimeException thrown = Assertions.assertThrows(RuntimeException.class, () -> { - InputStream slipInputStream = getResourceAsInputStream( "terminologyCacheManager", ZIP_SLIP_2_ZIP); - TerminologyCacheManager.unzip( slipInputStream, tempDir.toFile().getAbsolutePath()); - //Code under test - }); - assertNotNull(thrown); - assertEquals("Entry with an illegal path: child/../../evil.txt", thrown.getMessage()); + return Stream.of( + Arguments.of("zip-slip.zip", "../evil.txt"), + Arguments.of("zip-slip-2.zip", "child/../../evil.txt"), + Arguments.of("zip-slip-peer.zip", "../childpeer/evil.txt"), + Arguments.of("zip-slip-win.zip", "../evil.txt") + ); } - @Test - public void testSlipZipWin() throws IOException { + @ParameterizedTest(name = "{index}: file {0}") + @MethodSource("zipSlipData") + public void testLoadFromClasspathZipSlip(String fileName, String expectedMessage) { RuntimeException thrown = Assertions.assertThrows(RuntimeException.class, () -> { - InputStream slipInputStream = getResourceAsInputStream( "terminologyCacheManager", ZIP_SLIP_WIN_ZIP); + InputStream slipInputStream = getResourceAsInputStream( "zip-slip", fileName); TerminologyCacheManager.unzip( slipInputStream, tempDir.toFile().getAbsolutePath()); //Code under test }); assertNotNull(thrown); - assertEquals("Entry with an illegal path: ../evil.txt", thrown.getMessage()); + Assertions.assertTrue(thrown.getMessage().endsWith(expectedMessage)); } + } diff --git a/org.hl7.fhir.r5/src/test/resources/terminologyCacheManager/zip-normal.zip b/org.hl7.fhir.r5/src/test/resources/zip-slip/zip-normal.zip similarity index 100% rename from org.hl7.fhir.r5/src/test/resources/terminologyCacheManager/zip-normal.zip rename to org.hl7.fhir.r5/src/test/resources/zip-slip/zip-normal.zip diff --git a/org.hl7.fhir.r5/src/test/resources/terminologyCacheManager/zip-slip-2.zip b/org.hl7.fhir.r5/src/test/resources/zip-slip/zip-slip-2.zip similarity index 100% rename from org.hl7.fhir.r5/src/test/resources/terminologyCacheManager/zip-slip-2.zip rename to org.hl7.fhir.r5/src/test/resources/zip-slip/zip-slip-2.zip diff --git a/org.hl7.fhir.r5/src/test/resources/zip-slip/zip-slip-peer.zip b/org.hl7.fhir.r5/src/test/resources/zip-slip/zip-slip-peer.zip new file mode 100755 index 0000000000000000000000000000000000000000..1b1779ab96d56a581dd23fa7007fd4be328af6f5 GIT binary patch literal 157 zcmWIWW@Zs#0D%=%#$k2Q{vLurHVBIXv7VlOaz windowsRootPaths() { + return Stream.of( + Arguments.of((Object)new String[]{"C:"}), + Arguments.of((Object)new String[]{"D:"}), + Arguments.of((Object)new String[]{"C:", "anything"}), + Arguments.of((Object)new String[]{"D:", "anything"}), + Arguments.of((Object)new String[]{"C:/", "anything"}), + Arguments.of((Object)new String[]{"C:/.", "anything"}), + Arguments.of((Object)new String[]{"C:\\"}), + Arguments.of((Object)new String[]{"D:\\"}), + Arguments.of((Object)new String[]{"C:/child/.."}), + Arguments.of((Object)new String[]{"C:/child/..", "anything"}), + Arguments.of((Object)new String[]{"C:/child/../child/.."}), + Arguments.of((Object)new String[]{"C:/child/../child/..", "anything"}), + Arguments.of((Object)new String[]{"C:/child/second/../.."}), + Arguments.of((Object)new String[]{"C:/child/second/../..", "anything"}), + Arguments.of((Object)new String[]{"C:\\child\\.."}), + Arguments.of((Object)new String[]{"C:\\child\\..", "anything"}), + Arguments.of((Object)new String[]{"C:\\child\\..\\child/.."}), + Arguments.of((Object)new String[]{"C:\\child\\..\\child\\..", "anything"}), + Arguments.of((Object)new String[]{"C:\\child\\second\\..\\.."}), + Arguments.of((Object)new String[]{"C:\\child\\second\\..\\..", "anything"}) + ); + } + @ParameterizedTest + @MethodSource("windowsRootPaths") + @EnabledOnOs({OS.WINDOWS}) + public void testPathCantStartWithRootWindows(String[] pathStrings) { + testCantStartWithRoot(pathStrings); + } + + public static Stream macAndLinuxRootPaths() { + return Stream.of( + Arguments.of((Object)new String[]{"/"}), + Arguments.of((Object)new String[]{"/", "anything"}), + Arguments.of((Object)new String[]{"//"}), + Arguments.of((Object)new String[]{"//", "anything"}), + Arguments.of((Object)new String[]{"//child/.."}), + Arguments.of((Object)new String[]{"//child/..", "anything"}), + Arguments.of((Object)new String[]{"//child/../child/.."}), + Arguments.of((Object)new String[]{"//child/../child/..", "anything"}), + Arguments.of((Object)new String[]{"//child/second/../.."}), + Arguments.of((Object)new String[]{"//child/second/../..", "anything"}) + ); + } + @ParameterizedTest + @MethodSource("macAndLinuxRootPaths") + @EnabledOnOs({OS.MAC, OS.LINUX}) + public void testPathCantStartWithRootMacAndLinux(String[] pathStrings) { + testCantStartWithRoot(pathStrings); + } + + private static void testCantStartWithRoot(String[] pathStrings) { + RuntimeException thrown = Assertions.assertThrows(RuntimeException.class, () -> { + Utilities.path(pathStrings); + }); + assertTrue(thrown.getMessage().endsWith(pathStrings[0])); + } + + public static Stream macAndLinuxNonFirstElementStartPaths() { + return Stream.of( + Arguments.of((Object)new String[]{"/root", ".."}), + Arguments.of((Object)new String[]{"/root", "child/../.."}), + Arguments.of((Object)new String[]{"/root", "child", "/../.."}), + Arguments.of((Object)new String[]{"/root", "child", "../.."}), + Arguments.of((Object)new String[]{"/root/a", "../.."}), + Arguments.of((Object)new String[]{"/root/a", "child/../.."}), + Arguments.of((Object)new String[]{"/root/a", "child", "/../../.."}), + Arguments.of((Object)new String[]{"/root/a", "child", "../../.."}) + ); + } + + @ParameterizedTest + @MethodSource("macAndLinuxNonFirstElementStartPaths") + @EnabledOnOs({OS.MAC, OS.LINUX}) + public void testPathMustStartWithFirstElementMacAndLinux(String[] pathStrings) { + testPathMustStartWithFirstElement(pathStrings); + } + + private static void testPathMustStartWithFirstElement(String[] pathStrings) { + RuntimeException thrown = Assertions.assertThrows(RuntimeException.class, () -> { + Utilities.path(pathStrings); + }); + assertTrue(thrown.getMessage().startsWith("Computed path does not start with first element: " + pathStrings[0])); + } + + public static Stream macAndLinuxValidPaths() { + return Stream.of( + Arguments.of((Object) new String[]{"/root"}, "/root"), + Arguments.of( (Object) new String[]{"/root", "child"}, "/root/child"), + Arguments.of((Object) new String[]{"/root", "../root/child"}, "/root/child"), + Arguments.of((Object) new String[]{"/root", "child", "anotherchild"}, "/root/child/anotherchild") + ); + } + + @ParameterizedTest + @MethodSource("macAndLinuxValidPaths") + @EnabledOnOs({OS.MAC, OS.LINUX}) + public void testValidPathsMacAndLinux(String[] pathStrings, String expectedPath) throws IOException { + testValidPath(pathStrings,expectedPath); + } + + public static Stream windowsValidPaths() { + return Stream.of( + Arguments.of((Object) new String[]{"C://root"}, "C:\\\\root"), + Arguments.of( (Object) new String[]{"C://root", "child"}, "C:\\\\root\\child"), + Arguments.of((Object) new String[]{"C://root", "../root/child"}, "C:\\\\root\\child"), + Arguments.of((Object) new String[]{"C://root", "child", "anotherchild"}, "C:\\\\root\\child\\anotherchild"), + Arguments.of((Object) new String[]{"C:\\\\root"}, "C:\\\\root"), + Arguments.of( (Object) new String[]{"C:\\\\root", "child"}, "C:\\\\root\\child"), + Arguments.of((Object) new String[]{"C:\\\\root", "..\\root\\child"}, "C:\\\\root\\child"), + Arguments.of((Object) new String[]{"C:\\\\root", "child", "anotherchild"}, "C:\\\\root\\child\\anotherchild") + ); + } + + @ParameterizedTest + @MethodSource("windowsValidPaths") + @EnabledOnOs({OS.WINDOWS}) + public void testValidPathsWindows(String[] pathStrings, String expectedPath) throws IOException { + testValidPath(pathStrings,expectedPath); + } + + private static void testValidPath(String[] pathsStrings, String expectedPath) throws IOException { + String actualPath = Utilities.path(pathsStrings); + assertEquals(expectedPath, actualPath); + } + + public static Stream nullOrEmptyFirstEntryPaths() { + return Stream.of( + Arguments.of((Object)new String[]{null, "child"}), + Arguments.of((Object)new String[]{null, "child/otherchild"}), + Arguments.of((Object)new String[]{null, "child", "otherchild"}), + Arguments.of((Object)new String[]{"", "child"}), + Arguments.of((Object)new String[]{"", "child/otherchild"}), + Arguments.of((Object)new String[]{"", "child", "otherchild"}), + Arguments.of((Object)new String[]{" ", "child"}), + Arguments.of((Object)new String[]{" ", "child/otherchild"}), + Arguments.of((Object)new String[]{" ", "child", "otherchild"}) + ); + } + + @ParameterizedTest + @MethodSource("nullOrEmptyFirstEntryPaths") + public void testNullOrEmptyFirstPathEntryFails(String[] pathsStrings) { + RuntimeException thrown = Assertions.assertThrows(RuntimeException.class, () -> { + Utilities.path(pathsStrings); + }); + assertEquals("First entry cannot be null or empty",thrown.getMessage()); + } @Test @DisplayName("trimWS tests") @@ -266,5 +422,4 @@ void testRegex() { Assertions.assertFalse("\u0009\n\u000B\u000C\r\u0020\u0085\u00A0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u2028\u2029\u202F\u205F\u3000".matches("^.+$")); } - -} \ No newline at end of file +} diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/IgLoader.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/IgLoader.java index 31ae9d9124..489da45648 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/IgLoader.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/IgLoader.java @@ -6,6 +6,7 @@ import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -374,16 +375,19 @@ private int find(String s, int i, char c) { protected Map readZip(InputStream stream) throws IOException { Map res = new HashMap<>(); ZipInputStream zip = new ZipInputStream(stream); - ZipEntry ze; - while ((ze = zip.getNextEntry()) != null) { - String name = ze.getName(); + ZipEntry zipEntry; + while ((zipEntry = zip.getNextEntry()) != null) { + String entryName = zipEntry.getName(); + if (entryName.contains("..") || Path.of(entryName).isAbsolute()) { + throw new RuntimeException("Entry with an illegal path: " + entryName); + } ByteArrayOutputStream b = new ByteArrayOutputStream(); int n; byte[] buf = new byte[1024]; while ((n = ((InputStream) zip).read(buf, 0, 1024)) > -1) { b.write(buf, 0, n); } - res.put(name, b.toByteArray()); + res.put(entryName, b.toByteArray()); zip.closeEntry(); } zip.close(); diff --git a/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/conversion/tests/UtilitiesXTests.java b/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/conversion/tests/UtilitiesXTests.java index 6d6ff3d4e8..30b72695b2 100644 --- a/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/conversion/tests/UtilitiesXTests.java +++ b/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/conversion/tests/UtilitiesXTests.java @@ -35,11 +35,13 @@ WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWIS import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; +import java.nio.file.Path; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import javax.annotation.Nonnull; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; @@ -478,7 +480,7 @@ private static String compareText(String s1, String s2) { public static boolean findTestResource(String... paths) throws IOException { if (new File("../../fhir-test-cases").exists() && isTryToLoadFromFileSystem()) { - String n = Utilities.path(System.getProperty("user.dir"), "..", "..", "fhir-test-cases", Utilities.path(paths)); + String n = Utilities.path(getUserDirFhirTestCases(), Utilities.path(paths)); return new File(n).exists(); } else { String classpath = ("/org/hl7/fhir/testcases/"+ Utilities.pathURL(paths)); @@ -498,7 +500,7 @@ private static boolean isTryToLoadFromFileSystem() { public static String loadTestResource(String... paths) throws IOException { if (new File("../../fhir-test-cases").exists() && isTryToLoadFromFileSystem()) { - String n = Utilities.path(System.getProperty("user.dir"), "..", "..", "fhir-test-cases", Utilities.path(paths)); + String n = Utilities.path(getUserDirFhirTestCases(), Utilities.path(paths)); // ok, we'll resolve this locally return TextFile.fileToString(new File(n)); } else { @@ -515,9 +517,14 @@ public static String loadTestResource(String... paths) throws IOException { } } + @Nonnull + private static String getUserDirFhirTestCases() { + return Path.of(System.getProperty("user.dir"), "..", "..", "fhir-test-cases").normalize().toString(); + } + public static InputStream loadTestResourceStream(String... paths) throws IOException { if (new File("../../fhir-test-cases").exists() && isTryToLoadFromFileSystem()) { - String n = Utilities.path(System.getProperty("user.dir"), "..", "..", "fhir-test-cases", Utilities.path(paths)); + String n = Utilities.path(getUserDirFhirTestCases(), Utilities.path(paths)); return new FileInputStream(n); } else { String classpath = ("/org/hl7/fhir/testcases/"+ Utilities.pathURL(paths)); @@ -565,4 +572,4 @@ public static String tempFolder(String name) throws IOException { return path; } } -} \ No newline at end of file +} diff --git a/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/validation/IgLoaderTests.java b/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/validation/IgLoaderTests.java index f01ee18d96..abb02d6a1c 100644 --- a/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/validation/IgLoaderTests.java +++ b/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/validation/IgLoaderTests.java @@ -4,6 +4,7 @@ import org.hl7.fhir.r5.context.SimpleWorkerContext; import org.hl7.fhir.r5.model.ImplementationGuide; import org.hl7.fhir.utilities.npm.FilesystemPackageCacheManager; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; @@ -14,15 +15,13 @@ import org.mockito.junit.jupiter.MockitoExtension; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Paths; import java.util.*; import java.util.regex.Pattern; import java.util.stream.Stream; -import static org.junit.Assert.assertEquals; -import static org.junit.jupiter.api.Assertions.assertLinesMatch; -import static org.junit.jupiter.api.Assertions.assertThrows; - +import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) @@ -97,4 +96,43 @@ public void testFailIfInvalidFHIRVersion() throws IOException { assertLinesMatch(Arrays.asList(".*Unsupported FHIR Version.*"), Arrays.asList(exception.getMessage())); } + + public static Stream zipSlipData() { + + return Stream.of( + Arguments.of("/zip-slip/zip-slip.zip", "Entry with an illegal path: ../evil.txt"), + Arguments.of("/zip-slip/zip-slip-2.zip", "Entry with an illegal path: child/../../evil.txt"), + Arguments.of("/zip-slip/zip-slip-peer.zip", "Entry with an illegal path: ../childpeer/evil.txt"), + Arguments.of("/zip-slip/zip-slip-win.zip", "Entry with an illegal path: ../evil.txt") + ); + } + + @ParameterizedTest(name = "{index}: file {0}") + @MethodSource("zipSlipData") + public void testReadZipSlip(String classPath, String expectedMessage) { + RuntimeException thrown = Assertions.assertThrows(RuntimeException.class, () -> { + IgLoader igLoader = Mockito.spy(new IgLoader( + filesystemPackageCacheManager, + simpleWorkerContext, + "4.0.1" + )); + igLoader.readZip(IgLoaderTests.class.getResourceAsStream((classPath))); + }); + assertNotNull(thrown); + Assertions.assertEquals(expectedMessage, thrown.getMessage()); + } + + @Test + public void testReadZip() throws IOException { + IgLoader igLoader = Mockito.spy(new IgLoader( + filesystemPackageCacheManager, + simpleWorkerContext, + "4.0.1" + )); + Map map = igLoader.readZip(IgLoaderTests.class.getResourceAsStream("/zip-slip/zip-normal.zip")); + final String testPath = "zip-normal/depth1/test.txt"; + assertTrue(map.containsKey(testPath)); + String testFileContent = new String(map.get(testPath), StandardCharsets.UTF_8); + Assertions.assertEquals("dummy file content", testFileContent); + } } diff --git a/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/validation/ScannerTest.java b/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/validation/ScannerTest.java index aa427bf849..19c2430908 100644 --- a/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/validation/ScannerTest.java +++ b/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/validation/ScannerTest.java @@ -5,10 +5,14 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -19,7 +23,7 @@ public class ScannerTest implements ResourceLoaderTests { public static final String ZIP_NORMAL_ZIP = "zip-normal.zip"; public static final String ZIP_SLIP_ZIP = "zip-slip.zip"; public static final String ZIP_SLIP_2_ZIP = "zip-slip-2.zip"; - + public static final String ZIP_SLIP_PEER_ZIP = "zip-slip-peer.zip"; public static final String ZIP_SLIP_WIN_ZIP = "zip-slip-win.zip"; Path tempDir; @@ -28,6 +32,8 @@ public class ScannerTest implements ResourceLoaderTests { Path zipSlip2Path; + Path zipSlipPeerPath; + Path zipSlipWinPath; @BeforeAll @@ -37,12 +43,14 @@ public void beforeAll() throws IOException { zipNormalPath = tempDir.resolve(ZIP_NORMAL_ZIP); zipSlipPath = tempDir.resolve(ZIP_SLIP_ZIP); zipSlip2Path = tempDir.resolve(ZIP_SLIP_2_ZIP); + zipSlipPeerPath = tempDir.resolve(ZIP_SLIP_PEER_ZIP); zipSlipWinPath = tempDir.resolve(ZIP_SLIP_WIN_ZIP); - copyResourceToFile(zipNormalPath, "scanner", ZIP_NORMAL_ZIP); - copyResourceToFile(zipSlipPath, "scanner", ZIP_SLIP_ZIP); - copyResourceToFile(zipSlip2Path, "scanner", ZIP_SLIP_2_ZIP); - copyResourceToFile(zipSlipWinPath, "scanner", ZIP_SLIP_WIN_ZIP); + copyResourceToFile(zipNormalPath, "zip-slip", ZIP_NORMAL_ZIP); + copyResourceToFile(zipSlipPath, "zip-slip", ZIP_SLIP_ZIP); + copyResourceToFile(zipSlip2Path, "zip-slip", ZIP_SLIP_2_ZIP); + copyResourceToFile(zipSlipPeerPath, "zip-slip", ZIP_SLIP_PEER_ZIP); + copyResourceToFile(zipSlipWinPath, "zip-slip", ZIP_SLIP_WIN_ZIP); } @Test @@ -55,36 +63,25 @@ public void testNormalZip() throws IOException { assertEquals("dummy file content", actualContent); } - @Test - public void testSlipZip() throws IOException { - RuntimeException thrown = Assertions.assertThrows(RuntimeException.class, () -> { - Scanner scanner = new Scanner(null,null,null,null); - scanner.unzip(zipSlipPath.toFile().getAbsolutePath(), tempDir.toFile().getAbsolutePath()); - //Code under test - }); - assertNotNull(thrown); - assertEquals("Entry with an illegal path: ../evil.txt", thrown.getMessage()); - } + public Stream zipSlipData() { - @Test - public void testSlipZip2() throws IOException { - RuntimeException thrown = Assertions.assertThrows(RuntimeException.class, () -> { - Scanner scanner = new Scanner(null,null,null,null); - scanner.unzip(zipSlip2Path.toFile().getAbsolutePath(), tempDir.toFile().getAbsolutePath()); - //Code under test - }); - assertNotNull(thrown); - assertEquals("Entry with an illegal path: child/../../evil.txt", thrown.getMessage()); + return Stream.of( + Arguments.of(zipSlipPath, "Entry with an illegal path: ../evil.txt"), + Arguments.of(zipSlip2Path, "Entry with an illegal path: child/../../evil.txt"), + Arguments.of(zipSlipPeerPath, "Entry with an illegal path: ../childpeer/evil.txt"), + Arguments.of(zipSlipWinPath, "Entry with an illegal path: ../evil.txt") + ); } - @Test - public void testSlipZipWin() throws IOException { + @ParameterizedTest(name = "{index}: file {0}") + @MethodSource("zipSlipData") + public void testUnzipZipSlip(Path path, String expectedMessage) { RuntimeException thrown = Assertions.assertThrows(RuntimeException.class, () -> { Scanner scanner = new Scanner(null,null,null,null); - scanner.unzip(zipSlipWinPath.toFile().getAbsolutePath(), tempDir.toFile().getAbsolutePath()); - //Code under test + scanner.unzip(path.toFile().getAbsolutePath(), tempDir.toFile().getAbsolutePath()); }); assertNotNull(thrown); - assertEquals("Entry with an illegal path: ../evil.txt", thrown.getMessage()); + assertEquals(expectedMessage, thrown.getMessage()); } + } diff --git a/org.hl7.fhir.validation/src/test/resources/scanner/zip-normal.zip b/org.hl7.fhir.validation/src/test/resources/zip-slip/zip-normal.zip similarity index 100% rename from org.hl7.fhir.validation/src/test/resources/scanner/zip-normal.zip rename to org.hl7.fhir.validation/src/test/resources/zip-slip/zip-normal.zip diff --git a/org.hl7.fhir.validation/src/test/resources/scanner/zip-slip-2.zip b/org.hl7.fhir.validation/src/test/resources/zip-slip/zip-slip-2.zip similarity index 100% rename from org.hl7.fhir.validation/src/test/resources/scanner/zip-slip-2.zip rename to org.hl7.fhir.validation/src/test/resources/zip-slip/zip-slip-2.zip diff --git a/org.hl7.fhir.validation/src/test/resources/zip-slip/zip-slip-peer.zip b/org.hl7.fhir.validation/src/test/resources/zip-slip/zip-slip-peer.zip new file mode 100755 index 0000000000000000000000000000000000000000..1b1779ab96d56a581dd23fa7007fd4be328af6f5 GIT binary patch literal 157 zcmWIWW@Zs#0D%=%#$k2Q{vLurHVBIXv7VlOaz