Fix TestpageOverlay XSS Vulnerability (#2027)

* Resolve XSS vulnerability

* Add changelog
This commit is contained in:
James Agnew 2020-08-09 07:06:11 -04:00 committed by GitHub
parent 63ef2ce006
commit 0de0b88aa0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 64 additions and 64 deletions

View File

@ -79,7 +79,7 @@
<dependency> <dependency>
<groupId>com.google.guava</groupId> <groupId>com.google.guava</groupId>
<artifactId>guava</artifactId> <artifactId>guava</artifactId>
<version>21.0</version> <version>24.1.1</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.google.inject.extensions</groupId> <groupId>com.google.inject.extensions</groupId>

View File

@ -16,10 +16,16 @@ import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;
import java.net.URLDecoder; import java.net.URLDecoder;
import java.util.*; import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.StringTokenizer;
import static org.apache.commons.lang3.StringUtils.*; import static org.apache.commons.lang3.StringUtils.defaultIfBlank;
import static org.apache.commons.lang3.StringUtils.defaultString;
import static org.apache.commons.lang3.StringUtils.isBlank;
/* /*
* #%L * #%L
@ -370,7 +376,7 @@ public class UrlUtil {
/** /**
* This method specifically HTML-encodes the &quot; and * This method specifically HTML-encodes the &quot; and
* &lt; characters in order to prevent injection attacks. * &lt; characters in order to prevent injection attacks.
* * <p>
* The following characters are escaped: * The following characters are escaped:
* <ul> * <ul>
* <li>&apos;</li> * <li>&apos;</li>
@ -379,7 +385,6 @@ public class UrlUtil {
* <li>&gt;</li> * <li>&gt;</li>
* <li>\n (newline)</li> * <li>\n (newline)</li>
* </ul> * </ul>
*
*/ */
public static String sanitizeUrlPart(CharSequence theString) { public static String sanitizeUrlPart(CharSequence theString) {
if (theString == null) { if (theString == null) {
@ -432,6 +437,21 @@ public class UrlUtil {
return theString.toString(); return theString.toString();
} }
/**
* Applies the same logic as {@link #sanitizeUrlPart(CharSequence)} but against an array, returning an array with the
* same strings as the input but with sanitization applied
*/
public static String[] sanitizeUrlPart(String[] theParameterValues) {
String[] retVal = null;
if (theParameterValues != null) {
retVal = new String[theParameterValues.length];
for (int i = 0; i < theParameterValues.length; i++) {
retVal[i] = sanitizeUrlPart(theParameterValues[i]);
}
}
return retVal;
}
private static Map<String, String[]> toQueryStringMap(HashMap<String, List<String>> map) { private static Map<String, String[]> toQueryStringMap(HashMap<String, List<String>> map) {
HashMap<String, String[]> retVal = new HashMap<>(); HashMap<String, String[]> retVal = new HashMap<>();
for (Entry<String, List<String>> nextEntry : map.entrySet()) { for (Entry<String, List<String>> nextEntry : map.entrySet()) {

View File

@ -0,0 +1,8 @@
---
type: security
issue: 2026
title: "An XSS vulnerability was reported in the HAPI FHIR Testpage Overlay module. Thanks to Will Davison of NCC Group (Manchester UK) for disclosing this vulnerability.
Users of the HAPI FHIR Testpage Overlay can use a specially crafted URL to exploit an XSS vulnerability in this module, allowing arbitrary JavaScript to be executed in the user's browser. The impact of this vulnerability is believed to be low, as this module is intended for testing and not believed to be widely used for any production purposes. Nonetheless, we recommend all users of the affected module upgrade immediately.
A complete audit of the affected codebase has been completed in order to detect and resolve any similar issues."

View File

@ -41,6 +41,7 @@ import java.io.UnsupportedEncodingException;
import java.net.URLDecoder; import java.net.URLDecoder;
import java.util.*; import java.util.*;
import static ca.uhn.fhir.util.UrlUtil.sanitizeUrlPart;
import static org.apache.commons.lang3.StringUtils.defaultString; import static org.apache.commons.lang3.StringUtils.defaultString;
public class BaseController { public class BaseController {
@ -49,7 +50,7 @@ public class BaseController {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseController.class); private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseController.class);
@Autowired @Autowired
protected TesterConfig myConfig; protected TesterConfig myConfig;
private Map<FhirVersionEnum, FhirContext> myContexts = new HashMap<FhirVersionEnum, FhirContext>(); private final Map<FhirVersionEnum, FhirContext> myContexts = new HashMap<FhirVersionEnum, FhirContext>();
private List<String> myFilterHeaders; private List<String> myFilterHeaders;
@Autowired @Autowired
private ITemplateEngine myTemplateEngine; private ITemplateEngine myTemplateEngine;
@ -78,19 +79,6 @@ public class BaseController {
return loadAndAddConf(theServletRequest, theRequest, theModel); return loadAndAddConf(theServletRequest, theRequest, theModel);
} }
private Header[] applyHeaderFilters(Header[] theAllHeaders) {
if (myFilterHeaders == null || myFilterHeaders.isEmpty()) {
return theAllHeaders;
}
ArrayList<Header> retVal = new ArrayList<Header>();
for (Header next : theAllHeaders) {
if (!myFilterHeaders.contains(next.getName().toLowerCase())) {
retVal.add(next);
}
}
return retVal.toArray(new Header[retVal.size()]);
}
private Header[] applyHeaderFilters(Map<String, List<String>> theAllHeaders) { private Header[] applyHeaderFilters(Map<String, List<String>> theAllHeaders) {
ArrayList<Header> retVal = new ArrayList<Header>(); ArrayList<Header> retVal = new ArrayList<Header>();
for (String nextKey : theAllHeaders.keySet()) { for (String nextKey : theAllHeaders.keySet()) {
@ -274,7 +262,7 @@ public class BaseController {
} }
protected RuntimeResourceDefinition getResourceType(HomeRequest theRequest, HttpServletRequest theReq) throws ServletException { protected RuntimeResourceDefinition getResourceType(HomeRequest theRequest, HttpServletRequest theReq) throws ServletException {
String resourceName = StringUtils.defaultString(theReq.getParameter(PARAM_RESOURCE)); String resourceName = sanitizeUrlPart(defaultString(theReq.getParameter(PARAM_RESOURCE)));
RuntimeResourceDefinition def = getContext(theRequest).getResourceDefinition(resourceName); RuntimeResourceDefinition def = getContext(theRequest).getResourceDefinition(resourceName);
if (def == null) { if (def == null) {
throw new ServletException("Invalid resourceName: " + resourceName); throw new ServletException("Invalid resourceName: " + resourceName);
@ -317,7 +305,7 @@ public class BaseController {
ca.uhn.fhir.model.dstu2.resource.Conformance conformance; ca.uhn.fhir.model.dstu2.resource.Conformance conformance;
try { try {
conformance = (ca.uhn.fhir.model.dstu2.resource.Conformance) client.fetchConformance().ofType(Conformance.class).execute(); conformance = client.fetchConformance().ofType(Conformance.class).execute();
} catch (Exception e) { } catch (Exception e) {
ourLog.warn("Failed to load conformance statement, error was: {}", e.toString()); ourLog.warn("Failed to load conformance statement, error was: {}", e.toString());
theModel.put("errorMsg", toDisplayError("Failed to load conformance statement, error was: " + e.toString(), e)); theModel.put("errorMsg", toDisplayError("Failed to load conformance statement, error was: " + e.toString(), e));
@ -326,7 +314,7 @@ public class BaseController {
theModel.put("jsonEncodedConf", getContext(theRequest).newJsonParser().encodeResourceToString(conformance)); theModel.put("jsonEncodedConf", getContext(theRequest).newJsonParser().encodeResourceToString(conformance));
Map<String, Number> resourceCounts = new HashMap<String, Number>(); Map<String, Number> resourceCounts = new HashMap<>();
long total = 0; long total = 0;
for (ca.uhn.fhir.model.dstu2.resource.Conformance.Rest nextRest : conformance.getRest()) { for (ca.uhn.fhir.model.dstu2.resource.Conformance.Rest nextRest : conformance.getRest()) {
for (ca.uhn.fhir.model.dstu2.resource.Conformance.RestResource nextResource : nextRest.getResource()) { for (ca.uhn.fhir.model.dstu2.resource.Conformance.RestResource nextResource : nextRest.getResource()) {
@ -385,7 +373,7 @@ public class BaseController {
theModel.put("jsonEncodedConf", getContext(theRequest).newJsonParser().encodeResourceToString(capabilityStatement)); theModel.put("jsonEncodedConf", getContext(theRequest).newJsonParser().encodeResourceToString(capabilityStatement));
Map<String, Number> resourceCounts = new HashMap<String, Number>(); Map<String, Number> resourceCounts = new HashMap<>();
long total = 0; long total = 0;
for (CapabilityStatementRestComponent nextRest : capabilityStatement.getRest()) { for (CapabilityStatementRestComponent nextRest : capabilityStatement.getRest()) {
@ -446,7 +434,7 @@ public class BaseController {
theModel.put("jsonEncodedConf", getContext(theRequest).newJsonParser().encodeResourceToString(capabilityStatement)); theModel.put("jsonEncodedConf", getContext(theRequest).newJsonParser().encodeResourceToString(capabilityStatement));
Map<String, Number> resourceCounts = new HashMap<String, Number>(); Map<String, Number> resourceCounts = new HashMap<>();
long total = 0; long total = 0;
for (org.hl7.fhir.r4.model.CapabilityStatement.CapabilityStatementRestComponent nextRest : capabilityStatement.getRest()) { for (org.hl7.fhir.r4.model.CapabilityStatement.CapabilityStatementRestComponent nextRest : capabilityStatement.getRest()) {
@ -507,7 +495,7 @@ public class BaseController {
theModel.put("jsonEncodedConf", getContext(theRequest).newJsonParser().encodeResourceToString(capabilityStatement)); theModel.put("jsonEncodedConf", getContext(theRequest).newJsonParser().encodeResourceToString(capabilityStatement));
Map<String, Number> resourceCounts = new HashMap<String, Number>(); Map<String, Number> resourceCounts = new HashMap<>();
long total = 0; long total = 0;
for (org.hl7.fhir.r5.model.CapabilityStatement.CapabilityStatementRestComponent nextRest : capabilityStatement.getRest()) { for (org.hl7.fhir.r5.model.CapabilityStatement.CapabilityStatementRestComponent nextRest : capabilityStatement.getRest()) {
@ -614,25 +602,6 @@ public class BaseController {
protected void processAndAddLastClientInvocation(GenericClient theClient, ResultType theResultType, ModelMap theModelMap, long theLatency, String outcomeDescription, protected void processAndAddLastClientInvocation(GenericClient theClient, ResultType theResultType, ModelMap theModelMap, long theLatency, String outcomeDescription,
CaptureInterceptor theInterceptor, HomeRequest theRequest) { CaptureInterceptor theInterceptor, HomeRequest theRequest) {
try { try {
// ApacheHttpRequest lastRequest = theInterceptor.getLastRequest();
// HttpResponse lastResponse = theInterceptor.getLastResponse();
// String requestBody = null;
// String requestUrl = lastRequest != null ? lastRequest.getApacheRequest().getURI().toASCIIString() : null;
// String action = lastRequest != null ? lastRequest.getApacheRequest().getMethod() : null;
// String resultStatus = lastResponse != null ? lastResponse.getStatusLine().toString() : null;
// String resultBody = StringUtils.defaultString(theInterceptor.getLastResponseBody());
//
// if (lastRequest instanceof HttpEntityEnclosingRequest) {
// HttpEntity entity = ((HttpEntityEnclosingRequest) lastRequest).getEntity();
// if (entity.isRepeatable()) {
// requestBody = IOUtils.toString(entity.getContent());
// }
// }
//
// ContentType ct = lastResponse != null ? ContentType.get(lastResponse.getEntity()) : null;
// String mimeType = ct != null ? ct.getMimeType() : null;
IHttpRequest lastRequest = theInterceptor.getLastRequest(); IHttpRequest lastRequest = theInterceptor.getLastRequest();
IHttpResponse lastResponse = theInterceptor.getLastResponse(); IHttpResponse lastResponse = theInterceptor.getLastResponse();
String requestBody = null; String requestBody = null;

View File

@ -26,6 +26,7 @@ import ca.uhn.fhir.rest.gclient.TokenClientParam;
import ca.uhn.fhir.to.model.HomeRequest; import ca.uhn.fhir.to.model.HomeRequest;
import ca.uhn.fhir.to.model.ResourceRequest; import ca.uhn.fhir.to.model.ResourceRequest;
import ca.uhn.fhir.to.model.TransactionRequest; import ca.uhn.fhir.to.model.TransactionRequest;
import ca.uhn.fhir.util.UrlUtil;
import com.google.gson.stream.JsonWriter; import com.google.gson.stream.JsonWriter;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.dstu3.model.CapabilityStatement; import org.hl7.fhir.dstu3.model.CapabilityStatement;
@ -49,6 +50,7 @@ import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.TreeSet; import java.util.TreeSet;
import static ca.uhn.fhir.util.UrlUtil.sanitizeUrlPart;
import static org.apache.commons.lang3.StringUtils.defaultIfEmpty; import static org.apache.commons.lang3.StringUtils.defaultIfEmpty;
import static org.apache.commons.lang3.StringUtils.defaultString; import static org.apache.commons.lang3.StringUtils.defaultString;
import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isBlank;
@ -128,7 +130,7 @@ public class Controller extends BaseController {
return "resource"; return "resource";
} }
String id = StringUtils.defaultString(theServletRequest.getParameter("resource-delete-id")); String id = sanitizeUrlPart(defaultString(theServletRequest.getParameter("resource-delete-id")));
if (StringUtils.isBlank(id)) { if (StringUtils.isBlank(id)) {
populateModelForResource(theServletRequest, theRequest, theModel); populateModelForResource(theServletRequest, theRequest, theModel);
theModel.put("errorMsg", toDisplayError("No ID specified", null)); theModel.put("errorMsg", toDisplayError("No ID specified", null));
@ -184,7 +186,7 @@ public class Controller extends BaseController {
FhirContext context = getContext(theRequest); FhirContext context = getContext(theRequest);
GenericClient client = theRequest.newClient(theReq, context, myConfig, interceptor); GenericClient client = theRequest.newClient(theReq, context, myConfig, interceptor);
String url = defaultString(theReq.getParameter("page-url")); String url = sanitizeUrlPart(defaultString(theReq.getParameter("page-url")));
if (myConfig.isRefuseToFetchThirdPartyUrls()) { if (myConfig.isRefuseToFetchThirdPartyUrls()) {
if (!url.startsWith(theModel.get("base").toString())) { if (!url.startsWith(theModel.get("base").toString())) {
ourLog.warn(logPrefix(theModel) + "Refusing to load page URL: {}", url); ourLog.warn(logPrefix(theModel) + "Refusing to load page URL: {}", url);
@ -230,7 +232,7 @@ public class Controller extends BaseController {
theModel.put("errorMsg", toDisplayError(e.toString(), e)); theModel.put("errorMsg", toDisplayError(e.toString(), e));
return "resource"; return "resource";
} }
String id = StringUtils.defaultString(theServletRequest.getParameter("id")); String id = sanitizeUrlPart(defaultString(theServletRequest.getParameter("id")));
if (StringUtils.isBlank(id)) { if (StringUtils.isBlank(id)) {
populateModelForResource(theServletRequest, theRequest, theModel); populateModelForResource(theServletRequest, theRequest, theModel);
theModel.put("errorMsg", toDisplayError("No ID specified", null)); theModel.put("errorMsg", toDisplayError("No ID specified", null));
@ -238,7 +240,7 @@ public class Controller extends BaseController {
} }
ResultType returnsResource = ResultType.RESOURCE; ResultType returnsResource = ResultType.RESOURCE;
String versionId = StringUtils.defaultString(theServletRequest.getParameter("vid")); String versionId = sanitizeUrlPart(defaultString(theServletRequest.getParameter("vid")));
String outcomeDescription; String outcomeDescription;
if (StringUtils.isBlank(versionId)) { if (StringUtils.isBlank(versionId)) {
versionId = null; versionId = null;
@ -353,7 +355,7 @@ public class Controller extends BaseController {
return "resource"; return "resource";
} }
clientCodeJsonWriter.name("resource"); clientCodeJsonWriter.name("resource");
clientCodeJsonWriter.value(theServletRequest.getParameter("resource")); clientCodeJsonWriter.value(sanitizeUrlPart(theServletRequest.getParameter("resource")));
} else { } else {
query = search.forAllResources(); query = search.forAllResources();
clientCodeJsonWriter.name("resource"); clientCodeJsonWriter.name("resource");
@ -394,7 +396,7 @@ public class Controller extends BaseController {
clientCodeJsonWriter.name("includes"); clientCodeJsonWriter.name("includes");
clientCodeJsonWriter.beginArray(); clientCodeJsonWriter.beginArray();
String[] incValues = theServletRequest.getParameterValues(Constants.PARAM_INCLUDE); String[] incValues = sanitizeUrlPart(theServletRequest.getParameterValues(Constants.PARAM_INCLUDE));
if (incValues != null) { if (incValues != null) {
for (String next : incValues) { for (String next : incValues) {
if (isNotBlank(next)) { if (isNotBlank(next)) {
@ -407,7 +409,7 @@ public class Controller extends BaseController {
clientCodeJsonWriter.name("revincludes"); clientCodeJsonWriter.name("revincludes");
clientCodeJsonWriter.beginArray(); clientCodeJsonWriter.beginArray();
String[] revIncValues = theServletRequest.getParameterValues(Constants.PARAM_REVINCLUDE); String[] revIncValues = sanitizeUrlPart(theServletRequest.getParameterValues(Constants.PARAM_REVINCLUDE));
if (revIncValues != null) { if (revIncValues != null) {
for (String next : revIncValues) { for (String next : revIncValues) {
if (isNotBlank(next)) { if (isNotBlank(next)) {
@ -418,7 +420,7 @@ public class Controller extends BaseController {
} }
clientCodeJsonWriter.endArray(); clientCodeJsonWriter.endArray();
String limit = theServletRequest.getParameter("resource-search-limit"); String limit = sanitizeUrlPart(theServletRequest.getParameter("resource-search-limit"));
if (isNotBlank(limit)) { if (isNotBlank(limit)) {
if (!limit.matches("[0-9]+")) { if (!limit.matches("[0-9]+")) {
populateModelForResource(theServletRequest, theRequest, theModel); populateModelForResource(theServletRequest, theRequest, theModel);
@ -434,13 +436,13 @@ public class Controller extends BaseController {
clientCodeJsonWriter.nullValue(); clientCodeJsonWriter.nullValue();
} }
String[] sort = theServletRequest.getParameterValues("sort_by"); String[] sort = sanitizeUrlPart(theServletRequest.getParameterValues("sort_by"));
if (sort != null) { if (sort != null) {
for (String next : sort) { for (String next : sort) {
if (isBlank(next)) { if (isBlank(next)) {
continue; continue;
} }
String direction = theServletRequest.getParameter("sort_direction"); String direction = sanitizeUrlPart(theServletRequest.getParameter("sort_direction"));
if ("asc".equals(direction)) { if ("asc".equals(direction)) {
query.sort().ascending(new StringClientParam(next)); query.sort().ascending(new StringClientParam(next));
} else if ("desc".equals(direction)) { } else if ("desc".equals(direction)) {
@ -545,6 +547,7 @@ public class Controller extends BaseController {
type = def.getImplementingClass(); type = def.getImplementingClass();
} }
// Don't sanitize this param, it's a raw resource body and may well be XML
String body = validate ? theReq.getParameter("resource-validate-body") : theReq.getParameter("resource-create-body"); String body = validate ? theReq.getParameter("resource-validate-body") : theReq.getParameter("resource-create-body");
if (isBlank(body)) { if (isBlank(body)) {
theModel.put("errorMsg", toDisplayError("No message body specified", null)); theModel.put("errorMsg", toDisplayError("No message body specified", null));
@ -583,7 +586,7 @@ public class Controller extends BaseController {
outcomeDescription = "Validate Resource"; outcomeDescription = "Validate Resource";
client.validate().resource(resource).prettyPrint().execute(); client.validate().resource(resource).prettyPrint().execute();
} else { } else {
String id = theReq.getParameter("resource-create-id"); String id = sanitizeUrlPart(theReq.getParameter("resource-create-id"));
if ("update".equals(theMethod)) { if ("update".equals(theMethod)) {
outcomeDescription = "Update Resource"; outcomeDescription = "Update Resource";
client.update(id, resource); client.update(id, resource);
@ -626,17 +629,17 @@ public class Controller extends BaseController {
if ("history-type".equals(theMethod)) { if ("history-type".equals(theMethod)) {
RuntimeResourceDefinition def = getContext(theRequest).getResourceDefinition(theRequest.getResource()); RuntimeResourceDefinition def = getContext(theRequest).getResourceDefinition(theRequest.getResource());
type = def.getImplementingClass(); type = def.getImplementingClass();
id = StringUtils.defaultString(theReq.getParameter("resource-history-id")); id = sanitizeUrlPart(defaultString(theReq.getParameter("resource-history-id")));
} }
DateTimeDt since = null; DateTimeDt since = null;
String sinceStr = theReq.getParameter("since"); String sinceStr = sanitizeUrlPart(theReq.getParameter("since"));
if (isNotBlank(sinceStr)) { if (isNotBlank(sinceStr)) {
since = new DateTimeDt(sinceStr); since = new DateTimeDt(sinceStr);
} }
Integer limit = null; Integer limit = null;
String limitStr = theReq.getParameter("limit"); String limitStr = sanitizeUrlPart(theReq.getParameter("limit"));
if (isNotBlank(limitStr)) { if (isNotBlank(limitStr)) {
limit = Integer.parseInt(limitStr); limit = Integer.parseInt(limitStr);
} }
@ -811,17 +814,17 @@ public class Controller extends BaseController {
} }
private boolean handleSearchParam(String paramIdxString, HttpServletRequest theReq, IQuery theQuery, JsonWriter theClientCodeJsonWriter) throws IOException { private boolean handleSearchParam(String paramIdxString, HttpServletRequest theReq, IQuery theQuery, JsonWriter theClientCodeJsonWriter) throws IOException {
String nextName = theReq.getParameter("param." + paramIdxString + ".name"); String nextName = sanitizeUrlPart(theReq.getParameter("param." + paramIdxString + ".name"));
if (isBlank(nextName)) { if (isBlank(nextName)) {
return false; return false;
} }
String nextQualifier = StringUtils.defaultString(theReq.getParameter("param." + paramIdxString + ".qualifier")); String nextQualifier = sanitizeUrlPart(defaultString(theReq.getParameter("param." + paramIdxString + ".qualifier")));
String nextType = theReq.getParameter("param." + paramIdxString + ".type"); String nextType = sanitizeUrlPart(theReq.getParameter("param." + paramIdxString + ".type"));
List<String> parts = new ArrayList<String>(); List<String> parts = new ArrayList<String>();
for (int i = 0; i < 5; i++) { for (int i = 0; i < 5; i++) {
parts.add(defaultString(theReq.getParameter("param." + paramIdxString + "." + i))); parts.add(sanitizeUrlPart(defaultString(theReq.getParameter("param." + paramIdxString + "." + i))));
} }
List<String> values; List<String> values;

View File

@ -48,13 +48,13 @@ public class FhirTesterConfig {
.addServer() .addServer()
.withId("hapi") .withId("hapi")
.withFhirVersion(FhirVersionEnum.DSTU2) .withFhirVersion(FhirVersionEnum.DSTU2)
.withBaseUrl("http://fhirtest.uhn.ca/baseDstu2") .withBaseUrl("http://hapi.fhir.org/baseDstu2")
.withName("Public HAPI Test Server") .withName("Public HAPI Test Server")
.allowsApiKey() .allowsApiKey()
.addServer() .addServer()
.withId("home3") .withId("home3")
.withFhirVersion(FhirVersionEnum.DSTU3) .withFhirVersion(FhirVersionEnum.DSTU3)
.withBaseUrl("http://fhirtest.uhn.ca/baseDstu3") .withBaseUrl("http://hapi.fhir.org/baseDstu3")
.withName("Public HAPI Test Server (STU3)") .withName("Public HAPI Test Server (STU3)")
.addServer() .addServer()
.withId("home") .withId("home")