Additional zip-slip tests (#1162)

* Additional zip-slip tests

* Fix windows path test
This commit is contained in:
dotasek 2023-03-09 14:30:53 -05:00 committed by GitHub
parent f49eee623b
commit 909f7e64fe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 576 additions and 732 deletions

View File

@ -471,9 +471,13 @@ public class SimpleWorkerContext extends BaseWorkerContext implements IWorkerCon
private void loadFromStream(InputStream stream, IContextResourceLoader loader) throws IOException, FHIRException { private void loadFromStream(InputStream stream, IContextResourceLoader loader) throws IOException, FHIRException {
ZipInputStream zip = new ZipInputStream(stream); ZipInputStream zip = new ZipInputStream(stream);
ZipEntry ze; ZipEntry zipEntry;
while ((ze = zip.getNextEntry()) != null) { while ((zipEntry = zip.getNextEntry()) != null) {
loadDefinitionItem(ze.getName(), zip, loader, null, 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.closeEntry();
} }
zip.close(); zip.close();

View File

@ -37,6 +37,8 @@ import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator.Row;
import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator.TableModel; import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator.TableModel;
import org.hl7.fhir.utilities.xhtml.XhtmlNode; import org.hl7.fhir.utilities.xhtml.XhtmlNode;
import javax.annotation.Nonnull;
public class QuestionnaireRenderer extends TerminologyRenderer { public class QuestionnaireRenderer extends TerminologyRenderer {
public static final String EXT_QUESTIONNAIRE_ITEM_TYPE_ORIGINAL = "http://hl7.org/fhir/tools/StructureDefinition/original-item-type"; public static final String EXT_QUESTIONNAIRE_ITEM_TYPE_ORIGINAL = "http://hl7.org/fhir/tools/StructureDefinition/original-item-type";
@ -251,28 +253,28 @@ public class QuestionnaireRenderer extends TerminologyRenderer {
Cell flags = gen.new Cell(); Cell flags = gen.new Cell();
r.getCells().add(flags); r.getCells().add(flags);
if (i.getReadOnly()) { 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")) { 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")) { 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")) { 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")) { 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")) { if (i.hasExtension("http://hl7.org/fhir/StructureDefinition/questionnaire-choiceOrientation")) {
String code = ToolingExtensions.readStringExtension(i, "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")) { if (i.hasExtension("http://hl7.org/fhir/StructureDefinition/questionnaire-displayCategory")) {
CodeableConcept cc = i.getExtensionByUrl("http://hl7.org/fhir/StructureDefinition/questionnaire-displayCategory").getValueCodeableConcept(); CodeableConcept cc = i.getExtensionByUrl("http://hl7.org/fhir/StructureDefinition/questionnaire-displayCategory").getValueCodeableConcept();
String code = cc.getCode("http://hl7.org/fhir/questionnaire-display-category"); 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(); Cell defn = gen.new Cell();
@ -687,26 +689,26 @@ public class QuestionnaireRenderer extends TerminologyRenderer {
if (ToolingExtensions.readBoolExtension(i, "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-isSubject")) { if (ToolingExtensions.readBoolExtension(i, "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-isSubject")) {
hasFlag = true; 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")) { if (ToolingExtensions.readBoolExtension(i, "http://hl7.org/fhir/StructureDefinition/questionnaire-hidden")) {
hasFlag = true; 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"); d.style("background-color: #eeeeee");
} }
if (ToolingExtensions.readBoolExtension(i, "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-optionalDisplay")) { if (ToolingExtensions.readBoolExtension(i, "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-optionalDisplay")) {
hasFlag = true; 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")) { if (i.hasExtension("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-observationLinkPeriod")) {
hasFlag = true; 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")) { if (i.hasExtension("http://hl7.org/fhir/StructureDefinition/questionnaire-displayCategory")) {
CodeableConcept cc = i.getExtensionByUrl("http://hl7.org/fhir/StructureDefinition/questionnaire-displayCategory").getValueCodeableConcept(); CodeableConcept cc = i.getExtensionByUrl("http://hl7.org/fhir/StructureDefinition/questionnaire-displayCategory").getValueCodeableConcept();
String code = cc.getCode("http://hl7.org/fhir/questionnaire-display-category"); String code = cc.getCode("http://hl7.org/fhir/questionnaire-display-category");
hasFlag = true; 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()) { if (i.hasMaxLength()) {
@ -788,6 +790,13 @@ public class QuestionnaireRenderer extends TerminologyRenderer {
return hasExt; 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) { private void item(XhtmlNode ul, String name, String value, String valueLink) {
if (!Utilities.noString(value)) { if (!Utilities.noString(value)) {
ul.li().style("font-size: 10px").ah(valueLink).tx(name+": "+value); ul.li().style("font-size: 10px").ah(valueLink).tx(name+": "+value);
@ -862,7 +871,7 @@ public class QuestionnaireRenderer extends TerminologyRenderer {
boolean ext = false; boolean ext = false;
XhtmlNode td = tbl.tr().td("structure").colspan("2").span(null, null).attribute("class", "self-link-parent"); XhtmlNode td = tbl.tr().td("structure").colspan("2").span(null, null).attribute("class", "self-link-parent");
td.an(q.getId()); td.an(q.getId());
td.img(Utilities.path(context.getLocalPrefix(), "icon_q_root.gif"), "icon"); td.img(getImgPath("icon_q_root.gif"), "icon");
td.tx(" Questionnaire "); td.tx(" Questionnaire ");
td.b().tx(q.getId()); td.b().tx(q.getId());
@ -915,10 +924,10 @@ public class QuestionnaireRenderer extends TerminologyRenderer {
XhtmlNode td = tbl.tr().td("structure").colspan("2").span(null, null).attribute("class", "self-link-parent"); XhtmlNode td = tbl.tr().td("structure").colspan("2").span(null, null).attribute("class", "self-link-parent");
td.an("item."+qi.getLinkId()); td.an("item."+qi.getLinkId());
for (QuestionnaireItemComponent p : parents) { for (QuestionnaireItemComponent p : parents) {
td.ah("#item."+p.getLinkId()).img(Utilities.path(context.getLocalPrefix(), "icon_q_item.png"), "icon"); td.ah("#item."+p.getLinkId()).img(getImgPath("icon_q_item.png"), "icon");
td.tx(" > "); td.tx(" > ");
} }
td.img(Utilities.path(context.getLocalPrefix(), "icon_q_item.png"), "icon"); td.img(getImgPath("icon_q_item.png"), "icon");
td.tx(" Item "); td.tx(" Item ");
td.b().tx(qi.getLinkId()); td.b().tx(qi.getLinkId());
@ -1103,4 +1112,4 @@ public class QuestionnaireRenderer extends TerminologyRenderer {
} }
} }
} }

View File

@ -97,17 +97,17 @@ public class TerminologyCacheManager {
public static void unzip(InputStream is, String targetDir) throws IOException { public static void unzip(InputStream is, String targetDir) throws IOException {
try (ZipInputStream zipIn = new ZipInputStream(is)) { try (ZipInputStream zipIn = new ZipInputStream(is)) {
for (ZipEntry ze; (ze = zipIn.getNextEntry()) != null; ) { for (ZipEntry ze; (ze = zipIn.getNextEntry()) != null; ) {
String path = Path.of(Utilities.path(targetDir, ze.getName())).normalize().toFile().getAbsolutePath(); Path path = Path.of(Utilities.path(targetDir, ze.getName())).normalize();
String pathString = path.toFile().getAbsolutePath();
if (!path.startsWith(targetDir)) { if (!path.startsWith(Path.of(targetDir).normalize())) {
// see: https://snyk.io/research/zip-slip-vulnerability // see: https://snyk.io/research/zip-slip-vulnerability
throw new RuntimeException("Entry with an illegal path: " + ze.getName()); throw new RuntimeException("Entry with an illegal path: " + ze.getName());
} }
if (ze.isDirectory()) { if (ze.isDirectory()) {
Utilities.createDirectory(path); Utilities.createDirectory(pathString);
} else { } else {
Utilities.createDirectory(Utilities.getDirectoryForFile(path)); Utilities.createDirectory(Utilities.getDirectoryForFile(pathString));
TextFile.streamToFileNoClose(zipIn, path); TextFile.streamToFileNoClose(zipIn, pathString);
} }
} }
} }

View File

@ -1,33 +1,33 @@
package org.hl7.fhir.r4b.test.utils; package org.hl7.fhir.r4b.test.utils;
/* /*
Copyright (c) 2011+, HL7, Inc. Copyright (c) 2011+, HL7, Inc.
All rights reserved. All rights reserved.
Redistribution and use in source and binary forms, with or without modification, Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met: are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this * Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer. list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, * Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution. and/or other materials provided with the distribution.
* Neither the name of HL7 nor the names of its contributors may be used to * 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 endorse or promote products derived from this software without specific
prior written permission. prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 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 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 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, IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 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, PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 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 ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE. POSSIBILITY OF SUCH DAMAGE.
*/ */
@ -146,61 +146,6 @@ public class ToolsHelper {
// } // }
} }
private Map<String, byte[]> getDefinitions(String definitions) throws IOException, FHIRException {
Map<String, byte[]> results = new HashMap<String, byte[]>();
readDefinitions(results, loadDefinitions(definitions));
return results;
}
private void readDefinitions(Map<String, byte[]> 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 { protected XmlPullParser loadXml(InputStream stream) throws XmlPullParserException, IOException {
BufferedInputStream input = new BufferedInputStream(stream); BufferedInputStream input = new BufferedInputStream(stream);
XmlPullParserFactory factory = XmlPullParserFactory.newInstance(System.getProperty(XmlPullParserFactory.PROPERTY_NAME), null); XmlPullParserFactory factory = XmlPullParserFactory.newInstance(System.getProperty(XmlPullParserFactory.PROPERTY_NAME), null);
@ -438,4 +383,4 @@ public class ToolsHelper {
} }
} }

View File

@ -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<SearchParameterNode> {
@Override
public int compare(SearchParameterNode o1, SearchParameterNode o2) {
return o1.name.compareTo(o2.name);
}
}
public class SearchParameterParamNodeSorter implements Comparator<SearchParameterParamNode> {
@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<SearchParameterParamNode> params = new ArrayList<>();
public SearchParameterNode(String name) {
this.name = name;
}
}
public class StructureDefinitionNodeComparer implements Comparator<StructureDefinitionNode> {
@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<StructureDefinitionNode> 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<String, StructureDefinition> 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<String> 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<String, StructureDefinition> 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<String, byte[]> loadZip(InputStream stream) throws IOException {
Map<String, byte[]> res = new HashMap<String, byte[]>();
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<String, byte[]> files = loadZip(new FileInputStream(Utilities.path(dst, "examples-json.zip")));
for (Entry<String, byte[]> 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<String, byte[]> files = loadZip(new FileInputStream(Utilities.path(dst, "examples.zip")));
for (Entry<String, byte[]> 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<SearchParameter> 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<String, SearchParameterNode> 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<SearchParameterParamNode> sortP(List<SearchParameterParamNode> params) {
List<SearchParameterParamNode> res = new ArrayList<>();
res.addAll(params);
Collections.sort(res, new SearchParameterParamNodeSorter());
return res;
}
private List<SearchParameterNode> sort(Collection<SearchParameterNode> values) {
List<SearchParameterNode> 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<String, StructureDefinition> 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<StructureDefinitionNode> 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<StructureDefinitionNode> list) {
Collections.sort(list, new StructureDefinitionNodeComparer());
}
private void analyse(StructureDefinitionNode node, Map<String, StructureDefinition> 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<String, StructureDefinition> map, List<StructureDefinition> 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);
}
}

View File

@ -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<Arguments> 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);
}
}

View File

@ -5,36 +5,33 @@ import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance; 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.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; 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.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotNull;
@TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class TerminologyCacheManagerTests implements ResourceLoaderTests { 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; Path tempDir;
@BeforeAll @BeforeAll
public void beforeAll() throws IOException { public void beforeAll() throws IOException {
tempDir = Files.createTempDirectory("terminology-cache-manager"); tempDir = Files.createTempDirectory("terminology-cache-manager");
tempDir.resolve("child").toFile().mkdir(); tempDir.resolve("child").toFile().mkdir();
getResourceAsInputStream("terminologyCacheManager", ZIP_SLIP_ZIP);
} }
@Test @Test
public void testNormalZip() throws IOException { 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()); TerminologyCacheManager.unzip( normalInputStream, tempDir.toFile().getAbsolutePath());
Path expectedFilePath = tempDir.resolve("zip-normal").resolve("depth1").resolve("test.txt"); Path expectedFilePath = tempDir.resolve("zip-normal").resolve("depth1").resolve("test.txt");
@ -42,36 +39,25 @@ public class TerminologyCacheManagerTests implements ResourceLoaderTests {
assertEquals("dummy file content", actualContent); assertEquals("dummy file content", actualContent);
} }
@Test public static Stream<Arguments> zipSlipData() {
public void testSlipZip() throws IOException {
RuntimeException thrown = Assertions.assertThrows(RuntimeException.class, () -> { return Stream.of(
InputStream slipInputStream = getResourceAsInputStream( "terminologyCacheManager", ZIP_SLIP_ZIP); Arguments.of("zip-slip.zip", "../evil.txt"),
TerminologyCacheManager.unzip( slipInputStream, tempDir.toFile().getAbsolutePath()); Arguments.of("zip-slip-2.zip", "child/../../evil.txt"),
//Code under test Arguments.of("zip-slip-peer.zip", "../childpeer/evil.txt"),
}); Arguments.of("zip-slip-win.zip", "../evil.txt")
assertNotNull(thrown); );
assertEquals("Entry with an illegal path: ../evil.txt", thrown.getMessage());
} }
@Test @ParameterizedTest(name = "{index}: file {0}")
public void testSlip2Zip() throws IOException { @MethodSource("zipSlipData")
public void testLoadFromClasspathZipSlip(String fileName, String expectedMessage) {
RuntimeException thrown = Assertions.assertThrows(RuntimeException.class, () -> { RuntimeException thrown = Assertions.assertThrows(RuntimeException.class, () -> {
InputStream slipInputStream = getResourceAsInputStream( "terminologyCacheManager", ZIP_SLIP_2_ZIP); InputStream slipInputStream = getResourceAsInputStream("zip-slip", fileName);
TerminologyCacheManager.unzip( slipInputStream, tempDir.toFile().getAbsolutePath()); TerminologyCacheManager.unzip( slipInputStream, tempDir.toFile().getAbsolutePath());
//Code under test //Code under test
}); });
assertNotNull(thrown); assertNotNull(thrown);
assertEquals("Entry with an illegal path: child/../../evil.txt", thrown.getMessage()); Assertions.assertTrue(thrown.getMessage().endsWith(expectedMessage));
}
@Test
public void testSlipZipWin() throws IOException {
RuntimeException thrown = Assertions.assertThrows(RuntimeException.class, () -> {
InputStream slipInputStream = getResourceAsInputStream( "terminologyCacheManager", ZIP_SLIP_WIN_ZIP);
TerminologyCacheManager.unzip( slipInputStream, tempDir.toFile().getAbsolutePath());
//Code under test
});
assertNotNull(thrown);
assertEquals("Entry with an illegal path: ../evil.txt", thrown.getMessage());
} }
} }

View File

@ -520,9 +520,13 @@ public class SimpleWorkerContext extends BaseWorkerContext implements IWorkerCon
private void loadFromStream(InputStream stream, IContextResourceLoader loader) throws IOException, FHIRException { private void loadFromStream(InputStream stream, IContextResourceLoader loader) throws IOException, FHIRException {
ZipInputStream zip = new ZipInputStream(stream); ZipInputStream zip = new ZipInputStream(stream);
ZipEntry ze; ZipEntry zipEntry;
while ((ze = zip.getNextEntry()) != null) { while ((zipEntry = zip.getNextEntry()) != null) {
loadDefinitionItem(ze.getName(), zip, loader, null, 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.closeEntry();
} }
zip.close(); zip.close();

View File

@ -37,6 +37,8 @@ import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator.TableModel;
import org.hl7.fhir.utilities.xhtml.NodeType; import org.hl7.fhir.utilities.xhtml.NodeType;
import org.hl7.fhir.utilities.xhtml.XhtmlNode; import org.hl7.fhir.utilities.xhtml.XhtmlNode;
import javax.annotation.Nonnull;
public class QuestionnaireRenderer extends TerminologyRenderer { public class QuestionnaireRenderer extends TerminologyRenderer {
public static final String EXT_QUESTIONNAIRE_ITEM_TYPE_ORIGINAL = "http://hl7.org/fhir/tools/StructureDefinition/original-item-type"; public static final String EXT_QUESTIONNAIRE_ITEM_TYPE_ORIGINAL = "http://hl7.org/fhir/tools/StructureDefinition/original-item-type";
@ -255,28 +257,28 @@ public class QuestionnaireRenderer extends TerminologyRenderer {
Cell flags = gen.new Cell(); Cell flags = gen.new Cell();
r.getCells().add(flags); r.getCells().add(flags);
if (i.getReadOnly()) { 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")) { 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)) { 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)) { 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")) { 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)) { if (i.hasExtension(ToolingExtensions.EXT_Q_CHOICE_ORIENT)) {
String code = ToolingExtensions.readStringExtension(i, 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)) { if (i.hasExtension(ToolingExtensions.EXT_Q_DISPLAY_CAT)) {
CodeableConcept cc = i.getExtensionByUrl(ToolingExtensions.EXT_Q_DISPLAY_CAT).getValueCodeableConcept(); CodeableConcept cc = i.getExtensionByUrl(ToolingExtensions.EXT_Q_DISPLAY_CAT).getValueCodeableConcept();
String code = cc.getCode("http://hl7.org/fhir/questionnaire-display-category"); 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(); Cell defn = gen.new Cell();
@ -690,26 +692,26 @@ public class QuestionnaireRenderer extends TerminologyRenderer {
if (ToolingExtensions.readBoolExtension(i, "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-isSubject")) { if (ToolingExtensions.readBoolExtension(i, "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-isSubject")) {
hasFlag = true; 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)) { if (ToolingExtensions.readBoolExtension(i, ToolingExtensions.EXT_Q_HIDDEN)) {
hasFlag = true; 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"); d.style("background-color: #eeeeee");
} }
if (ToolingExtensions.readBoolExtension(i, ToolingExtensions.EXT_Q_OTP_DISP)) { if (ToolingExtensions.readBoolExtension(i, ToolingExtensions.EXT_Q_OTP_DISP)) {
hasFlag = true; 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")) { if (i.hasExtension("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-observationLinkPeriod")) {
hasFlag = true; 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)) { if (i.hasExtension(ToolingExtensions.EXT_Q_DISPLAY_CAT)) {
CodeableConcept cc = i.getExtensionByUrl(ToolingExtensions.EXT_Q_DISPLAY_CAT).getValueCodeableConcept(); CodeableConcept cc = i.getExtensionByUrl(ToolingExtensions.EXT_Q_DISPLAY_CAT).getValueCodeableConcept();
String code = cc.getCode("http://hl7.org/fhir/questionnaire-display-category"); String code = cc.getCode("http://hl7.org/fhir/questionnaire-display-category");
hasFlag = true; 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()) { if (i.hasMaxLength()) {
@ -791,6 +793,13 @@ public class QuestionnaireRenderer extends TerminologyRenderer {
return hasExt; 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) { private void item(XhtmlNode ul, String name, String value, String valueLink) {
if (!Utilities.noString(value)) { if (!Utilities.noString(value)) {
ul.li().style("font-size: 10px").ah(valueLink).tx(name+": "+value); ul.li().style("font-size: 10px").ah(valueLink).tx(name+": "+value);
@ -865,7 +874,7 @@ public class QuestionnaireRenderer extends TerminologyRenderer {
boolean ext = false; boolean ext = false;
XhtmlNode td = tbl.tr().td("structure").colspan("2").span(null, null).attribute("class", "self-link-parent"); XhtmlNode td = tbl.tr().td("structure").colspan("2").span(null, null).attribute("class", "self-link-parent");
td.an(q.getId()); td.an(q.getId());
td.img(Utilities.path(context.getLocalPrefix(), "icon_q_root.gif"), "icon"); td.img(getImgPath("icon_q_root.gif"), "icon");
td.tx(" Questionnaire "); td.tx(" Questionnaire ");
td.b().tx(q.getId()); td.b().tx(q.getId());
@ -918,10 +927,10 @@ public class QuestionnaireRenderer extends TerminologyRenderer {
XhtmlNode td = tbl.tr().td("structure").colspan("2").span(null, null).attribute("class", "self-link-parent"); XhtmlNode td = tbl.tr().td("structure").colspan("2").span(null, null).attribute("class", "self-link-parent");
td.an("item."+qi.getLinkId()); td.an("item."+qi.getLinkId());
for (QuestionnaireItemComponent p : parents) { for (QuestionnaireItemComponent p : parents) {
td.ah("#item."+p.getLinkId()).img(Utilities.path(context.getLocalPrefix(), "icon_q_item.png"), "icon"); td.ah("#item."+p.getLinkId()).img(getImgPath("icon_q_item.png"), "icon");
td.tx(" > "); td.tx(" > ");
} }
td.img(Utilities.path(context.getLocalPrefix(), "icon_q_item.png"), "icon"); td.img(getImgPath("icon_q_item.png"), "icon");
td.tx(" Item "); td.tx(" Item ");
td.b().tx(qi.getLinkId()); td.b().tx(qi.getLinkId());
@ -1106,4 +1115,4 @@ public class QuestionnaireRenderer extends TerminologyRenderer {
} }
} }
} }

View File

@ -97,16 +97,17 @@ public class TerminologyCacheManager {
public static void unzip(InputStream is, String targetDir) throws IOException { public static void unzip(InputStream is, String targetDir) throws IOException {
try (ZipInputStream zipIn = new ZipInputStream(is)) { try (ZipInputStream zipIn = new ZipInputStream(is)) {
for (ZipEntry ze; (ze = zipIn.getNextEntry()) != null; ) { for (ZipEntry ze; (ze = zipIn.getNextEntry()) != null; ) {
String path = Path.of(Utilities.path(targetDir, ze.getName())).normalize().toFile().getAbsolutePath(); Path path = Path.of(Utilities.path(targetDir, ze.getName())).normalize();
if (!path.startsWith(targetDir)) { String pathString = path.toFile().getAbsolutePath();
if (!path.startsWith(Path.of(targetDir).normalize())) {
// see: https://snyk.io/research/zip-slip-vulnerability // see: https://snyk.io/research/zip-slip-vulnerability
throw new RuntimeException("Entry with an illegal path: " + ze.getName()); throw new RuntimeException("Entry with an illegal path: " + ze.getName());
} }
if (ze.isDirectory()) { if (ze.isDirectory()) {
Utilities.createDirectory(path); Utilities.createDirectory(pathString);
} else { } else {
Utilities.createDirectory(Utilities.getDirectoryForFile(path)); Utilities.createDirectory(Utilities.getDirectoryForFile(pathString));
TextFile.streamToFileNoClose(zipIn, path); TextFile.streamToFileNoClose(zipIn, pathString);
} }
} }
} }

View File

@ -1,33 +1,33 @@
package org.hl7.fhir.r5.test.utils; package org.hl7.fhir.r5.test.utils;
/* /*
Copyright (c) 2011+, HL7, Inc. Copyright (c) 2011+, HL7, Inc.
All rights reserved. All rights reserved.
Redistribution and use in source and binary forms, with or without modification, Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met: are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this * Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer. list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, * Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution. and/or other materials provided with the distribution.
* Neither the name of HL7 nor the names of its contributors may be used to * 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 endorse or promote products derived from this software without specific
prior written permission. prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 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 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 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, IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 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, PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 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 ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE. POSSIBILITY OF SUCH DAMAGE.
*/ */
@ -146,61 +146,6 @@ public class ToolsHelper {
// } // }
} }
private Map<String, byte[]> getDefinitions(String definitions) throws IOException, FHIRException {
Map<String, byte[]> results = new HashMap<String, byte[]>();
readDefinitions(results, loadDefinitions(definitions));
return results;
}
private void readDefinitions(Map<String, byte[]> 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 { protected XmlPullParser loadXml(InputStream stream) throws XmlPullParserException, IOException {
BufferedInputStream input = new BufferedInputStream(stream); BufferedInputStream input = new BufferedInputStream(stream);
XmlPullParserFactory factory = XmlPullParserFactory.newInstance(System.getProperty(XmlPullParserFactory.PROPERTY_NAME), null); XmlPullParserFactory factory = XmlPullParserFactory.newInstance(System.getProperty(XmlPullParserFactory.PROPERTY_NAME), null);
@ -438,4 +383,4 @@ public class ToolsHelper {
} }
} }

View File

@ -1,6 +1,6 @@
package org.hl7.fhir.r5.context; 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.any;
import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.argThat;
@ -9,9 +9,9 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays; import java.nio.charset.StandardCharsets;
import java.util.HashMap; import java.util.*;
import java.util.Map; import java.util.stream.Stream;
import org.hl7.fhir.r5.model.CapabilityStatement; import org.hl7.fhir.r5.model.CapabilityStatement;
import org.hl7.fhir.r5.model.CodeableConcept; import org.hl7.fhir.r5.model.CodeableConcept;
@ -27,9 +27,13 @@ import org.hl7.fhir.r5.terminologies.ValueSetExpanderSimple;
import org.hl7.fhir.r5.utils.validation.ValidationContextCarrier; import org.hl7.fhir.r5.utils.validation.ValidationContextCarrier;
import org.hl7.fhir.utilities.ToolingClientLogger; import org.hl7.fhir.utilities.ToolingClientLogger;
import org.hl7.fhir.utilities.validation.ValidationOptions; import org.hl7.fhir.utilities.validation.ValidationOptions;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; 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.ArgumentMatcher;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.Mockito; import org.mockito.Mockito;
@ -423,4 +427,31 @@ public class SimpleWorkerContextTests {
Mockito.verify(context).setTxCaps(terminologyCapabilities); Mockito.verify(context).setTxCaps(terminologyCapabilities);
} }
public static Stream<Arguments> 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);
}
} }

View File

@ -5,37 +5,34 @@ import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance; 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.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; 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.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotNull;
@TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class TerminologyCacheManagerTests implements ResourceLoaderTests { 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; Path tempDir;
@BeforeAll @BeforeAll
public void beforeAll() throws IOException { public void beforeAll() throws IOException {
tempDir = Files.createTempDirectory("terminology-cache-manager"); tempDir = Files.createTempDirectory("terminology-cache-manager");
tempDir.resolve("child").toFile().mkdir(); tempDir.resolve("child").toFile().mkdir();
getResourceAsInputStream("terminologyCacheManager", ZIP_SLIP_ZIP);
} }
@Test @Test
public void testNormalZip() throws IOException { 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()); TerminologyCacheManager.unzip( normalInputStream, tempDir.toFile().getAbsolutePath());
Path expectedFilePath = tempDir.resolve("zip-normal").resolve("depth1").resolve("test.txt"); Path expectedFilePath = tempDir.resolve("zip-normal").resolve("depth1").resolve("test.txt");
@ -43,36 +40,26 @@ public class TerminologyCacheManagerTests implements ResourceLoaderTests {
assertEquals("dummy file content", actualContent); assertEquals("dummy file content", actualContent);
} }
@Test public static Stream<Arguments> zipSlipData() {
public void testSlipZip() throws IOException {
RuntimeException thrown = Assertions.assertThrows(RuntimeException.class, () -> { return Stream.of(
InputStream slipInputStream = getResourceAsInputStream( "terminologyCacheManager", ZIP_SLIP_ZIP); Arguments.of("zip-slip.zip", "../evil.txt"),
TerminologyCacheManager.unzip( slipInputStream, tempDir.toFile().getAbsolutePath()); Arguments.of("zip-slip-2.zip", "child/../../evil.txt"),
//Code under test Arguments.of("zip-slip-peer.zip", "../childpeer/evil.txt"),
}); Arguments.of("zip-slip-win.zip", "../evil.txt")
assertNotNull(thrown); );
assertEquals("Entry with an illegal path: ../evil.txt", thrown.getMessage());
} }
@Test @ParameterizedTest(name = "{index}: file {0}")
public void testSlip2Zip() throws IOException { @MethodSource("zipSlipData")
public void testLoadFromClasspathZipSlip(String fileName, String expectedMessage) {
RuntimeException thrown = Assertions.assertThrows(RuntimeException.class, () -> { RuntimeException thrown = Assertions.assertThrows(RuntimeException.class, () -> {
InputStream slipInputStream = getResourceAsInputStream( "terminologyCacheManager", ZIP_SLIP_2_ZIP); InputStream slipInputStream = getResourceAsInputStream( "zip-slip", fileName);
TerminologyCacheManager.unzip( slipInputStream, tempDir.toFile().getAbsolutePath()); TerminologyCacheManager.unzip( slipInputStream, tempDir.toFile().getAbsolutePath());
//Code under test //Code under test
}); });
assertNotNull(thrown); assertNotNull(thrown);
assertEquals("Entry with an illegal path: child/../../evil.txt", thrown.getMessage()); Assertions.assertTrue(thrown.getMessage().endsWith(expectedMessage));
}
@Test
public void testSlipZipWin() throws IOException {
RuntimeException thrown = Assertions.assertThrows(RuntimeException.class, () -> {
InputStream slipInputStream = getResourceAsInputStream( "terminologyCacheManager", ZIP_SLIP_WIN_ZIP);
TerminologyCacheManager.unzip( slipInputStream, tempDir.toFile().getAbsolutePath());
//Code under test
});
assertNotNull(thrown);
assertEquals("Entry with an illegal path: ../evil.txt", thrown.getMessage());
} }
} }

View File

@ -613,39 +613,33 @@ public class Utilities {
return s.toString(); return s.toString();
} }
private static boolean isPathRoot(String pathString) {
boolean actual;
Path path = Path.of(pathString);
Path normalizedPath = path.normalize();
actual = normalizedPath.equals(path.getRoot());
return actual;
}
public static String path(String... args) throws IOException { public static String path(String... args) throws IOException {
StringBuilder s = new StringBuilder(); StringBuilder s = new StringBuilder();
boolean d = false; boolean argIsNotEmptyOrNull = false;
boolean first = true;
if (args[0] == null || noString(args[0].trim())) {
throw new RuntimeException("First entry cannot be null or empty");
}
if (isPathRoot(args[0])) {
throw new RuntimeException("First entry cannot be root: " + args[0]);
}
for (String arg : args) { for (String arg : args) {
if (first && arg == null) if (!argIsNotEmptyOrNull)
continue; argIsNotEmptyOrNull = !noString(arg);
first = false;
if (!d)
d = !noString(arg);
else if (!s.toString().endsWith(File.separator)) else if (!s.toString().endsWith(File.separator))
s.append(File.separator); s.append(File.separator);
String a = arg; String a = arg;
if (s.length() == 0) { if (s.length() == 0) {
if ("[tmp]".equals(a)) { a = replaceVariables(a);
if (hasCTempDir()) {
a = C_TEMP_DIR;
} else if (ToolGlobalSettings.hasTempPath()) {
a = ToolGlobalSettings.getTempPath();
} else {
a = System.getProperty("java.io.tmpdir");
}
} else if ("[user]".equals(a)) {
a = System.getProperty("user.home");
} else if (a.startsWith("[") && a.endsWith("]")) {
String ev = System.getenv(a.replace("[", "").replace("]", ""));
if (ev != null) {
a = ev;
} else {
a = "null";
}
}
} }
a = a.replace("\\", File.separator); a = a.replace("\\", File.separator);
a = a.replace("/", File.separator); a = a.replace("/", File.separator);
@ -671,9 +665,34 @@ public class Utilities {
} else } else
s.append(a); s.append(a);
} }
if (!Path.of(s.toString()).normalize().startsWith(Path.of(replaceVariables(args[0])).normalize())) {
throw new RuntimeException("Computed path does not start with first element: " + String.join(", ", args));
}
return s.toString(); return s.toString();
} }
private static String replaceVariables(String a) {
if ("[tmp]".equals(a)) {
if (hasCTempDir()) {
return C_TEMP_DIR;
} else if (ToolGlobalSettings.hasTempPath()) {
return ToolGlobalSettings.getTempPath();
} else {
return System.getProperty("java.io.tmpdir");
}
} else if ("[user]".equals(a)) {
return System.getProperty("user.home");
} else if (a.startsWith("[") && a.endsWith("]")) {
String ev = System.getenv(a.replace("[", "").replace("]", ""));
if (ev != null) {
return ev;
} else {
return "null";
}
}
return a;
}
private static boolean hasCTempDir() { private static boolean hasCTempDir() {
if (!System.getProperty("os.name").toLowerCase().contains("win")) { if (!System.getProperty("os.name").toLowerCase().contains("win")) {
return false; return false;

View File

@ -1,5 +1,6 @@
package org.hl7.fhir.utilities; package org.hl7.fhir.utilities;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.File; import java.io.File;
@ -7,11 +8,17 @@ import java.io.IOException;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.text.MessageFormat; import java.text.MessageFormat;
import java.util.Random; import java.util.Random;
import java.util.stream.Stream;
import org.apache.commons.lang3.SystemUtils; import org.apache.commons.lang3.SystemUtils;
import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledOnOs;
import org.junit.jupiter.api.condition.OS;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
class UtilitiesTest { class UtilitiesTest {
@ -39,22 +46,22 @@ class UtilitiesTest {
@DisplayName("Test Utilities.path maps temp directory correctly") @DisplayName("Test Utilities.path maps temp directory correctly")
public void testTempDirPath() throws IOException { public void testTempDirPath() throws IOException {
if (ToolGlobalSettings.hasTempPath()) { if (ToolGlobalSettings.hasTempPath()) {
Assertions.assertEquals(Utilities.path("[tmp]", TEST_TXT), ToolGlobalSettings.getTempPath() +File.separator+ TEST_TXT); assertEquals(Utilities.path("[tmp]", TEST_TXT), ToolGlobalSettings.getTempPath() +File.separator+ TEST_TXT);
} else { } else {
Assertions.assertEquals(Utilities.path("[tmp]", TEST_TXT), getTempDirectory() + TEST_TXT); assertEquals(Utilities.path("[tmp]", TEST_TXT), getTempDirectory() + TEST_TXT);
} }
} }
@Test @Test
@DisplayName("Test Utilities.path maps user directory correctly") @DisplayName("Test Utilities.path maps user directory correctly")
public void testUserDirPath() throws IOException { public void testUserDirPath() throws IOException {
Assertions.assertEquals(Utilities.path("[user]", TEST_TXT), getUserDirectory() + TEST_TXT); assertEquals(Utilities.path("[user]", TEST_TXT), getUserDirectory() + TEST_TXT);
} }
@Test @Test
@DisplayName("Test Utilities.path maps JAVA_HOME correctly") @DisplayName("Test Utilities.path maps JAVA_HOME correctly")
public void testJavaHomeDirPath() throws IOException { public void testJavaHomeDirPath() throws IOException {
Assertions.assertEquals(Utilities.path("[JAVA_HOME]", TEST_TXT), getJavaHomeDirectory() + TEST_TXT); assertEquals(Utilities.path("[JAVA_HOME]", TEST_TXT), getJavaHomeDirectory() + TEST_TXT);
} }
private String getJavaHomeDirectory() { private String getJavaHomeDirectory() {
@ -171,24 +178,24 @@ class UtilitiesTest {
@Test @Test
@DisplayName("Decimal Reasoning Tests") @DisplayName("Decimal Reasoning Tests")
void testDecimalRoutines() { void testDecimalRoutines() {
Assertions.assertEquals("-0.500000", Utilities.lowBoundaryForDecimal("0", 6)); assertEquals("-0.500000", Utilities.lowBoundaryForDecimal("0", 6));
Assertions.assertEquals("0.50000000", Utilities.lowBoundaryForDecimal("1", 8)); assertEquals("0.50000000", Utilities.lowBoundaryForDecimal("1", 8));
Assertions.assertEquals("0.950000", Utilities.lowBoundaryForDecimal("1.0", 6)); assertEquals("0.950000", Utilities.lowBoundaryForDecimal("1.0", 6));
Assertions.assertEquals("0.95", Utilities.lowBoundaryForDecimal("1.0", 2)); assertEquals("0.95", Utilities.lowBoundaryForDecimal("1.0", 2));
Assertions.assertEquals("-1.05000000", Utilities.lowBoundaryForDecimal("-1.0", 8)); assertEquals("-1.05000000", Utilities.lowBoundaryForDecimal("-1.0", 8));
Assertions.assertEquals("1.23", Utilities.lowBoundaryForDecimal("1.234", 2)); assertEquals("1.23", Utilities.lowBoundaryForDecimal("1.234", 2));
Assertions.assertEquals("1.57", Utilities.lowBoundaryForDecimal("1.567", 2)); assertEquals("1.57", Utilities.lowBoundaryForDecimal("1.567", 2));
Assertions.assertEquals("0.50000000", Utilities.highBoundaryForDecimal("0", 8)); assertEquals("0.50000000", Utilities.highBoundaryForDecimal("0", 8));
Assertions.assertEquals("1.500000", Utilities.highBoundaryForDecimal("1", 6)); assertEquals("1.500000", Utilities.highBoundaryForDecimal("1", 6));
Assertions.assertEquals("1.0500000000", Utilities.highBoundaryForDecimal("1.0", 10)); assertEquals("1.0500000000", Utilities.highBoundaryForDecimal("1.0", 10));
Assertions.assertEquals("-0.9500", Utilities.highBoundaryForDecimal("-1.0", 4)); assertEquals("-0.9500", Utilities.highBoundaryForDecimal("-1.0", 4));
Assertions.assertEquals(0, Utilities.getDecimalPrecision("0")); assertEquals(0, Utilities.getDecimalPrecision("0"));
Assertions.assertEquals(0, Utilities.getDecimalPrecision("1")); assertEquals(0, Utilities.getDecimalPrecision("1"));
Assertions.assertEquals(1, Utilities.getDecimalPrecision("1.0")); assertEquals(1, Utilities.getDecimalPrecision("1.0"));
Assertions.assertEquals(1, Utilities.getDecimalPrecision("-1.0")); assertEquals(1, Utilities.getDecimalPrecision("-1.0"));
Assertions.assertEquals(4, Utilities.getDecimalPrecision("-1.0200")); assertEquals(4, Utilities.getDecimalPrecision("-1.0200"));
} }
@Test @Test
@ -212,23 +219,172 @@ class UtilitiesTest {
// Assertions.assertEquals("2021-04-04T21:22:23.999Z", Utilities.highBoundaryForDate("2021-04-04T21:22:23Z")); // Assertions.assertEquals("2021-04-04T21:22:23.999Z", Utilities.highBoundaryForDate("2021-04-04T21:22:23Z"));
// Assertions.assertEquals("2021-04-04T21:22:23.245+10:00", Utilities.highBoundaryForDate("2021-04-04T21:22:23.245+10:00")); // Assertions.assertEquals("2021-04-04T21:22:23.245+10:00", Utilities.highBoundaryForDate("2021-04-04T21:22:23.245+10:00"));
Assertions.assertEquals(8, Utilities.getDatePrecision("1900-01-01")); assertEquals(8, Utilities.getDatePrecision("1900-01-01"));
Assertions.assertEquals(4, Utilities.getDatePrecision("1900")); assertEquals(4, Utilities.getDatePrecision("1900"));
Assertions.assertEquals(6, Utilities.getDatePrecision("1900-06")); assertEquals(6, Utilities.getDatePrecision("1900-06"));
Assertions.assertEquals(14, Utilities.getDatePrecision("1900-06-06T14:00:00")); assertEquals(14, Utilities.getDatePrecision("1900-06-06T14:00:00"));
Assertions.assertEquals(17, Utilities.getDatePrecision("1900-06-06T14:00:00.000")); assertEquals(17, Utilities.getDatePrecision("1900-06-06T14:00:00.000"));
Assertions.assertEquals(8, Utilities.getDatePrecision("1900-01-01Z")); assertEquals(8, Utilities.getDatePrecision("1900-01-01Z"));
Assertions.assertEquals(4, Utilities.getDatePrecision("1900Z")); assertEquals(4, Utilities.getDatePrecision("1900Z"));
Assertions.assertEquals(6, Utilities.getDatePrecision("1900-06Z")); assertEquals(6, Utilities.getDatePrecision("1900-06Z"));
Assertions.assertEquals(14, Utilities.getDatePrecision("1900-06-06T14:00:00Z")); assertEquals(14, Utilities.getDatePrecision("1900-06-06T14:00:00Z"));
Assertions.assertEquals(17, Utilities.getDatePrecision("1900-06-06T14:00:00.000Z")); assertEquals(17, Utilities.getDatePrecision("1900-06-06T14:00:00.000Z"));
Assertions.assertEquals(8, Utilities.getDatePrecision("1900-01-01+10:00")); assertEquals(8, Utilities.getDatePrecision("1900-01-01+10:00"));
Assertions.assertEquals(4, Utilities.getDatePrecision("1900+10:00")); assertEquals(4, Utilities.getDatePrecision("1900+10:00"));
Assertions.assertEquals(6, Utilities.getDatePrecision("1900-06+10:00")); assertEquals(6, Utilities.getDatePrecision("1900-06+10:00"));
Assertions.assertEquals(14, Utilities.getDatePrecision("1900-06-06T14:00:00+10:00")); assertEquals(14, Utilities.getDatePrecision("1900-06-06T14:00:00+10:00"));
Assertions.assertEquals(17, Utilities.getDatePrecision("1900-06-06T14:00:00.000-10:00")); assertEquals(17, Utilities.getDatePrecision("1900-06-06T14:00:00.000-10:00"));
} }
public static Stream<Arguments> 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<Arguments> 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<Arguments> 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<Arguments> 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<Arguments> 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<Arguments> 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 @Test
@DisplayName("trimWS tests") @DisplayName("trimWS tests")
@ -266,5 +422,4 @@ class UtilitiesTest {
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("^.+$")); 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("^.+$"));
} }
}
}

View File

@ -6,6 +6,7 @@ import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
@ -374,16 +375,19 @@ public class IgLoader {
protected Map<String, byte[]> readZip(InputStream stream) throws IOException { protected Map<String, byte[]> readZip(InputStream stream) throws IOException {
Map<String, byte[]> res = new HashMap<>(); Map<String, byte[]> res = new HashMap<>();
ZipInputStream zip = new ZipInputStream(stream); ZipInputStream zip = new ZipInputStream(stream);
ZipEntry ze; ZipEntry zipEntry;
while ((ze = zip.getNextEntry()) != null) { while ((zipEntry = zip.getNextEntry()) != null) {
String name = ze.getName(); String entryName = zipEntry.getName();
if (entryName.contains("..") || Path.of(entryName).isAbsolute()) {
throw new RuntimeException("Entry with an illegal path: " + entryName);
}
ByteArrayOutputStream b = new ByteArrayOutputStream(); ByteArrayOutputStream b = new ByteArrayOutputStream();
int n; int n;
byte[] buf = new byte[1024]; byte[] buf = new byte[1024];
while ((n = ((InputStream) zip).read(buf, 0, 1024)) > -1) { while ((n = ((InputStream) zip).read(buf, 0, 1024)) > -1) {
b.write(buf, 0, n); b.write(buf, 0, n);
} }
res.put(name, b.toByteArray()); res.put(entryName, b.toByteArray());
zip.closeEntry(); zip.closeEntry();
} }
zip.close(); zip.close();

View File

@ -35,11 +35,13 @@ import java.io.FileInputStream;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import javax.annotation.Nonnull;
import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.DocumentBuilderFactory;
@ -478,7 +480,7 @@ public class UtilitiesXTests {
public static boolean findTestResource(String... paths) throws IOException { public static boolean findTestResource(String... paths) throws IOException {
if (new File("../../fhir-test-cases").exists() && isTryToLoadFromFileSystem()) { 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(); return new File(n).exists();
} else { } else {
String classpath = ("/org/hl7/fhir/testcases/"+ Utilities.pathURL(paths)); String classpath = ("/org/hl7/fhir/testcases/"+ Utilities.pathURL(paths));
@ -498,7 +500,7 @@ public class UtilitiesXTests {
public static String loadTestResource(String... paths) throws IOException { public static String loadTestResource(String... paths) throws IOException {
if (new File("../../fhir-test-cases").exists() && isTryToLoadFromFileSystem()) { 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 // ok, we'll resolve this locally
return TextFile.fileToString(new File(n)); return TextFile.fileToString(new File(n));
} else { } else {
@ -515,9 +517,14 @@ public class UtilitiesXTests {
} }
} }
@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 { public static InputStream loadTestResourceStream(String... paths) throws IOException {
if (new File("../../fhir-test-cases").exists() && isTryToLoadFromFileSystem()) { 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); return new FileInputStream(n);
} else { } else {
String classpath = ("/org/hl7/fhir/testcases/"+ Utilities.pathURL(paths)); String classpath = ("/org/hl7/fhir/testcases/"+ Utilities.pathURL(paths));
@ -565,4 +572,4 @@ public class UtilitiesXTests {
return path; return path;
} }
} }
} }

View File

@ -4,6 +4,7 @@ import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.r5.context.SimpleWorkerContext; import org.hl7.fhir.r5.context.SimpleWorkerContext;
import org.hl7.fhir.r5.model.ImplementationGuide; import org.hl7.fhir.r5.model.ImplementationGuide;
import org.hl7.fhir.utilities.npm.FilesystemPackageCacheManager; import org.hl7.fhir.utilities.npm.FilesystemPackageCacheManager;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.ParameterizedTest;
@ -14,15 +15,13 @@ import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.*; import java.util.*;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.stream.Stream; import java.util.stream.Stream;
import static org.junit.Assert.assertEquals; import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertLinesMatch;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class) @ExtendWith(MockitoExtension.class)
@ -97,4 +96,43 @@ public class IgLoaderTests {
assertLinesMatch(Arrays.asList(".*Unsupported FHIR Version.*"), Arrays.asList(exception.getMessage())); assertLinesMatch(Arrays.asList(".*Unsupported FHIR Version.*"), Arrays.asList(exception.getMessage()));
} }
public static Stream<Arguments> 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<String, byte[]> 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);
}
} }

View File

@ -5,10 +5,14 @@ import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance; 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.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; 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.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull; 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_NORMAL_ZIP = "zip-normal.zip";
public static final String ZIP_SLIP_ZIP = "zip-slip.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_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"; public static final String ZIP_SLIP_WIN_ZIP = "zip-slip-win.zip";
Path tempDir; Path tempDir;
@ -28,6 +32,8 @@ public class ScannerTest implements ResourceLoaderTests {
Path zipSlip2Path; Path zipSlip2Path;
Path zipSlipPeerPath;
Path zipSlipWinPath; Path zipSlipWinPath;
@BeforeAll @BeforeAll
@ -37,12 +43,14 @@ public class ScannerTest implements ResourceLoaderTests {
zipNormalPath = tempDir.resolve(ZIP_NORMAL_ZIP); zipNormalPath = tempDir.resolve(ZIP_NORMAL_ZIP);
zipSlipPath = tempDir.resolve(ZIP_SLIP_ZIP); zipSlipPath = tempDir.resolve(ZIP_SLIP_ZIP);
zipSlip2Path = tempDir.resolve(ZIP_SLIP_2_ZIP); zipSlip2Path = tempDir.resolve(ZIP_SLIP_2_ZIP);
zipSlipPeerPath = tempDir.resolve(ZIP_SLIP_PEER_ZIP);
zipSlipWinPath = tempDir.resolve(ZIP_SLIP_WIN_ZIP); zipSlipWinPath = tempDir.resolve(ZIP_SLIP_WIN_ZIP);
copyResourceToFile(zipNormalPath, "scanner", ZIP_NORMAL_ZIP); copyResourceToFile(zipNormalPath, "zip-slip", ZIP_NORMAL_ZIP);
copyResourceToFile(zipSlipPath, "scanner", ZIP_SLIP_ZIP); copyResourceToFile(zipSlipPath, "zip-slip", ZIP_SLIP_ZIP);
copyResourceToFile(zipSlip2Path, "scanner", ZIP_SLIP_2_ZIP); copyResourceToFile(zipSlip2Path, "zip-slip", ZIP_SLIP_2_ZIP);
copyResourceToFile(zipSlipWinPath, "scanner", ZIP_SLIP_WIN_ZIP); copyResourceToFile(zipSlipPeerPath, "zip-slip", ZIP_SLIP_PEER_ZIP);
copyResourceToFile(zipSlipWinPath, "zip-slip", ZIP_SLIP_WIN_ZIP);
} }
@Test @Test
@ -55,36 +63,25 @@ public class ScannerTest implements ResourceLoaderTests {
assertEquals("dummy file content", actualContent); assertEquals("dummy file content", actualContent);
} }
@Test public Stream<Arguments> zipSlipData() {
public void testSlipZip() throws IOException {
RuntimeException thrown = Assertions.assertThrows(RuntimeException.class, () -> { return Stream.of(
Scanner scanner = new Scanner(null,null,null,null); Arguments.of(zipSlipPath, "Entry with an illegal path: ../evil.txt"),
scanner.unzip(zipSlipPath.toFile().getAbsolutePath(), tempDir.toFile().getAbsolutePath()); Arguments.of(zipSlip2Path, "Entry with an illegal path: child/../../evil.txt"),
//Code under test Arguments.of(zipSlipPeerPath, "Entry with an illegal path: ../childpeer/evil.txt"),
}); Arguments.of(zipSlipWinPath, "Entry with an illegal path: ../evil.txt")
assertNotNull(thrown); );
assertEquals("Entry with an illegal path: ../evil.txt", thrown.getMessage());
} }
@Test @ParameterizedTest(name = "{index}: file {0}")
public void testSlipZip2() throws IOException { @MethodSource("zipSlipData")
public void testUnzipZipSlip(Path path, String expectedMessage) {
RuntimeException thrown = Assertions.assertThrows(RuntimeException.class, () -> { RuntimeException thrown = Assertions.assertThrows(RuntimeException.class, () -> {
Scanner scanner = new Scanner(null,null,null,null); Scanner scanner = new Scanner(null,null,null,null);
scanner.unzip(zipSlip2Path.toFile().getAbsolutePath(), tempDir.toFile().getAbsolutePath()); scanner.unzip(path.toFile().getAbsolutePath(), tempDir.toFile().getAbsolutePath());
//Code under test
}); });
assertNotNull(thrown); assertNotNull(thrown);
assertEquals("Entry with an illegal path: child/../../evil.txt", thrown.getMessage()); assertEquals(expectedMessage, thrown.getMessage());
} }
@Test
public void testSlipZipWin() throws IOException {
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
});
assertNotNull(thrown);
assertEquals("Entry with an illegal path: ../evil.txt", thrown.getMessage());
}
} }