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 {
ZipInputStream zip = new ZipInputStream(stream);
ZipEntry ze;
while ((ze = zip.getNextEntry()) != null) {
loadDefinitionItem(ze.getName(), zip, loader, null, null);
ZipEntry zipEntry;
while ((zipEntry = zip.getNextEntry()) != null) {
String entryName = zipEntry.getName();
if (entryName.contains("..")) {
throw new RuntimeException("Entry with an illegal path: " + entryName);
}
loadDefinitionItem(entryName, zip, loader, null, null);
zip.closeEntry();
}
zip.close();

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.XhtmlNode;
import javax.annotation.Nonnull;
public class QuestionnaireRenderer extends TerminologyRenderer {
public static final String EXT_QUESTIONNAIRE_ITEM_TYPE_ORIGINAL = "http://hl7.org/fhir/tools/StructureDefinition/original-item-type";
@ -251,28 +253,28 @@ public class QuestionnaireRenderer extends TerminologyRenderer {
Cell flags = gen.new Cell();
r.getCells().add(flags);
if (i.getReadOnly()) {
flags.addPiece(gen.new Piece(Utilities.pathURL(context.getSpecificationLink(), "questionnaire-definitions.html#Questionnaire.item.readOnly"), null, "Is Readonly").addHtml(new XhtmlNode(NodeType.Element, "img").attribute("src", Utilities.path(context.getLocalPrefix(), "icon-qi-readonly.png"))));
flags.addPiece(gen.new Piece(Utilities.pathURL(context.getSpecificationLink(), "questionnaire-definitions.html#Questionnaire.item.readOnly"), null, "Is Readonly").addHtml(new XhtmlNode(NodeType.Element, "img").attribute("src", getImgPath("icon-qi-readonly.png"))));
}
if (ToolingExtensions.readBoolExtension(i, "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-isSubject")) {
flags.addPiece(gen.new Piece(getSDCLink("StructureDefinition-sdc-questionnaire-isSubject.html"), null, "Can change the subject of the questionnaire").addHtml(new XhtmlNode(NodeType.Element, "img").attribute("src", Utilities.path(context.getLocalPrefix(), "icon-qi-subject.png"))));
flags.addPiece(gen.new Piece(getSDCLink("StructureDefinition-sdc-questionnaire-isSubject.html"), null, "Can change the subject of the questionnaire").addHtml(new XhtmlNode(NodeType.Element, "img").attribute("src", getImgPath("icon-qi-subject.png"))));
}
if (ToolingExtensions.readBoolExtension(i, "http://hl7.org/fhir/StructureDefinition/questionnaire-hidden")) {
flags.addPiece(gen.new Piece(getSpecLink("extension-questionnaire-hidden.html"), null, "Is a hidden item").addHtml(new XhtmlNode(NodeType.Element, "img").attribute("src", Utilities.path(context.getLocalPrefix(), "icon-qi-hidden.png"))));
flags.addPiece(gen.new Piece(getSpecLink("extension-questionnaire-hidden.html"), null, "Is a hidden item").addHtml(new XhtmlNode(NodeType.Element, "img").attribute("src", getImgPath("icon-qi-hidden.png"))));
}
if (ToolingExtensions.readBoolExtension(i, "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-optionalDisplay")) {
flags.addPiece(gen.new Piece(getSDCLink("StructureDefinition-sdc-questionnaire-optionalDisplay.html"), null, "Is optional to display").addHtml(new XhtmlNode(NodeType.Element, "img").attribute("src", Utilities.path(context.getLocalPrefix(), "icon-qi-optional.png"))));
flags.addPiece(gen.new Piece(getSDCLink("StructureDefinition-sdc-questionnaire-optionalDisplay.html"), null, "Is optional to display").addHtml(new XhtmlNode(NodeType.Element, "img").attribute("src", getImgPath("icon-qi-optional.png"))));
}
if (i.hasExtension("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-observationLinkPeriod")) {
flags.addPiece(gen.new Piece(getSDCLink("StructureDefinition-sdc-questionnaire-observationLinkPeriod.html"), null, "Is linked to an observation").addHtml(new XhtmlNode(NodeType.Element, "img").attribute("src", Utilities.path(context.getLocalPrefix(), "icon-qi-observation.png"))));
flags.addPiece(gen.new Piece(getSDCLink("StructureDefinition-sdc-questionnaire-observationLinkPeriod.html"), null, "Is linked to an observation").addHtml(new XhtmlNode(NodeType.Element, "img").attribute("src", getImgPath("icon-qi-observation.png"))));
}
if (i.hasExtension("http://hl7.org/fhir/StructureDefinition/questionnaire-choiceOrientation")) {
String code = ToolingExtensions.readStringExtension(i, "http://hl7.org/fhir/StructureDefinition/questionnaire-choiceOrientation");
flags.addPiece(gen.new Piece(getSpecLink("extension-questionnaire-choiceorientation.html"), null, "Orientation: "+code).addHtml(new XhtmlNode(NodeType.Element, "img").attribute("src", Utilities.path(context.getLocalPrefix(), "icon-qi-"+code+".png"))));
flags.addPiece(gen.new Piece(getSpecLink("extension-questionnaire-choiceorientation.html"), null, "Orientation: "+code).addHtml(new XhtmlNode(NodeType.Element, "img").attribute("src", getImgPath("icon-qi-" + code + ".png"))));
}
if (i.hasExtension("http://hl7.org/fhir/StructureDefinition/questionnaire-displayCategory")) {
CodeableConcept cc = i.getExtensionByUrl("http://hl7.org/fhir/StructureDefinition/questionnaire-displayCategory").getValueCodeableConcept();
String code = cc.getCode("http://hl7.org/fhir/questionnaire-display-category");
flags.addPiece(gen.new Piece(getSDCLink("StructureDefinition-sdc-questionnaire-displayCategory.html"), null, "Category: "+code).addHtml(new XhtmlNode(NodeType.Element, "img").attribute("src", Utilities.path(context.getLocalPrefix(), "icon-qi-"+code+".png"))));
flags.addPiece(gen.new Piece(getSDCLink("StructureDefinition-sdc-questionnaire-displayCategory.html"), null, "Category: "+code).addHtml(new XhtmlNode(NodeType.Element, "img").attribute("src", getImgPath("icon-qi-" + code + ".png"))));
}
}
Cell defn = gen.new Cell();
@ -687,26 +689,26 @@ public class QuestionnaireRenderer extends TerminologyRenderer {
if (ToolingExtensions.readBoolExtension(i, "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-isSubject")) {
hasFlag = true;
flags.ah(getSDCLink("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-isSubject"), "Can change the subject of the questionnaire").img(Utilities.path(context.getLocalPrefix(), "icon-qi-subject.png"), "icon");
flags.ah(getSDCLink("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-isSubject"), "Can change the subject of the questionnaire").img(getImgPath("icon-qi-subject.png"), "icon");
}
if (ToolingExtensions.readBoolExtension(i, "http://hl7.org/fhir/StructureDefinition/questionnaire-hidden")) {
hasFlag = true;
flags.ah(Utilities.pathURL(context.getSpecificationLink(), "extension-questionnaire-hidden.html"), "Is a hidden item").img(Utilities.path(context.getLocalPrefix(), "icon-qi-hidden.png"), "icon");
flags.ah(Utilities.pathURL(context.getSpecificationLink(), "extension-questionnaire-hidden.html"), "Is a hidden item").img(getImgPath("icon-qi-hidden.png"), "icon");
d.style("background-color: #eeeeee");
}
if (ToolingExtensions.readBoolExtension(i, "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-optionalDisplay")) {
hasFlag = true;
flags.ah(getSDCLink("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-optionalDisplay"), "Is optional to display").img(Utilities.path(context.getLocalPrefix(), "icon-qi-optional.png"), "icon");
flags.ah(getSDCLink("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-optionalDisplay"), "Is optional to display").img(getImgPath("icon-qi-optional.png"), "icon");
}
if (i.hasExtension("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-observationLinkPeriod")) {
hasFlag = true;
flags.ah(getSDCLink("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-observationLinkPeriod"), "Is linked to an observation").img(Utilities.path(context.getLocalPrefix(), "icon-qi-observation.png"), "icon");
flags.ah(getSDCLink("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-observationLinkPeriod"), "Is linked to an observation").img(getImgPath("icon-qi-observation.png"), "icon");
}
if (i.hasExtension("http://hl7.org/fhir/StructureDefinition/questionnaire-displayCategory")) {
CodeableConcept cc = i.getExtensionByUrl("http://hl7.org/fhir/StructureDefinition/questionnaire-displayCategory").getValueCodeableConcept();
String code = cc.getCode("http://hl7.org/fhir/questionnaire-display-category");
hasFlag = true;
flags.ah(getSDCLink("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-displayCategory"), "Category: "+code).img(Utilities.path(context.getLocalPrefix(), "icon-qi-"+code+".png"), "icon");
flags.ah(getSDCLink("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-displayCategory"), "Category: "+code).img(getImgPath("icon-qi-" + code + ".png"), "icon");
}
if (i.hasMaxLength()) {
@ -788,6 +790,13 @@ public class QuestionnaireRenderer extends TerminologyRenderer {
return hasExt;
}
@Nonnull
private String getImgPath(String code) throws IOException {
return context.getLocalPrefix().length() > 0
? Utilities.path(context.getLocalPrefix(), code)
: Utilities.path(code);
}
private void item(XhtmlNode ul, String name, String value, String valueLink) {
if (!Utilities.noString(value)) {
ul.li().style("font-size: 10px").ah(valueLink).tx(name+": "+value);
@ -862,7 +871,7 @@ public class QuestionnaireRenderer extends TerminologyRenderer {
boolean ext = false;
XhtmlNode td = tbl.tr().td("structure").colspan("2").span(null, null).attribute("class", "self-link-parent");
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.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");
td.an("item."+qi.getLinkId());
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.img(Utilities.path(context.getLocalPrefix(), "icon_q_item.png"), "icon");
td.img(getImgPath("icon_q_item.png"), "icon");
td.tx(" Item ");
td.b().tx(qi.getLinkId());

View File

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

View File

@ -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 {
BufferedInputStream input = new BufferedInputStream(stream);
XmlPullParserFactory factory = XmlPullParserFactory.newInstance(System.getProperty(XmlPullParserFactory.PROPERTY_NAME), null);

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

View File

@ -520,9 +520,13 @@ public class SimpleWorkerContext extends BaseWorkerContext implements IWorkerCon
private void loadFromStream(InputStream stream, IContextResourceLoader loader) throws IOException, FHIRException {
ZipInputStream zip = new ZipInputStream(stream);
ZipEntry ze;
while ((ze = zip.getNextEntry()) != null) {
loadDefinitionItem(ze.getName(), zip, loader, null, null);
ZipEntry zipEntry;
while ((zipEntry = zip.getNextEntry()) != null) {
String entryName = zipEntry.getName();
if (entryName.contains("..")) {
throw new RuntimeException("Entry with an illegal path: " + entryName);
}
loadDefinitionItem(entryName, zip, loader, null, null);
zip.closeEntry();
}
zip.close();

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.XhtmlNode;
import javax.annotation.Nonnull;
public class QuestionnaireRenderer extends TerminologyRenderer {
public static final String EXT_QUESTIONNAIRE_ITEM_TYPE_ORIGINAL = "http://hl7.org/fhir/tools/StructureDefinition/original-item-type";
@ -255,28 +257,28 @@ public class QuestionnaireRenderer extends TerminologyRenderer {
Cell flags = gen.new Cell();
r.getCells().add(flags);
if (i.getReadOnly()) {
flags.addPiece(gen.new Piece(Utilities.pathURL(context.getLink(KnownLinkType.SPEC), "questionnaire-definitions.html#Questionnaire.item.readOnly"), null, "Is Readonly").addHtml(new XhtmlNode(NodeType.Element, "img").attribute("alt", "icon").attribute("src", Utilities.path(context.getLocalPrefix(), "icon-qi-readonly.png"))));
flags.addPiece(gen.new Piece(Utilities.pathURL(context.getLink(KnownLinkType.SPEC), "questionnaire-definitions.html#Questionnaire.item.readOnly"), null, "Is Readonly").addHtml(new XhtmlNode(NodeType.Element, "img").attribute("alt", "icon").attribute("src", getImgPath("icon-qi-readonly.png"))));
}
if (ToolingExtensions.readBoolExtension(i, "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-isSubject")) {
flags.addPiece(gen.new Piece(getSDCLink("StructureDefinition-sdc-questionnaire-isSubject.html"), null, "Can change the subject of the questionnaire").addHtml(new XhtmlNode(NodeType.Element, "img").attribute("alt", "icon").attribute("src", Utilities.path(context.getLocalPrefix(), "icon-qi-subject.png"))));
flags.addPiece(gen.new Piece(getSDCLink("StructureDefinition-sdc-questionnaire-isSubject.html"), null, "Can change the subject of the questionnaire").addHtml(new XhtmlNode(NodeType.Element, "img").attribute("alt", "icon").attribute("src", getImgPath("icon-qi-subject.png"))));
}
if (ToolingExtensions.readBoolExtension(i, ToolingExtensions.EXT_Q_HIDDEN)) {
flags.addPiece(gen.new Piece(getSpecLink("extension-questionnaire-hidden.html"), null, "Is a hidden item").addHtml(new XhtmlNode(NodeType.Element, "img").attribute("alt", "icon").attribute("src", Utilities.path(context.getLocalPrefix(), "icon-qi-hidden.png"))));
flags.addPiece(gen.new Piece(getSpecLink("extension-questionnaire-hidden.html"), null, "Is a hidden item").addHtml(new XhtmlNode(NodeType.Element, "img").attribute("alt", "icon").attribute("src", getImgPath("icon-qi-hidden.png"))));
}
if (ToolingExtensions.readBoolExtension(i, ToolingExtensions.EXT_Q_OTP_DISP)) {
flags.addPiece(gen.new Piece(getSDCLink("StructureDefinition-sdc-questionnaire-optionalDisplay.html"), null, "Is optional to display").addHtml(new XhtmlNode(NodeType.Element, "img").attribute("alt", "icon").attribute("src", Utilities.path(context.getLocalPrefix(), "icon-qi-optional.png"))));
flags.addPiece(gen.new Piece(getSDCLink("StructureDefinition-sdc-questionnaire-optionalDisplay.html"), null, "Is optional to display").addHtml(new XhtmlNode(NodeType.Element, "img").attribute("alt", "icon").attribute("src", getImgPath("icon-qi-optional.png"))));
}
if (i.hasExtension("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-observationLinkPeriod")) {
flags.addPiece(gen.new Piece(getSDCLink("StructureDefinition-sdc-questionnaire-observationLinkPeriod.html"), null, "Is linked to an observation").addHtml(new XhtmlNode(NodeType.Element, "img").attribute("alt", "icon").attribute("src", Utilities.path(context.getLocalPrefix(), "icon-qi-observation.png"))));
flags.addPiece(gen.new Piece(getSDCLink("StructureDefinition-sdc-questionnaire-observationLinkPeriod.html"), null, "Is linked to an observation").addHtml(new XhtmlNode(NodeType.Element, "img").attribute("alt", "icon").attribute("src", getImgPath("icon-qi-observation.png"))));
}
if (i.hasExtension(ToolingExtensions.EXT_Q_CHOICE_ORIENT)) {
String code = ToolingExtensions.readStringExtension(i, ToolingExtensions.EXT_Q_CHOICE_ORIENT);
flags.addPiece(gen.new Piece(getSpecLink("extension-questionnaire-choiceorientation.html"), null, "Orientation: "+code).addHtml(new XhtmlNode(NodeType.Element, "img").attribute("alt", "icon").attribute("src", Utilities.path(context.getLocalPrefix(), "icon-qi-"+code+".png"))));
flags.addPiece(gen.new Piece(getSpecLink("extension-questionnaire-choiceorientation.html"), null, "Orientation: "+code).addHtml(new XhtmlNode(NodeType.Element, "img").attribute("alt", "icon").attribute("src", getImgPath("icon-qi-" + code + ".png"))));
}
if (i.hasExtension(ToolingExtensions.EXT_Q_DISPLAY_CAT)) {
CodeableConcept cc = i.getExtensionByUrl(ToolingExtensions.EXT_Q_DISPLAY_CAT).getValueCodeableConcept();
String code = cc.getCode("http://hl7.org/fhir/questionnaire-display-category");
flags.addPiece(gen.new Piece(getSDCLink("StructureDefinition-sdc-questionnaire-displayCategory.html"), null, "Category: "+code).addHtml(new XhtmlNode(NodeType.Element, "img").attribute("alt", "icon").attribute("src", Utilities.path(context.getLocalPrefix(), "icon-qi-"+code+".png"))));
flags.addPiece(gen.new Piece(getSDCLink("StructureDefinition-sdc-questionnaire-displayCategory.html"), null, "Category: "+code).addHtml(new XhtmlNode(NodeType.Element, "img").attribute("alt", "icon").attribute("src", getImgPath("icon-qi-" + code + ".png"))));
}
}
Cell defn = gen.new Cell();
@ -690,26 +692,26 @@ public class QuestionnaireRenderer extends TerminologyRenderer {
if (ToolingExtensions.readBoolExtension(i, "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-isSubject")) {
hasFlag = true;
flags.ah(getSDCLink("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-isSubject"), "Can change the subject of the questionnaire").img(Utilities.path(context.getLocalPrefix(), "icon-qi-subject.png"), "icon");
flags.ah(getSDCLink("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-isSubject"), "Can change the subject of the questionnaire").img(getImgPath("icon-qi-subject.png"), "icon");
}
if (ToolingExtensions.readBoolExtension(i, ToolingExtensions.EXT_Q_HIDDEN)) {
hasFlag = true;
flags.ah(Utilities.pathURL(context.getLink(KnownLinkType.SPEC), "extension-questionnaire-hidden.html"), "Is a hidden item").img(Utilities.path(context.getLocalPrefix(), "icon-qi-hidden.png"), "icon");
flags.ah(Utilities.pathURL(context.getLink(KnownLinkType.SPEC), "extension-questionnaire-hidden.html"), "Is a hidden item").img(getImgPath("icon-qi-hidden.png"), "icon");
d.style("background-color: #eeeeee");
}
if (ToolingExtensions.readBoolExtension(i, ToolingExtensions.EXT_Q_OTP_DISP)) {
hasFlag = true;
flags.ah(getSDCLink(ToolingExtensions.EXT_Q_OTP_DISP), "Is optional to display").img(Utilities.path(context.getLocalPrefix(), "icon-qi-optional.png"), "icon");
flags.ah(getSDCLink(ToolingExtensions.EXT_Q_OTP_DISP), "Is optional to display").img(getImgPath("icon-qi-optional.png"), "icon");
}
if (i.hasExtension("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-observationLinkPeriod")) {
hasFlag = true;
flags.ah(getSDCLink("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-observationLinkPeriod"), "Is linked to an observation").img(Utilities.path(context.getLocalPrefix(), "icon-qi-observation.png"), "icon");
flags.ah(getSDCLink("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-observationLinkPeriod"), "Is linked to an observation").img(getImgPath("icon-qi-observation.png"), "icon");
}
if (i.hasExtension(ToolingExtensions.EXT_Q_DISPLAY_CAT)) {
CodeableConcept cc = i.getExtensionByUrl(ToolingExtensions.EXT_Q_DISPLAY_CAT).getValueCodeableConcept();
String code = cc.getCode("http://hl7.org/fhir/questionnaire-display-category");
hasFlag = true;
flags.ah(getSDCLink("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-displayCategory"), "Category: "+code).img(Utilities.path(context.getLocalPrefix(), "icon-qi-"+code+".png"), "icon");
flags.ah(getSDCLink("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-displayCategory"), "Category: "+code).img(getImgPath("icon-qi-" + code + ".png"), "icon");
}
if (i.hasMaxLength()) {
@ -791,6 +793,13 @@ public class QuestionnaireRenderer extends TerminologyRenderer {
return hasExt;
}
@Nonnull
private String getImgPath(String code) throws IOException {
return context.getLocalPrefix().length() > 0
? Utilities.path(context.getLocalPrefix(), code)
: Utilities.path(code);
}
private void item(XhtmlNode ul, String name, String value, String valueLink) {
if (!Utilities.noString(value)) {
ul.li().style("font-size: 10px").ah(valueLink).tx(name+": "+value);
@ -865,7 +874,7 @@ public class QuestionnaireRenderer extends TerminologyRenderer {
boolean ext = false;
XhtmlNode td = tbl.tr().td("structure").colspan("2").span(null, null).attribute("class", "self-link-parent");
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.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");
td.an("item."+qi.getLinkId());
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.img(Utilities.path(context.getLocalPrefix(), "icon_q_item.png"), "icon");
td.img(getImgPath("icon_q_item.png"), "icon");
td.tx(" Item ");
td.b().tx(qi.getLinkId());

View File

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

View File

@ -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 {
BufferedInputStream input = new BufferedInputStream(stream);
XmlPullParserFactory factory = XmlPullParserFactory.newInstance(System.getProperty(XmlPullParserFactory.PROPERTY_NAME), null);

View File

@ -1,6 +1,6 @@
package org.hl7.fhir.r5.context;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.argThat;
@ -9,9 +9,9 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.stream.Stream;
import org.hl7.fhir.r5.model.CapabilityStatement;
import org.hl7.fhir.r5.model.CodeableConcept;
@ -27,9 +27,13 @@ import org.hl7.fhir.r5.terminologies.ValueSetExpanderSimple;
import org.hl7.fhir.r5.utils.validation.ValidationContextCarrier;
import org.hl7.fhir.utilities.ToolingClientLogger;
import org.hl7.fhir.utilities.validation.ValidationOptions;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.ArgumentMatcher;
import org.mockito.Mock;
import org.mockito.Mockito;
@ -423,4 +427,31 @@ public class SimpleWorkerContextTests {
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,12 +5,16 @@ import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@ -18,24 +22,17 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class TerminologyCacheManagerTests implements ResourceLoaderTests {
public static final String ZIP_NORMAL_ZIP = "zip-normal.zip";
public static final String ZIP_SLIP_ZIP = "zip-slip.zip";
public static final String ZIP_SLIP_2_ZIP = "zip-slip-2.zip";
public static final String ZIP_SLIP_WIN_ZIP = "zip-slip-win.zip";
Path tempDir;
@BeforeAll
public void beforeAll() throws IOException {
tempDir = Files.createTempDirectory("terminology-cache-manager");
tempDir.resolve("child").toFile().mkdir();
getResourceAsInputStream("terminologyCacheManager", ZIP_SLIP_ZIP);
}
@Test
public void testNormalZip() throws IOException {
InputStream normalInputStream = getResourceAsInputStream( "terminologyCacheManager", ZIP_NORMAL_ZIP);
InputStream normalInputStream = getResourceAsInputStream( "zip-slip", "zip-normal.zip");
TerminologyCacheManager.unzip( normalInputStream, tempDir.toFile().getAbsolutePath());
Path expectedFilePath = tempDir.resolve("zip-normal").resolve("depth1").resolve("test.txt");
@ -43,36 +40,26 @@ public class TerminologyCacheManagerTests implements ResourceLoaderTests {
assertEquals("dummy file content", actualContent);
}
@Test
public void testSlipZip() throws IOException {
RuntimeException thrown = Assertions.assertThrows(RuntimeException.class, () -> {
InputStream slipInputStream = getResourceAsInputStream( "terminologyCacheManager", ZIP_SLIP_ZIP);
TerminologyCacheManager.unzip( slipInputStream, tempDir.toFile().getAbsolutePath());
//Code under test
});
assertNotNull(thrown);
assertEquals("Entry with an illegal path: ../evil.txt", thrown.getMessage());
public static Stream<Arguments> zipSlipData() {
return Stream.of(
Arguments.of("zip-slip.zip", "../evil.txt"),
Arguments.of("zip-slip-2.zip", "child/../../evil.txt"),
Arguments.of("zip-slip-peer.zip", "../childpeer/evil.txt"),
Arguments.of("zip-slip-win.zip", "../evil.txt")
);
}
@Test
public void testSlip2Zip() throws IOException {
@ParameterizedTest(name = "{index}: file {0}")
@MethodSource("zipSlipData")
public void testLoadFromClasspathZipSlip(String fileName, String expectedMessage) {
RuntimeException thrown = Assertions.assertThrows(RuntimeException.class, () -> {
InputStream slipInputStream = getResourceAsInputStream( "terminologyCacheManager", ZIP_SLIP_2_ZIP);
InputStream slipInputStream = getResourceAsInputStream( "zip-slip", fileName);
TerminologyCacheManager.unzip( slipInputStream, tempDir.toFile().getAbsolutePath());
//Code under test
});
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();
}
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 {
StringBuilder s = new StringBuilder();
boolean d = false;
boolean first = true;
boolean argIsNotEmptyOrNull = false;
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) {
if (first && arg == null)
continue;
first = false;
if (!d)
d = !noString(arg);
if (!argIsNotEmptyOrNull)
argIsNotEmptyOrNull = !noString(arg);
else if (!s.toString().endsWith(File.separator))
s.append(File.separator);
String a = arg;
if (s.length() == 0) {
if ("[tmp]".equals(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 = replaceVariables(a);
}
a = a.replace("\\", File.separator);
a = a.replace("/", File.separator);
@ -671,9 +665,34 @@ public class Utilities {
} else
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();
}
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() {
if (!System.getProperty("os.name").toLowerCase().contains("win")) {
return false;

View File

@ -1,5 +1,6 @@
package org.hl7.fhir.utilities;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.File;
@ -7,11 +8,17 @@ import java.io.IOException;
import java.nio.file.Paths;
import java.text.MessageFormat;
import java.util.Random;
import java.util.stream.Stream;
import org.apache.commons.lang3.SystemUtils;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
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 {
@ -39,22 +46,22 @@ class UtilitiesTest {
@DisplayName("Test Utilities.path maps temp directory correctly")
public void testTempDirPath() throws IOException {
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 {
Assertions.assertEquals(Utilities.path("[tmp]", TEST_TXT), getTempDirectory() + TEST_TXT);
assertEquals(Utilities.path("[tmp]", TEST_TXT), getTempDirectory() + TEST_TXT);
}
}
@Test
@DisplayName("Test Utilities.path maps user directory correctly")
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
@DisplayName("Test Utilities.path maps JAVA_HOME correctly")
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() {
@ -171,24 +178,24 @@ class UtilitiesTest {
@Test
@DisplayName("Decimal Reasoning Tests")
void testDecimalRoutines() {
Assertions.assertEquals("-0.500000", Utilities.lowBoundaryForDecimal("0", 6));
Assertions.assertEquals("0.50000000", Utilities.lowBoundaryForDecimal("1", 8));
Assertions.assertEquals("0.950000", Utilities.lowBoundaryForDecimal("1.0", 6));
Assertions.assertEquals("0.95", Utilities.lowBoundaryForDecimal("1.0", 2));
Assertions.assertEquals("-1.05000000", Utilities.lowBoundaryForDecimal("-1.0", 8));
Assertions.assertEquals("1.23", Utilities.lowBoundaryForDecimal("1.234", 2));
Assertions.assertEquals("1.57", Utilities.lowBoundaryForDecimal("1.567", 2));
assertEquals("-0.500000", Utilities.lowBoundaryForDecimal("0", 6));
assertEquals("0.50000000", Utilities.lowBoundaryForDecimal("1", 8));
assertEquals("0.950000", Utilities.lowBoundaryForDecimal("1.0", 6));
assertEquals("0.95", Utilities.lowBoundaryForDecimal("1.0", 2));
assertEquals("-1.05000000", Utilities.lowBoundaryForDecimal("-1.0", 8));
assertEquals("1.23", Utilities.lowBoundaryForDecimal("1.234", 2));
assertEquals("1.57", Utilities.lowBoundaryForDecimal("1.567", 2));
Assertions.assertEquals("0.50000000", Utilities.highBoundaryForDecimal("0", 8));
Assertions.assertEquals("1.500000", Utilities.highBoundaryForDecimal("1", 6));
Assertions.assertEquals("1.0500000000", Utilities.highBoundaryForDecimal("1.0", 10));
Assertions.assertEquals("-0.9500", Utilities.highBoundaryForDecimal("-1.0", 4));
assertEquals("0.50000000", Utilities.highBoundaryForDecimal("0", 8));
assertEquals("1.500000", Utilities.highBoundaryForDecimal("1", 6));
assertEquals("1.0500000000", Utilities.highBoundaryForDecimal("1.0", 10));
assertEquals("-0.9500", Utilities.highBoundaryForDecimal("-1.0", 4));
Assertions.assertEquals(0, Utilities.getDecimalPrecision("0"));
Assertions.assertEquals(0, Utilities.getDecimalPrecision("1"));
Assertions.assertEquals(1, Utilities.getDecimalPrecision("1.0"));
Assertions.assertEquals(1, Utilities.getDecimalPrecision("-1.0"));
Assertions.assertEquals(4, Utilities.getDecimalPrecision("-1.0200"));
assertEquals(0, Utilities.getDecimalPrecision("0"));
assertEquals(0, Utilities.getDecimalPrecision("1"));
assertEquals(1, Utilities.getDecimalPrecision("1.0"));
assertEquals(1, Utilities.getDecimalPrecision("-1.0"));
assertEquals(4, Utilities.getDecimalPrecision("-1.0200"));
}
@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.245+10:00", Utilities.highBoundaryForDate("2021-04-04T21:22:23.245+10:00"));
Assertions.assertEquals(8, Utilities.getDatePrecision("1900-01-01"));
Assertions.assertEquals(4, Utilities.getDatePrecision("1900"));
Assertions.assertEquals(6, Utilities.getDatePrecision("1900-06"));
Assertions.assertEquals(14, Utilities.getDatePrecision("1900-06-06T14:00:00"));
Assertions.assertEquals(17, Utilities.getDatePrecision("1900-06-06T14:00:00.000"));
Assertions.assertEquals(8, Utilities.getDatePrecision("1900-01-01Z"));
Assertions.assertEquals(4, Utilities.getDatePrecision("1900Z"));
Assertions.assertEquals(6, Utilities.getDatePrecision("1900-06Z"));
Assertions.assertEquals(14, Utilities.getDatePrecision("1900-06-06T14:00:00Z"));
Assertions.assertEquals(17, Utilities.getDatePrecision("1900-06-06T14:00:00.000Z"));
Assertions.assertEquals(8, Utilities.getDatePrecision("1900-01-01+10:00"));
Assertions.assertEquals(4, Utilities.getDatePrecision("1900+10:00"));
Assertions.assertEquals(6, Utilities.getDatePrecision("1900-06+10:00"));
Assertions.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(8, Utilities.getDatePrecision("1900-01-01"));
assertEquals(4, Utilities.getDatePrecision("1900"));
assertEquals(6, Utilities.getDatePrecision("1900-06"));
assertEquals(14, Utilities.getDatePrecision("1900-06-06T14:00:00"));
assertEquals(17, Utilities.getDatePrecision("1900-06-06T14:00:00.000"));
assertEquals(8, Utilities.getDatePrecision("1900-01-01Z"));
assertEquals(4, Utilities.getDatePrecision("1900Z"));
assertEquals(6, Utilities.getDatePrecision("1900-06Z"));
assertEquals(14, Utilities.getDatePrecision("1900-06-06T14:00:00Z"));
assertEquals(17, Utilities.getDatePrecision("1900-06-06T14:00:00.000Z"));
assertEquals(8, Utilities.getDatePrecision("1900-01-01+10:00"));
assertEquals(4, Utilities.getDatePrecision("1900+10:00"));
assertEquals(6, Utilities.getDatePrecision("1900-06+10:00"));
assertEquals(14, Utilities.getDatePrecision("1900-06-06T14:00:00+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
@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("^.+$"));
}
}

View File

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

View File

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

View File

@ -4,6 +4,7 @@ import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.r5.context.SimpleWorkerContext;
import org.hl7.fhir.r5.model.ImplementationGuide;
import org.hl7.fhir.utilities.npm.FilesystemPackageCacheManager;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
@ -14,15 +15,13 @@ import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Paths;
import java.util.*;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import static org.junit.Assert.assertEquals;
import static org.junit.jupiter.api.Assertions.assertLinesMatch;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
@ -97,4 +96,43 @@ public class IgLoaderTests {
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.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@ -19,7 +23,7 @@ public class ScannerTest implements ResourceLoaderTests {
public static final String ZIP_NORMAL_ZIP = "zip-normal.zip";
public static final String ZIP_SLIP_ZIP = "zip-slip.zip";
public static final String ZIP_SLIP_2_ZIP = "zip-slip-2.zip";
public static final String ZIP_SLIP_PEER_ZIP = "zip-slip-peer.zip";
public static final String ZIP_SLIP_WIN_ZIP = "zip-slip-win.zip";
Path tempDir;
@ -28,6 +32,8 @@ public class ScannerTest implements ResourceLoaderTests {
Path zipSlip2Path;
Path zipSlipPeerPath;
Path zipSlipWinPath;
@BeforeAll
@ -37,12 +43,14 @@ public class ScannerTest implements ResourceLoaderTests {
zipNormalPath = tempDir.resolve(ZIP_NORMAL_ZIP);
zipSlipPath = tempDir.resolve(ZIP_SLIP_ZIP);
zipSlip2Path = tempDir.resolve(ZIP_SLIP_2_ZIP);
zipSlipPeerPath = tempDir.resolve(ZIP_SLIP_PEER_ZIP);
zipSlipWinPath = tempDir.resolve(ZIP_SLIP_WIN_ZIP);
copyResourceToFile(zipNormalPath, "scanner", ZIP_NORMAL_ZIP);
copyResourceToFile(zipSlipPath, "scanner", ZIP_SLIP_ZIP);
copyResourceToFile(zipSlip2Path, "scanner", ZIP_SLIP_2_ZIP);
copyResourceToFile(zipSlipWinPath, "scanner", ZIP_SLIP_WIN_ZIP);
copyResourceToFile(zipNormalPath, "zip-slip", ZIP_NORMAL_ZIP);
copyResourceToFile(zipSlipPath, "zip-slip", ZIP_SLIP_ZIP);
copyResourceToFile(zipSlip2Path, "zip-slip", ZIP_SLIP_2_ZIP);
copyResourceToFile(zipSlipPeerPath, "zip-slip", ZIP_SLIP_PEER_ZIP);
copyResourceToFile(zipSlipWinPath, "zip-slip", ZIP_SLIP_WIN_ZIP);
}
@Test
@ -55,36 +63,25 @@ public class ScannerTest implements ResourceLoaderTests {
assertEquals("dummy file content", actualContent);
}
@Test
public void testSlipZip() throws IOException {
RuntimeException thrown = Assertions.assertThrows(RuntimeException.class, () -> {
Scanner scanner = new Scanner(null,null,null,null);
scanner.unzip(zipSlipPath.toFile().getAbsolutePath(), tempDir.toFile().getAbsolutePath());
//Code under test
});
assertNotNull(thrown);
assertEquals("Entry with an illegal path: ../evil.txt", thrown.getMessage());
public Stream<Arguments> zipSlipData() {
return Stream.of(
Arguments.of(zipSlipPath, "Entry with an illegal path: ../evil.txt"),
Arguments.of(zipSlip2Path, "Entry with an illegal path: child/../../evil.txt"),
Arguments.of(zipSlipPeerPath, "Entry with an illegal path: ../childpeer/evil.txt"),
Arguments.of(zipSlipWinPath, "Entry with an illegal path: ../evil.txt")
);
}
@Test
public void testSlipZip2() throws IOException {
@ParameterizedTest(name = "{index}: file {0}")
@MethodSource("zipSlipData")
public void testUnzipZipSlip(Path path, String expectedMessage) {
RuntimeException thrown = Assertions.assertThrows(RuntimeException.class, () -> {
Scanner scanner = new Scanner(null,null,null,null);
scanner.unzip(zipSlip2Path.toFile().getAbsolutePath(), tempDir.toFile().getAbsolutePath());
//Code under test
scanner.unzip(path.toFile().getAbsolutePath(), tempDir.toFile().getAbsolutePath());
});
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());
}
}